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

pull/8757/head
Jacob Sandlund 2025-08-21 21:13:40 -04:00
commit 90832d89b3
63 changed files with 2519 additions and 920 deletions

View File

@ -47,7 +47,7 @@ jobs:
sentry-cli dif upload --project ghostty --wait dsym.zip
build-macos:
runs-on: namespace-profile-ghostty-macos-sequoia
runs-on: namespace-profile-ghostty-macos-tahoe
timeout-minutes: 90
steps:
- name: Checkout code
@ -57,9 +57,9 @@ jobs:
fetch-depth: 0
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: DeterminateSystems/nix-installer-action@main
with:
nix_path: nixpkgs=channel:nixos-unstable
determinate: true
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
@ -68,7 +68,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.6.4
SPARKLE_VERSION: 2.7.1
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@ -95,6 +95,7 @@ jobs:
run: |
cd macos
sudo xcode-select -s /Applications/Xcode_26.0.app
xcodebuild -version
xcodebuild -target Ghostty -configuration Release
# We inject the "build number" as simply the number of commits since HEAD.
@ -201,7 +202,7 @@ jobs:
destination-dir: ./
build-macos-debug:
runs-on: namespace-profile-ghostty-macos-sequoia
runs-on: namespace-profile-ghostty-macos-tahoe
timeout-minutes: 90
steps:
- name: Checkout code
@ -211,9 +212,9 @@ jobs:
fetch-depth: 0
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: DeterminateSystems/nix-installer-action@main
with:
nix_path: nixpkgs=channel:nixos-unstable
determinate: true
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
@ -222,7 +223,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.5.1
SPARKLE_VERSION: 2.7.1
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@ -249,6 +250,7 @@ jobs:
run: |
cd macos
sudo xcode-select -s /Applications/Xcode_26.0.app
xcodebuild -version
xcodebuild -target Ghostty -configuration Release
# We inject the "build number" as simply the number of commits since HEAD.

View File

@ -120,7 +120,7 @@ jobs:
build-macos:
needs: [setup]
runs-on: namespace-profile-ghostty-macos-sequoia
runs-on: namespace-profile-ghostty-macos-tahoe
timeout-minutes: 90
env:
GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
@ -130,9 +130,9 @@ jobs:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: DeterminateSystems/nix-installer-action@main
with:
nix_path: nixpkgs=channel:nixos-unstable
determinate: true
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
@ -141,9 +141,12 @@ jobs:
- name: XCode Select
run: sudo xcode-select -s /Applications/Xcode_16.4.app
- name: Xcode Version
run: xcodebuild -version
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.6.4
SPARKLE_VERSION: 2.7.1
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@ -291,7 +294,7 @@ jobs:
appcast:
needs: [setup, build-macos]
runs-on: namespace-profile-ghostty-macos-sequoia
runs-on: namespace-profile-ghostty-macos-tahoe
env:
GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
GHOSTTY_BUILD: ${{ needs.setup.outputs.build }}
@ -308,7 +311,7 @@ jobs:
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.6.4
SPARKLE_VERSION: 2.7.1
run: |
mkdir -p .action/sparkle
cd .action/sparkle

View File

@ -154,7 +154,7 @@ jobs:
)
}}
runs-on: namespace-profile-ghostty-macos-sequoia
runs-on: namespace-profile-ghostty-macos-tahoe
timeout-minutes: 90
steps:
- name: Checkout code
@ -163,10 +163,10 @@ jobs:
# Important so that build number generation works
fetch-depth: 0
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main
with:
nix_path: nixpkgs=channel:nixos-unstable
determinate: true
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
@ -181,7 +181,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.6.4
SPARKLE_VERSION: 2.7.1
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@ -374,7 +374,7 @@ jobs:
)
}}
runs-on: namespace-profile-ghostty-macos-sequoia
runs-on: namespace-profile-ghostty-macos-tahoe
timeout-minutes: 90
steps:
- name: Checkout code
@ -383,10 +383,10 @@ jobs:
# Important so that build number generation works
fetch-depth: 0
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main
with:
nix_path: nixpkgs=channel:nixos-unstable
determinate: true
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
@ -401,7 +401,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.5.1
SPARKLE_VERSION: 2.7.1
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@ -554,7 +554,7 @@ jobs:
)
}}
runs-on: namespace-profile-ghostty-macos-sequoia
runs-on: namespace-profile-ghostty-macos-tahoe
timeout-minutes: 90
steps:
- name: Checkout code
@ -563,10 +563,10 @@ jobs:
# Important so that build number generation works
fetch-depth: 0
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main
with:
nix_path: nixpkgs=channel:nixos-unstable
determinate: true
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
@ -581,7 +581,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.5.1
SPARKLE_VERSION: 2.7.1
run: |
mkdir -p .action/sparkle
cd .action/sparkle

View File

@ -18,11 +18,9 @@ jobs:
- build-nix
- build-snap
- build-macos
- build-macos-tahoe
- build-macos-matrix
- build-windows
- flatpak-check-zig-cache
- flatpak
- test
- test-gtk
- test-gtk-ng
@ -37,7 +35,9 @@ jobs:
- blueprint-compiler
- test-pkg-linux
- test-debian-13
- valgrind
- zig-fmt
- flatpak
steps:
- id: status
name: Determine status
@ -272,16 +272,16 @@ jobs:
ghostty-source.tar.gz
build-macos:
runs-on: namespace-profile-ghostty-macos-sequoia
runs-on: namespace-profile-ghostty-macos-tahoe
needs: test
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main
with:
nix_path: nixpkgs=channel:nixos-unstable
determinate: true
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
@ -314,7 +314,7 @@ jobs:
cd macos
xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO"
build-macos-tahoe:
build-macos-matrix:
runs-on: namespace-profile-ghostty-macos-tahoe
needs: test
steps:
@ -333,45 +333,8 @@ jobs:
- name: Xcode Select
run: sudo xcode-select -s /Applications/Xcode_26.0.app
- name: get the Zig deps
id: deps
run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT
# GhosttyKit is the framework that is built from Zig for our native
# Mac app to access.
- name: Build GhosttyKit
run: nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false
# The native app is built with native Xcode tooling. This also does
# codesigning. IMPORTANT: this must NOT run in a Nix environment.
# Nix breaks xcodebuild so this has to be run outside.
- name: Build Ghostty.app
run: cd macos && xcodebuild -target Ghostty
# Build the iOS target without code signing just to verify it works.
- name: Build Ghostty iOS
run: |
cd macos
xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO"
build-macos-matrix:
runs-on: namespace-profile-ghostty-macos-sequoia
needs: test
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Xcode Select
run: sudo xcode-select -s /Applications/Xcode_26.0.app
- name: Xcode Version
run: xcodebuild -version
- name: get the Zig deps
id: deps
@ -400,6 +363,7 @@ jobs:
os:
[namespace-profile-ghostty-snap, namespace-profile-ghostty-snap-arm64]
runs-on: ${{ matrix.os }}
timeout-minutes: 45
needs: [test, build-dist]
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
@ -434,6 +398,7 @@ jobs:
runs-on: windows-2022
# this will not stop other jobs from running
continue-on-error: true
timeout-minutes: 45
needs: test
steps:
- name: Checkout code
@ -671,16 +636,16 @@ jobs:
nix develop -c zig build -Dsentry=${{ matrix.sentry }}
test-macos:
runs-on: namespace-profile-ghostty-macos-sequoia
runs-on: namespace-profile-ghostty-macos-tahoe
needs: test
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main
with:
nix_path: nixpkgs=channel:nixos-unstable
determinate: true
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
@ -689,6 +654,9 @@ jobs:
- name: Xcode Select
run: sudo xcode-select -s /Applications/Xcode_26.0.app
- name: Xcode Version
run: xcodebuild -version
- name: get the Zig deps
id: deps
run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT
@ -1038,3 +1006,40 @@ jobs:
cache-key: flatpak-builder-${{ github.sha }}
arch: ${{ matrix.variant.arch }}
verbose: true
valgrind:
if: github.repository == 'ghostty-org/ghostty'
runs-on: namespace-profile-ghostty-lg
timeout-minutes: 30
needs: test
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: valgrind deps
run: |
sudo apt update -y
sudo apt install -y valgrind libc6-dbg
- name: valgrind
run: |
nix develop -c zig build test-valgrind

View File

@ -13,6 +13,40 @@ we can always convert that to an issue later.
> time to fixing bugs, maintaining features, and reviewing code, I do kindly
> ask you spend a few minutes reading this document. Thank you. ❤️
## AI Assistance Notice
> [!IMPORTANT]
>
> If you are using **any kind of AI assistance** to contribute to Ghostty,
> it must be disclosed in the pull request.
If you are using any kind of AI assistance while contributing to Ghostty,
**this must be disclosed in the pull request**, along with the extent to
which AI assistance was used (e.g. docs only vs. code generation).
If PR responses are being generated by an AI, disclose that as well.
As a small exception, trivial tab-completion doesn't need to be disclosed,
so long as it is limited to single keywords or short phrases.
An example disclosure:
> This PR was written primarily by Claude Code.
Or a more detailed disclosure:
> I consulted ChatGPT to understand the codebase but the solution
> was fully authored manually by myself.
Failure to disclose this is first and foremost rude to the human operators
on the other end of the pull request, but it also makes it difficult to
determine how much scrutiny to apply to the contribution.
In a perfect world, AI assistance would produce equal or higher quality
work than any human. That isn't the world we live in today, and in most cases
it's generating slop. I say this despite being a fan of and using them
successfully myself (with heavy supervision)!
Please be respectful to maintainers and disclose AI assistance.
## Quick Guide
**I'd like to contribute!**
@ -99,6 +133,28 @@ pull request will be accepted with a high degree of certainty.
See the [Contributor's Guide](po/README_CONTRIBUTORS.md) for more details.
## Checking for Memory Leaks
While Zig does an amazing job of finding and preventing memory leaks,
Ghostty uses many third-party libraries that are written in C. Improper usage
of those libraries or bugs in those libraries can cause memory leaks that
Zig cannot detect by itself.
### On Linux
On Linux the recommended tool to check for memory leaks is Valgrind. The
recommended way to run Valgrind is via `zig build`:
```sh
zig build run-valgrind
```
This builds a Ghostty executable with Valgrind support and runs Valgrind
with the proper flags to ensure we're suppressing known false positives.
You can combine the same build args with `run-valgrind` that you can with
`run`, such as specifying additional configurations after a trailing `--`.
## Input Stack Testing
The input stack is the part of the codebase that starts with a

View File

@ -19,7 +19,15 @@ pub fn build(b: *std.Build) !void {
// All our steps which we'll hook up later. The steps are shown
// up here just so that they are more self-documenting.
const run_step = b.step("run", "Run the app");
const test_step = b.step("test", "Run all tests");
const run_valgrind_step = b.step(
"run-valgrind",
"Run the app under valgrind",
);
const test_step = b.step("test", "Run tests");
const test_valgrind_step = b.step(
"test-valgrind",
"Run tests under valgrind",
);
const translations_step = b.step(
"update-translations",
"Update translation files",
@ -77,9 +85,11 @@ pub fn build(b: *std.Build) !void {
// Runtime "none" is libghostty, anything else is an executable.
if (config.app_runtime != .none) {
exe.install();
resources.install();
if (i18n) |v| v.install();
if (config.emit_exe) {
exe.install();
resources.install();
if (i18n) |v| v.install();
}
} else {
// Libghostty
//
@ -181,6 +191,31 @@ pub fn build(b: *std.Build) !void {
}
}
// Valgrind
if (config.app_runtime != .none) {
// We need to rebuild Ghostty with a baseline CPU target.
const valgrind_exe = exe: {
var valgrind_config = config;
valgrind_config.target = valgrind_config.baselineTarget();
break :exe try buildpkg.GhosttyExe.init(
b,
&valgrind_config,
&deps,
);
};
const run_cmd = b.addSystemCommand(&.{
"valgrind",
"--leak-check=full",
"--num-callers=50",
b.fmt("--suppressions={s}", .{b.pathFromRoot("valgrind.supp")}),
"--gen-suppressions=all",
});
run_cmd.addArtifactArg(valgrind_exe.exe);
if (b.args) |args| run_cmd.addArgs(args);
run_valgrind_step.dependOn(&run_cmd.step);
}
// Tests
{
const test_exe = b.addTest(.{
@ -188,7 +223,7 @@ pub fn build(b: *std.Build) !void {
.filters = if (test_filter) |v| &.{v} else &.{},
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = config.target,
.target = config.baselineTarget(),
.optimize = .Debug,
.strip = false,
.omit_frame_pointer = false,
@ -198,8 +233,21 @@ pub fn build(b: *std.Build) !void {
if (config.emit_test_exe) b.installArtifact(test_exe);
_ = try deps.add(test_exe);
// Normal test running
const test_run = b.addRunArtifact(test_exe);
test_step.dependOn(&test_run.step);
// Valgrind test running
const valgrind_run = b.addSystemCommand(&.{
"valgrind",
"--leak-check=full",
"--num-callers=50",
b.fmt("--suppressions={s}", .{b.pathFromRoot("valgrind.supp")}),
"--gen-suppressions=all",
});
valgrind_run.addArtifactArg(test_exe);
test_valgrind_step.dependOn(&valgrind_run.step);
}
// update-translations does what it sounds like and updates the "pot"

View File

@ -115,8 +115,8 @@
// Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/3cbeca99efa10beba24b0efe86331736f09f9ed1.tar.gz",
.hash = "N-V-__8AABemXQQj_VhMpwuOSOiSzywW_yGD6aEL9YGI9uBu",
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz",
.hash = "N-V-__8AAAlgXwSghpDmXBXZM4Rpd80WKOXVWTrcL0ucVmls",
.lazy = true,
},
},

6
build.zig.zon.json generated
View File

@ -49,10 +49,10 @@
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
},
"N-V-__8AABemXQQj_VhMpwuOSOiSzywW_yGD6aEL9YGI9uBu": {
"N-V-__8AAAlgXwSghpDmXBXZM4Rpd80WKOXVWTrcL0ucVmls": {
"name": "iterm2_themes",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/3cbeca99efa10beba24b0efe86331736f09f9ed1.tar.gz",
"hash": "sha256-gl42NOZ59ok+umHCHbdBQhWCgFVpj5PAZDVGhJRpbiA="
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz",
"hash": "sha256-PySWF/9IAK4DZCkd5FRpiaIl6et2Qm6t8IKCTzh/Xa0="
},
"N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": {
"name": "jetbrains_mono",

6
build.zig.zon.nix generated
View File

@ -162,11 +162,11 @@ in
};
}
{
name = "N-V-__8AABemXQQj_VhMpwuOSOiSzywW_yGD6aEL9YGI9uBu";
name = "N-V-__8AAAlgXwSghpDmXBXZM4Rpd80WKOXVWTrcL0ucVmls";
path = fetchZigArtifact {
name = "iterm2_themes";
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/3cbeca99efa10beba24b0efe86331736f09f9ed1.tar.gz";
hash = "sha256-gl42NOZ59ok+umHCHbdBQhWCgFVpj5PAZDVGhJRpbiA=";
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz";
hash = "sha256-PySWF/9IAK4DZCkd5FRpiaIl6et2Qm6t8IKCTzh/Xa0=";
};
}
{

2
build.zig.zon.txt generated
View File

@ -29,7 +29,7 @@ https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90
https://github.com/jacobsandlund/uucode.x/archive/ca9a9a4560307a30319d206b1ac68a7fc2f2fce9.tar.gz
https://github.com/jacobsandlund/uucode/archive/658743f845f25f8f8d30f535329829660c808eaf.tar.gz
https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-08-09-37-1/ghostty-gobject-0.14.1-2025-08-09-37-1.tar.zst
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/3cbeca99efa10beba24b0efe86331736f09f9ed1.tar.gz
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz
https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz
https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz
https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz

View File

@ -2,8 +2,6 @@ app-id: com.mitchellh.ghostty-debug
runtime: org.gnome.Platform
runtime-version: "48"
sdk: org.gnome.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.ziglang
default-branch: tip
command: ghostty
rename-icon: com.mitchellh.ghostty
@ -37,7 +35,7 @@ modules:
- name: ghostty
buildsystem: simple
build-options:
append-path: /usr/lib/sdk/ziglang
append-path: /app/zig
build-commands:
- zig build
-Doptimize=Debug

View File

@ -2,8 +2,6 @@ app-id: com.mitchellh.ghostty
runtime: org.gnome.Platform
runtime-version: "48"
sdk: org.gnome.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.ziglang
default-branch: tip
command: ghostty
finish-args:
@ -36,7 +34,7 @@ modules:
- name: ghostty
buildsystem: simple
build-options:
append-path: /usr/lib/sdk/ziglang
append-path: /app/zig
build-commands:
- zig build
-Doptimize=ReleaseFast

View File

@ -3,6 +3,24 @@ buildsystem: simple
build-commands:
- true
modules:
- name: zig
buildsystem: simple
cleanup:
- "*"
build-commands:
- mkdir -p /app/zig
- cp -r ./* /app/zig
- chmod a+x /app/zig/zig
sources:
- type: archive
sha256: 24aeeec8af16c381934a6cd7d95c807a8cb2cf7df9fa40d359aa884195c4716c
url: https://ziglang.org/download/0.14.1/zig-x86_64-linux-0.14.1.tar.xz
only-arches: [x86_64]
- type: archive
sha256: f7a654acc967864f7a050ddacfaa778c7504a0eca8d2b678839c21eea47c992b
url: https://ziglang.org/download/0.14.1/zig-aarch64-linux-0.14.1.tar.xz
only-arches: [aarch64]
- name: bzip2-redirect
buildsystem: simple
build-commands:

View File

@ -61,9 +61,9 @@
},
{
"type": "archive",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/3cbeca99efa10beba24b0efe86331736f09f9ed1.tar.gz",
"dest": "vendor/p/N-V-__8AABemXQQj_VhMpwuOSOiSzywW_yGD6aEL9YGI9uBu",
"sha256": "825e3634e679f6893eba61c21db7414215828055698f93c06435468494696e20"
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz",
"dest": "vendor/p/N-V-__8AAAlgXwSghpDmXBXZM4Rpd80WKOXVWTrcL0ucVmls",
"sha256": "3f249617ff4800ae0364291de4546989a225e9eb76426eadf082824f387f5dad"
},
{
"type": "archive",

View File

@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle",
"state" : {
"revision" : "0ef1ee0220239b3776f433314515fd849025673f",
"version" : "2.6.4"
"revision" : "df074165274afaa39539c05d57b0832620775b11",
"version" : "2.7.1"
}
}
],

View File

@ -119,6 +119,9 @@ class AppDelegate: NSObject,
@Published private(set) var appIcon: NSImage? = nil {
didSet {
NSApplication.shared.applicationIconImage = appIcon
let appPath = Bundle.main.bundlePath
NSWorkspace.shared.setIcon(appIcon, forFile: appPath, options: [])
NSWorkspace.shared.noteFileSystemChanged(appPath)
}
}
@ -255,13 +258,13 @@ class AppDelegate: NSObject,
// Setup signal handlers
setupSignals()
// If we launched via zig run then we need to force foreground.
if Ghostty.launchSource == .zig_run {
// This never gets called until we click the dock icon. This forces it
// activate immediately.
applicationDidBecomeActive(.init(name: NSApplication.didBecomeActiveNotification))
// We run in the background, this forces us to the front.
DispatchQueue.main.async {
NSApp.setActivationPolicy(.regular)
@ -399,11 +402,9 @@ class AppDelegate: NSObject,
var config = Ghostty.SurfaceConfiguration()
if (isDirectory.boolValue) {
// When opening a directory, create a new tab in the main
// window with that as the working directory.
// If no windows exist, a new one will be created.
// When opening a directory, check the configuration to decide
// whether to open in a new tab or new window.
config.workingDirectory = filename
_ = TerminalController.newTab(ghostty, withBaseConfig: config)
} else {
// 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
@ -415,8 +416,11 @@ class AppDelegate: NSObject,
// 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)
}
switch ghostty.config.macosDockDropBehavior {
case .new_tab: _ = TerminalController.newTab(ghostty, withBaseConfig: config)
case .new_window: _ = TerminalController.newWindow(ghostty, withBaseConfig: config)
}
return true
@ -834,6 +838,13 @@ class AppDelegate: NSObject,
case .xray:
self.appIcon = NSImage(named: "XrayImage")!
case .custom:
if let userIcon = NSImage(contentsOfFile: config.macosCustomIcon) {
self.appIcon = userIcon
} else {
self.appIcon = nil // Revert back to official icon if invalid location
}
case .customStyle:
guard let ghostColor = config.macosIconGhostColor else { break }
guard let screenColors = config.macosIconScreenColor else { break }
@ -946,18 +957,10 @@ class AppDelegate: NSObject,
@IBAction func newWindow(_ sender: Any?) {
_ = TerminalController.newWindow(ghostty)
// We also activate our app so that it becomes front. This may be
// necessary for the dock menu.
NSApp.activate(ignoringOtherApps: true)
}
@IBAction func newTab(_ sender: Any?) {
_ = TerminalController.newTab(ghostty)
// We also activate our app so that it becomes front. This may be
// necessary for the dock menu.
NSApp.activate(ignoringOtherApps: true)
}
@IBAction func closeAllWindows(_ sender: Any?) {

View File

@ -226,6 +226,10 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
}
c.showWindow(self)
// All new_window actions force our app to be active, so that the new
// window is focused and visible.
NSApp.activate(ignoringOtherApps: true)
}
// Setup our undo
@ -332,6 +336,10 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
controller.showWindow(self)
window.makeKeyAndOrderFront(self)
// We also activate our app so that it becomes front. This may be
// necessary for the dock menu.
NSApp.activate(ignoringOtherApps: true)
}
// It takes an event loop cycle until the macOS tabGroup state becomes

View File

@ -164,7 +164,7 @@ extension Ghostty {
let key = "window-position-x"
return ghostty_config_get(config, &v, key, UInt(key.count)) ? v : nil
}
var windowPositionY: Int16? {
guard let config = self.config else { return nil }
var v: Int16 = 0
@ -282,6 +282,17 @@ extension Ghostty {
return MacOSTitlebarProxyIcon(rawValue: str) ?? defaultValue
}
var macosDockDropBehavior: MacDockDropBehavior {
let defaultValue = MacDockDropBehavior.new_tab
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "macos-dock-drop-behavior"
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 MacDockDropBehavior(rawValue: str) ?? defaultValue
}
var macosWindowShadow: Bool {
guard let config = self.config else { return false }
var v = false;
@ -301,6 +312,24 @@ extension Ghostty {
return MacOSIcon(rawValue: str) ?? defaultValue
}
var macosCustomIcon: String {
#if os(macOS)
let homeDirURL = FileManager.default.homeDirectoryForCurrentUser
let ghosttyConfigIconPath = homeDirURL.appendingPathComponent(
".config/ghostty/Ghostty.icns",
conformingTo: .fileURL).path()
let defaultValue = ghosttyConfigIconPath
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "macos-custom-icon"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard let ptr = v else { return defaultValue }
return String(cString: ptr)
#else
return ""
#endif
}
var macosIconFrame: MacOSIconFrame {
let defaultValue = MacOSIconFrame.aluminum
guard let config = self.config else { return defaultValue }
@ -589,6 +618,11 @@ extension Ghostty.Config {
static let attention = BellFeatures(rawValue: 1 << 2)
static let title = BellFeatures(rawValue: 1 << 3)
}
enum MacDockDropBehavior: String {
case new_tab = "new-tab"
case new_window = "new-window"
}
enum MacHidden : String {
case never

View File

@ -280,6 +280,7 @@ extension Ghostty {
case paper
case retro
case xray
case custom
case customStyle = "custom-style"
}

View File

@ -1327,7 +1327,7 @@ extension Ghostty {
var item: NSMenuItem
// If we have a selection, add copy
if self.selectedRange().length > 0 {
if let text = self.accessibilitySelectedText(), text.count > 0 {
menu.addItem(withTitle: "Copy", action: #selector(copy(_:)), keyEquivalent: "")
}
menu.addItem(withTitle: "Paste", action: #selector(paste(_:)), keyEquivalent: "")

View File

@ -13,11 +13,7 @@ pub fn build(b: *std.Build) !void {
const unit_tests = b.addTest(.{
.name = "test",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
.root_module = module,
});
unit_tests.linkLibC();
@ -34,12 +30,6 @@ pub fn build(b: *std.Build) !void {
.file = wuffs_dep.path("release/c/wuffs-v0.4.c"),
.flags = flags.items,
});
unit_tests.addIncludePath(wuffs_dep.path("release/c"));
unit_tests.addCSourceFile(.{
.file = wuffs_dep.path("release/c/wuffs-v0.4.c"),
.flags = flags.items,
});
}
if (b.lazyDependency("pixels", .{})) |pixels_dep| {

View File

@ -247,6 +247,7 @@ const DerivedConfig = struct {
clipboard_paste_protection: bool,
clipboard_paste_bracketed_safe: bool,
copy_on_select: configpkg.CopyOnSelect,
right_click_action: configpkg.RightClickAction,
confirm_close_surface: configpkg.ConfirmCloseSurface,
cursor_click_to_move: bool,
desktop_notifications: bool,
@ -314,6 +315,7 @@ const DerivedConfig = struct {
.clipboard_paste_protection = config.@"clipboard-paste-protection",
.clipboard_paste_bracketed_safe = config.@"clipboard-paste-bracketed-safe",
.copy_on_select = config.@"copy-on-select",
.right_click_action = config.@"right-click-action",
.confirm_close_surface = config.@"confirm-close-surface",
.cursor_click_to_move = config.@"cursor-click-to-move",
.desktop_notifications = config.@"desktop-notifications",
@ -1529,11 +1531,6 @@ pub const Text = struct {
/// The viewport information about this text, if it is visible in
/// the viewport.
///
/// NOTE(mitchellh): This will only be non-null currently if the entirety
/// of the selection is contained within the viewport. We don't have a
/// use case currently for partial bounds but we should support this
/// eventually.
viewport: ?Viewport = null,
pub const Viewport = struct {
@ -1544,6 +1541,13 @@ pub const Text = struct {
/// The linear offset of the start of the selection and the length.
/// This is "linear" in the sense that it is the offset in the
/// flattened viewport as a single array of text.
///
/// Note: these values are currently wrong if there is a partially
/// visible selection in the viewport (i.e. the top-left or
/// bottom-right of the selection is outside the viewport). But the
/// apprt usecase we have right now doesn't require these to be
/// correct so... let's fix this later. The wrong values will always
/// be within the text bounds so we aren't risking an overflow.
offset_start: u32,
offset_len: u32,
};
@ -1585,17 +1589,57 @@ pub fn dumpTextLocked(
// Calculate our viewport info if we can.
const vp: ?Text.Viewport = viewport: {
// If our tl or br is not in the viewport then we don't
// have a viewport. One day we should extend this to support
// partial selections that are in the viewport.
const tl_pt = self.io.terminal.screen.pages.pointFromPin(
// If our bottom right pin is before the viewport, then we can't
// possibly have this text be within the viewport.
const vp_tl_pin = self.io.terminal.screen.pages.getTopLeft(.viewport);
const br_pin = sel.bottomRight(&self.io.terminal.screen);
if (br_pin.before(vp_tl_pin)) break :viewport null;
// If our top-left pin is after the viewport, then we can't possibly
// have this text be within the viewport.
const vp_br_pin = self.io.terminal.screen.pages.getBottomRight(.viewport) orelse {
// I don't think this is possible but I don't want to crash on
// that assertion so let's just break out...
log.warn("viewport bottom-right pin not found, bug?", .{});
break :viewport null;
};
const tl_pin = sel.topLeft(&self.io.terminal.screen);
if (vp_br_pin.before(tl_pin)) break :viewport null;
// We established that our top-left somewhere before the viewport
// bottom-right and that our bottom-right is somewhere after
// the top-left. This means that at least some portion of our
// selection is within the viewport.
// Our top-left point. If it doesn't exist in the viewport it must
// be before and we can return (0,0).
const tl_pt: terminal.Point = self.io.terminal.screen.pages.pointFromPin(
.viewport,
sel.topLeft(&self.io.terminal.screen),
) orelse break :viewport null;
tl_pin,
) orelse tl: {
if (comptime std.debug.runtime_safety) {
assert(tl_pin.before(vp_tl_pin));
}
break :tl .{ .viewport = .{} };
};
// Our bottom-right point. If it doesn't exist in the viewport
// it must be the bottom-right of the viewport.
const br_pt = self.io.terminal.screen.pages.pointFromPin(
.viewport,
sel.bottomRight(&self.io.terminal.screen),
) orelse break :viewport null;
br_pin,
) orelse br: {
if (comptime std.debug.runtime_safety) {
assert(vp_br_pin.before(br_pin));
}
break :br self.io.terminal.screen.pages.pointFromPin(
.viewport,
vp_br_pin,
).?;
};
const tl_coord = tl_pt.coord();
const br_coord = br_pt.coord();
@ -1666,73 +1710,6 @@ pub fn selectionString(self: *Surface, alloc: Allocator) !?[:0]const u8 {
});
}
/// Return the apprt selection metadata used by apprt's for implementing
/// things like contextual information on right click and so on.
///
/// This only returns non-null if the selection is fully contained within
/// the viewport. The use case for this function at the time of authoring
/// it is for apprt's to implement right-click contextual menus and
/// those only make sense for selections fully contained within the
/// viewport. We don't handle the case where you right click a word-wrapped
/// word at the end of the viewport yet.
pub fn selectionInfo(self: *const Surface) ?apprt.Selection {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
const sel = self.io.terminal.screen.selection orelse return null;
// Get the TL/BR pins for the selection and convert to viewport.
const tl = sel.topLeft(&self.io.terminal.screen);
const br = sel.bottomRight(&self.io.terminal.screen);
const tl_pt = self.io.terminal.screen.pages.pointFromPin(.viewport, tl) orelse return null;
const br_pt = self.io.terminal.screen.pages.pointFromPin(.viewport, br) orelse return null;
const tl_coord = tl_pt.coord();
const br_coord = br_pt.coord();
// Utilize viewport sizing to convert to offsets
const start = tl_coord.y * self.io.terminal.screen.pages.cols + tl_coord.x;
const end = br_coord.y * self.io.terminal.screen.pages.cols + br_coord.x;
// Our sizes are all scaled so we need to send the unscaled values back.
const content_scale = self.rt_surface.getContentScale() catch .{ .x = 1, .y = 1 };
const x: f64 = x: {
// Simple x * cell width gives the left
var x: f64 = @floatFromInt(tl_coord.x * self.size.cell.width);
// Add padding
x += @floatFromInt(self.size.padding.left);
// Scale
x /= content_scale.x;
break :x x;
};
const y: f64 = y: {
// Simple y * cell height gives the top
var y: f64 = @floatFromInt(tl_coord.y * self.size.cell.height);
// We want the text baseline
y += @floatFromInt(self.size.cell.height);
y -= @floatFromInt(self.font_metrics.cell_baseline);
// Add padding
y += @floatFromInt(self.size.padding.top);
// Scale
y /= content_scale.y;
break :y y;
};
return .{
.tl_x_px = x,
.tl_y_px = y,
.offset_start = start,
.offset_len = end - start,
};
}
/// Returns the pwd of the terminal, if any. This is always copied because
/// the pwd can change at any point from termio. If we are calling from the IO
/// thread you should just check the terminal directly.
@ -1833,6 +1810,32 @@ fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard)
};
}
fn copySelectionToClipboards(
self: *Surface,
sel: terminal.Selection,
clipboards: []const apprt.Clipboard,
) void {
const buf = self.io.terminal.screen.selectionString(self.alloc, .{
.sel = sel,
.trim = self.config.clipboard_trim_trailing_spaces,
}) catch |err| {
log.err("error reading selection string err={}", .{err});
return;
};
defer self.alloc.free(buf);
for (clipboards) |clipboard| self.rt_surface.setClipboardString(
buf,
clipboard,
false,
) catch |err| {
log.err(
"error setting clipboard string clipboard={} err={}",
.{ clipboard, err },
);
};
}
/// Set the selection contents.
///
/// This must be called with the renderer mutex held.
@ -1850,33 +1853,12 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) !void {
const sel = sel_ orelse return;
if (prev_) |prev| if (sel.eql(prev)) return;
const buf = self.io.terminal.screen.selectionString(self.alloc, .{
.sel = sel,
.trim = self.config.clipboard_trim_trailing_spaces,
}) catch |err| {
log.err("error reading selection string err={}", .{err});
return;
};
defer self.alloc.free(buf);
// Set the clipboard. This is not super DRY but it is clear what
// we're doing for each setting without being clever.
switch (self.config.copy_on_select) {
.false => unreachable, // handled above with an early exit
// Both standard and selection clipboards are set.
.clipboard => {
const clipboards: []const apprt.Clipboard = &.{ .standard, .selection };
for (clipboards) |clipboard| self.rt_surface.setClipboardString(
buf,
clipboard,
false,
) catch |err| {
log.err(
"error setting clipboard string clipboard={} err={}",
.{ clipboard, err },
);
};
self.copySelectionToClipboards(sel, &.{ .standard, .selection });
},
// The selection clipboard is set if supported, otherwise the standard.
@ -1885,17 +1867,7 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) !void {
.selection
else
.standard;
self.rt_surface.setClipboardString(
buf,
clipboard,
false,
) catch |err| {
log.err(
"error setting clipboard string clipboard={} err={}",
.{ clipboard, err },
);
};
self.copySelectionToClipboards(sel, &.{clipboard});
},
}
}
@ -3582,18 +3554,49 @@ pub fn mouseButtonCallback(
break :pin pin;
};
// If we already have a selection and the selection contains
// where we clicked then we don't want to modify the selection.
if (self.io.terminal.screen.selection) |prev_sel| {
if (prev_sel.contains(screen, pin)) break :sel;
switch (self.config.right_click_action) {
.ignore => {
// Return early to skip clearing the selection.
try self.queueRender();
return true;
},
.copy => {
if (self.io.terminal.screen.selection) |sel| {
self.copySelectionToClipboards(sel, &.{.standard});
}
},
.@"copy-or-paste" => {
if (self.io.terminal.screen.selection) |sel| {
self.copySelectionToClipboards(sel, &.{.standard});
} else {
try self.startClipboardRequest(.standard, .paste);
}
},
.paste => {
try self.startClipboardRequest(.standard, .paste);
},
.@"context-menu" => {
// If we already have a selection and the selection contains
// where we clicked then we don't want to modify the selection.
if (self.io.terminal.screen.selection) |prev_sel| {
if (prev_sel.contains(screen, pin)) break :sel;
// The selection doesn't contain our pin, so we create a new
// word selection where we clicked.
// The selection doesn't contain our pin, so we create a new
// word selection where we clicked.
}
const sel = screen.selectWord(pin) orelse break :sel;
try self.setSelection(sel);
try self.queueRender();
return false;
},
}
const sel = screen.selectWord(pin) orelse break :sel;
try self.setSelection(sel);
try self.setSelection(null);
try self.queueRender();
// Consume the event such that the context menu is not displayed.
return true;
}
return false;

View File

@ -1,6 +1,7 @@
//! This files contains all the GObject classes for the GTK apprt
//! along with helpers to work with them.
const std = @import("std");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
@ -53,6 +54,111 @@ pub fn Common(
}
}).private else {};
/// Get the class for the object.
///
/// This _seems_ ugly and unsafe but this is how GObject
/// works under the hood. From the [GObject Type System
/// Concepts](https://docs.gtk.org/gobject/concepts.html) documentation:
///
/// Every object must define two structures: its class structure
/// and its instance structure. All class structures must contain
/// as first member a GTypeClass structure. All instance structures
/// must contain as first member a GTypeInstance structure.
///
/// These constraints allow the type system to make sure that
/// every object instance (identified by a pointer to the objects
/// instance structure) contains in its first bytes a pointer to the
/// objects class structure.
///
/// The C standard mandates that the first field of a C structure is
/// stored starting in the first byte of the buffer used to hold the
/// structures fields in memory. This means that the first field of
/// an instance of an object B is As first field which in turn is
/// GTypeInstances first field which in turn is g_class, a pointer
/// to Bs class structure.
///
/// This means that to access the class structure for an object you cast it
/// to `*gobject.TypeInstance` and then access the `f_g_class` field.
///
/// https://gitlab.gnome.org/GNOME/glib/-/blob/2c08654b62d52a31c4e4d13d7d85e12b989e72be/gobject/gtype.h#L555-571
/// https://gitlab.gnome.org/GNOME/glib/-/blob/2c08654b62d52a31c4e4d13d7d85e12b989e72be/gobject/gtype.h#L2673
///
pub fn getClass(self: *Self) ?*Self.Class {
const type_instance: *gobject.TypeInstance = @ptrCast(self);
return @ptrCast(type_instance.f_g_class orelse return null);
}
/// Define a virtual method. The `Self.Class` type must have a field
/// named `name` which is a function pointer in the following form:
///
/// ?*const fn (*Self) callconv(.c) void
///
/// The virtual method may take additional parameters and specify
/// a non-void return type. The parameters and return type must be
/// valid for the C calling convention.
pub fn defineVirtualMethod(
comptime name: [:0]const u8,
) type {
return struct {
pub fn call(
class: anytype,
object: *ClassInstance(@TypeOf(class)),
params: anytype,
) (fn_info.return_type orelse void) {
const func = @field(
gobject.ext.as(Self.Class, class),
name,
).?;
@call(.auto, func, .{
gobject.ext.as(Self, object),
} ++ params);
}
pub fn implement(
class: anytype,
implementation: *const ImplementFunc(@TypeOf(class)),
) void {
@field(gobject.ext.as(
Self.Class,
class,
), name) = @ptrCast(implementation);
}
/// The type info of the virtual method.
const fn_info = fn_info: {
// This is broken down like this so its slightly more
// readable. We expect a field named "name" on the Class
// with the rough type of `?*const fn` and we need the
// function info.
const Field = @FieldType(Self.Class, name);
const opt = @typeInfo(Field).optional;
const ptr = @typeInfo(opt.child).pointer;
break :fn_info @typeInfo(ptr.child).@"fn";
};
/// The instance type for a class.
fn ClassInstance(comptime T: type) type {
return @typeInfo(T).pointer.child.Instance;
}
/// The function type for implementations. This is the same type
/// as the virtual method but the self parameter points to the
/// target instead of the original class.
fn ImplementFunc(comptime T: type) type {
var params: [fn_info.params.len]std.builtin.Type.Fn.Param = undefined;
@memcpy(&params, fn_info.params);
params[0].type = *ClassInstance(T);
return @Type(.{ .@"fn" = .{
.calling_convention = fn_info.calling_convention,
.is_generic = fn_info.is_generic,
.is_var_args = fn_info.is_var_args,
.return_type = fn_info.return_type,
.params = &params,
} });
}
};
}
/// A helper that creates a property that reads and writes a
/// private field with only shallow copies. This is good for primitives
/// such as bools, numbers, etc.

View File

@ -2216,7 +2216,7 @@ const Action = struct {
Window,
surface.as(gtk.Widget),
) orelse {
log.warn("surface is not in a window, ignoring new_tab", .{});
log.warn("surface is not in a window, ignoring toggle_window_decorations", .{});
return false;
};

View File

@ -35,36 +35,18 @@ pub const ImguiWidget = extern struct {
pub const properties = struct {};
pub const signals = struct {
/// Emitted when the child widget should render. During the callback,
/// the Imgui context is valid.
pub const render = struct {
pub const name = "render";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
pub const signals = struct {};
/// Emitted when first realized to allow the embedded ImGui application
/// to initialize itself. When this is called, the ImGui context
/// is properly set.
///
/// This might be called multiple times, but each time it is
/// called a new Imgui context will be created.
pub const setup = struct {
pub const name = "setup";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
pub const virtual_methods = struct {
/// This virtual method will be called to allow the Dear ImGui
/// application to do one-time setup of the context. The correct context
/// will be current when the virtual method is called.
pub const setup = C.defineVirtualMethod("setup");
/// This virtual method will be called at each frame to allow the Dear
/// ImGui application to draw the application. The correct context will
/// be current when the virtual method is called.
pub const render = C.defineVirtualMethod("render");
};
const Private = struct {
@ -113,6 +95,25 @@ pub const ImguiWidget = extern struct {
priv.gl_area.queueRender();
}
//---------------------------------------------------------------
// Public wrappers for virtual methods
/// This virtual method will be called to allow the Dear ImGui application
/// to do one-time setup of the context. The correct context will be current
/// when the virtual method is called.
pub fn setup(self: *Self) callconv(.c) void {
const class = self.getClass() orelse return;
virtual_methods.setup.call(class, self, .{});
}
/// This virtual method will be called at each frame to allow the Dear ImGui
/// application to draw the application. The correct context will be current
/// when the virtual method is called.
pub fn render(self: *Self) callconv(.c) void {
const class = self.getClass() orelse return;
virtual_methods.render.call(class, self, .{});
}
//---------------------------------------------------------------
// Private Methods
@ -232,13 +233,8 @@ pub const ImguiWidget = extern struct {
// initialize the ImgUI OpenGL backend for our context.
_ = cimgui.ImGui_ImplOpenGL3_Init(null);
// Setup our app
signals.setup.impl.emit(
self,
null,
.{},
null,
);
// Call the virtual method to setup the UI.
self.setup();
}
/// Handle a request to unrealize the GLArea
@ -279,13 +275,8 @@ pub const ImguiWidget = extern struct {
self.newFrame();
cimgui.c.igNewFrame();
// Use the callback to draw the UI.
signals.render.impl.emit(
self,
null,
.{},
null,
);
// Call the virtual method to draw the UI.
self.render();
// Render
cimgui.c.igRender();
@ -422,15 +413,34 @@ pub const ImguiWidget = extern struct {
cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, bytes);
}
//---------------------------------------------------------------
// Default virtual method handlers
/// Default setup function. Does nothing but log a warning.
fn defaultSetup(_: *Self) callconv(.c) void {
log.warn("default Dear ImGui setup called, this is a bug.", .{});
}
/// Default render function. Does nothing but log a warning.
fn defaultRender(_: *Self) callconv(.c) void {
log.warn("default Dear ImGui render called, this is a bug.", .{});
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
pub const refSink = C.refSink;
pub const unref = C.unref;
pub const getClass = C.getClass;
const private = C.private;
pub const Class = extern struct {
parent_class: Parent.Class,
/// Function pointers for virtual methods.
setup: ?*const fn (*Self) callconv(.c) void,
render: ?*const fn (*Self) callconv(.c) void,
var parent: *Parent.Class = undefined;
pub const Instance = Self;
@ -444,6 +454,10 @@ pub const ImguiWidget = extern struct {
}),
);
// Initialize our virtual methods with default functions.
class.setup = defaultSetup;
class.render = defaultRender;
// Bindings
class.bindTemplateChildPrivate("gl_area", .{});
class.bindTemplateChildPrivate("im_context", .{});
@ -464,8 +478,6 @@ pub const ImguiWidget = extern struct {
class.bindTemplateCallback("im_commit", &imCommit);
// Signals
signals.render.impl.register(.{});
signals.setup.impl.register(.{});
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);

View File

@ -17,7 +17,7 @@ const log = std.log.scoped(.gtk_ghostty_inspector_widget);
pub const InspectorWidget = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = adw.Bin;
pub const Parent = ImguiWidget;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttyInspectorWidget",
.instanceInit = &init,
@ -50,9 +50,6 @@ pub const InspectorWidget = extern struct {
/// We attach a weak notify to the object.
surface: ?*Surface = null,
/// The embedded Dear ImGui widget.
imgui_widget: *ImguiWidget,
pub var offset: c_int = 0;
};
@ -78,13 +75,30 @@ pub const InspectorWidget = extern struct {
);
}
/// Called to do initial setup of the UI.
fn imguiSetup(
_: *Self,
) callconv(.c) void {
Inspector.setup();
}
/// Called for every frame to draw the UI.
fn imguiRender(
self: *Self,
) callconv(.c) void {
const priv = self.private();
const surface = priv.surface orelse return;
const core_surface = surface.core() orelse return;
const inspector = core_surface.inspector orelse return;
inspector.render();
}
//---------------------------------------------------------------
// Public methods
/// Queue a render of the Dear ImGui widget.
pub fn queueRender(self: *Self) void {
const priv = self.private();
priv.imgui_widget.queueRender();
self.as(ImguiWidget).queueRender();
}
//---------------------------------------------------------------
@ -189,24 +203,6 @@ pub const InspectorWidget = extern struct {
// for completeness sake we should clean this up.
}
fn imguiRender(
_: *ImguiWidget,
self: *Self,
) callconv(.c) void {
const priv = self.private();
const surface = priv.surface orelse return;
const core_surface = surface.core() orelse return;
const inspector = core_surface.inspector orelse return;
inspector.render();
}
fn imguiSetup(
_: *ImguiWidget,
_: *Self,
) callconv(.c) void {
Inspector.setup();
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
@ -230,13 +226,6 @@ pub const InspectorWidget = extern struct {
}),
);
// Bindings
class.bindTemplateChildPrivate("imgui_widget", .{});
// Template callbacks
class.bindTemplateCallback("imgui_render", &imguiRender);
class.bindTemplateCallback("imgui_setup", &imguiSetup);
// Properties
gobject.ext.registerProperties(class, &.{
properties.surface.impl,
@ -245,6 +234,8 @@ pub const InspectorWidget = extern struct {
// Signals
// Virtual methods
ImguiWidget.virtual_methods.setup.implement(class, imguiSetup);
ImguiWidget.virtual_methods.render.implement(class, imguiRender);
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
}

View File

@ -554,13 +554,21 @@ pub const Surface = extern struct {
config_: ?*Config,
bell_ringing_: c_int,
) callconv(.c) c_int {
const bell_ringing = bell_ringing_ != 0;
// If the bell isn't ringing exit early because when the surface is
// first created there's a race between this code being run and the
// config being set on the surface. That way we don't overwhelm people
// with the warning that we issue if the config isn't set and overwhelm
// ourselves with large numbers of bug reports.
if (!bell_ringing) return @intFromBool(false);
const config = if (config_) |v| v.get() else {
log.warn("config unavailable for computing whether border should be shown , likely bug", .{});
log.warn("config unavailable for computing whether border should be shown, likely bug", .{});
return @intFromBool(false);
};
const bell_ringing = bell_ringing_ != 0;
return @intFromBool(config.@"bell-features".border and bell_ringing);
return @intFromBool(config.@"bell-features".border);
}
pub fn toggleFullscreen(self: *Self) void {
@ -830,7 +838,7 @@ pub const Surface = extern struct {
// such as single quote on a US international keyboard layout.
if (priv.im_composing) return true;
// If we were composing and now we're not it means that we committed
// If we were composing and now we're not, it means that we committed
// the text. We also don't want to encode a key event for this.
// Example: enable Japanese input method, press "konn" and then
// press enter. The final enter should not be encoded and "konn"
@ -870,9 +878,24 @@ pub const Surface = extern struct {
// We want to get the physical unmapped key to process physical keybinds.
// (These are keybinds explicitly marked as requesting physical mapping).
const physical_key = keycode: for (input.keycodes.entries) |entry| {
if (entry.native == keycode) break :keycode entry.key;
} else .unidentified;
const physical_key = keycode: {
const w3c_key: input.Key = w3c: for (input.keycodes.entries) |entry| {
if (entry.native == keycode) break :w3c entry.key;
} else .unidentified;
// If the key should be remappable, then consult the pre-remapped
// XKB keyval/keysym to get the (possibly) remapped key.
//
// See the docs for `shouldBeRemappable` for why we even have to
// do this in the first place.
if (w3c_key.shouldBeRemappable()) {
if (gtk_key.keyFromKeyval(keyval)) |remapped|
break :keycode remapped;
}
// Return the original physical key
break :keycode w3c_key;
};
// Get our modifier for the event
const mods: input.Mods = gtk_key.eventMods(

View File

@ -301,24 +301,6 @@ pub const Window = extern struct {
// Initialize our actions
self.initActionMap();
// We need to setup resize notifications on our surface
if (self.as(gtk.Native).getSurface()) |gdk_surface| {
_ = gobject.Object.signals.notify.connect(
gdk_surface,
*Self,
propGdkSurfaceWidth,
self,
.{ .detail = "width" },
);
_ = gobject.Object.signals.notify.connect(
gdk_surface,
*Self,
propGdkSurfaceHeight,
self,
.{ .detail = "height" },
);
}
// Start states based on config.
if (priv.config) |config_obj| {
const config = config_obj.get();
@ -810,9 +792,18 @@ pub const Window = extern struct {
/// Toggle the window decorations for this window.
pub fn toggleWindowDecorations(self: *Self) void {
self.setWindowDecoration(switch (self.getWindowDecoration()) {
// Null will force using the central config
.none => null,
const priv = self.private();
if (priv.window_decoration) |_| {
// Unset any previously set window decoration settings
self.setWindowDecoration(null);
return;
}
const config = if (priv.config) |v| v.get() else return;
self.setWindowDecoration(switch (config.@"window-decoration") {
// Use auto when the decoration is initially none
.none => .auto,
// Anything non-none to none
.auto, .client, .server => .none,
@ -1154,6 +1145,25 @@ pub const Window = extern struct {
return;
}
// We need to setup resize notifications on our surface,
// which is only available after the window had been realized.
if (self.as(gtk.Native).getSurface()) |gdk_surface| {
_ = gobject.Object.signals.notify.connect(
gdk_surface,
*Self,
propGdkSurfaceWidth,
self,
.{ .detail = "width" },
);
_ = gobject.Object.signals.notify.connect(
gdk_surface,
*Self,
propGdkSurfaceHeight,
self,
.{ .detail = "height" },
);
}
// When we are realized we always setup our appearance since this
// calls some winproto functions.
self.syncAppearance();

View File

@ -103,7 +103,10 @@ pub fn addAsGroup(comptime T: type, self: *T, comptime name: [:0]const u8, actio
test "adding actions to an object" {
// This test requires a connection to an active display environment.
if (gtk.initCheck() == 0) return;
if (gtk.initCheck() == 0) return error.SkipZigTest;
_ = glib.MainContext.acquire(null);
defer glib.MainContext.release(null);
const callbacks = struct {
fn callback(_: *gio.SimpleAction, variant_: ?*glib.Variant, self: *gtk.Box) callconv(.c) void {
@ -155,4 +158,6 @@ test "adding actions to an object" {
const actual = value.getInt();
try testing.expectEqual(expected, actual);
while (glib.MainContext.iteration(null, 0) != 0) {}
}

View File

@ -0,0 +1,189 @@
//! DBus helper for IPC
const Self = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const gio = @import("gio");
const glib = @import("glib");
const apprt = @import("../../../apprt.zig");
const ApprtApp = @import("../App.zig");
/// The target for this IPC.
target: apprt.ipc.Target,
/// Connection to the DBus session bus.
dbus: *gio.DBusConnection,
/// The bus name of the Ghostty instance that we are calling.
bus_name: [:0]const u8,
/// The object path of the Ghostty instance that we are calling.
object_path: [:0]const u8,
/// Used to build the DBus payload.
payload_builder: *glib.VariantBuilder,
/// Used to build the parameters for the IPC.
parameters_builder: *glib.VariantBuilder,
/// Initialize the helper.
pub fn init(alloc: Allocator, target: apprt.ipc.Target, action: [:0]const u8) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!Self {
// Get the appropriate bus name and object path for contacting the
// Ghostty instance we're interested in.
const bus_name: [:0]const u8, const object_path: [:0]const u8 = switch (target) {
.class => |class| result: {
// Force the usage of the class specified on the CLI to determine the
// bus name and object path.
const object_path = try std.fmt.allocPrintZ(alloc, "/{s}", .{class});
std.mem.replaceScalar(u8, object_path, '.', '/');
std.mem.replaceScalar(u8, object_path, '-', '_');
break :result .{ class, object_path };
},
.detect => .{ ApprtApp.application_id, ApprtApp.object_path },
};
errdefer {
switch (target) {
.class => alloc.free(object_path),
.detect => {},
}
}
if (gio.Application.idIsValid(bus_name.ptr) == 0) {
const stderr = std.io.getStdErr().writer();
try stderr.print("D-Bus bus name is not valid: {s}\n", .{bus_name});
return error.IPCFailed;
}
if (glib.Variant.isObjectPath(object_path.ptr) == 0) {
const stderr = std.io.getStdErr().writer();
try stderr.print("D-Bus object path is not valid: {s}\n", .{object_path});
return error.IPCFailed;
}
// Get a connection to the DBus session bus.
const dbus = dbus: {
var err_: ?*glib.Error = null;
defer if (err_) |err| err.free();
const dbus_ = gio.busGetSync(.session, null, &err_);
if (err_) |err| {
const stderr = std.io.getStdErr().writer();
try stderr.print(
"Unable to establish connection to D-Bus session bus: {s}\n",
.{err.f_message orelse "(unknown)"},
);
return error.IPCFailed;
}
break :dbus dbus_ orelse {
const stderr = std.io.getStdErr().writer();
try stderr.print("gio.busGetSync returned null\n", .{});
return error.IPCFailed;
};
};
// Set up the payload builder.
const payload_variant_type = glib.VariantType.new("(sava{sv})");
defer glib.free(payload_variant_type);
const payload_builder = glib.VariantBuilder.new(payload_variant_type);
// Add the action name to the payload.
{
const s_variant_type = glib.VariantType.new("s");
defer s_variant_type.free();
const bytes = glib.Bytes.new(action.ptr, action.len + 1);
defer bytes.unref();
const value = glib.Variant.newFromBytes(s_variant_type, bytes, @intFromBool(true));
payload_builder.addValue(value);
}
// Set up the parameter builder.
const parameters_variant_type = glib.VariantType.new("av");
defer parameters_variant_type.free();
const parameters_builder = glib.VariantBuilder.new(parameters_variant_type);
return .{
.target = target,
.dbus = dbus,
.bus_name = bus_name,
.object_path = object_path,
.payload_builder = payload_builder,
.parameters_builder = parameters_builder,
};
}
/// Add a parameter to the IPC call.
pub fn addParameter(self: *Self, variant: *glib.Variant) void {
self.parameters_builder.add("v", variant);
}
/// Send the IPC to the remote Ghostty. Once it completes, nothing further
/// should be done with this object other than call `deinit`.
pub fn send(self: *Self) (std.posix.WriteError || apprt.ipc.Errors)!void {
// finish building the parameters
const parameters = self.parameters_builder.end();
// Add the parameters to the payload.
self.payload_builder.addValue(parameters);
// Add the platform data to the payload.
{
const platform_data_variant_type = glib.VariantType.new("a{sv}");
defer platform_data_variant_type.free();
self.payload_builder.open(platform_data_variant_type);
defer self.payload_builder.close();
// We have no platform data.
}
const payload = self.payload_builder.end();
{
var err_: ?*glib.Error = null;
defer if (err_) |err| err.free();
const result_ = self.dbus.callSync(
self.bus_name,
self.object_path,
"org.gtk.Actions",
"Activate",
payload,
null, // We don't care about the return type, we don't do anything with it.
.{}, // no flags
-1, // default timeout
null, // not cancellable
&err_,
);
defer if (result_) |result| result.unref();
if (err_) |err| {
const stderr = std.io.getStdErr().writer();
try stderr.print(
"D-Bus method call returned an error err={s}\n",
.{err.f_message orelse "(unknown)"},
);
return error.IPCFailed;
}
}
}
/// Free/unref any data held by this instance.
pub fn deinit(self: *Self, alloc: Allocator) void {
switch (self.target) {
.class => alloc.free(self.object_path),
.detect => {},
}
self.parameters_builder.unref();
self.payload_builder.unref();
self.dbus.unref();
}

View File

@ -1,11 +1,10 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const gio = @import("gio");
const glib = @import("glib");
const apprt = @import("../../../apprt.zig");
const ApprtApp = @import("../App.zig");
const DBus = @import("DBus.zig");
// Use a D-Bus method call to open a new window on GTK.
// See: https://wiki.gnome.org/Projects/GLib/GApplication/DBusAPI
@ -22,149 +21,42 @@ const ApprtApp = @import("../App.zig");
// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["echo" "hello"]>]' []
// ```
pub fn newWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Action.NewWindow) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool {
const stderr = std.io.getStdErr().writer();
var dbus = try DBus.init(
alloc,
target,
if (value.arguments == null)
"new-window"
else
"new-window-command",
);
defer dbus.deinit(alloc);
// Get the appropriate bus name and object path for contacting the
// Ghostty instance we're interested in.
const bus_name: [:0]const u8, const object_path: [:0]const u8 = switch (target) {
.class => |class| result: {
// Force the usage of the class specified on the CLI to determine the
// bus name and object path.
const object_path = try std.fmt.allocPrintZ(alloc, "/{s}", .{class});
if (value.arguments) |arguments| {
// If `-e` was specified on the command line, the first
// parameter is an array of strings that contain the arguments
// that came after `-e`, which will be interpreted as a command
// to run.
const as_variant_type = glib.VariantType.new("as");
defer as_variant_type.free();
std.mem.replaceScalar(u8, object_path, '.', '/');
std.mem.replaceScalar(u8, object_path, '-', '_');
const s_variant_type = glib.VariantType.new("s");
defer s_variant_type.free();
break :result .{ class, object_path };
},
.detect => .{ ApprtApp.application_id, ApprtApp.object_path },
};
defer {
switch (target) {
.class => alloc.free(object_path),
.detect => {},
var command: glib.VariantBuilder = undefined;
command.init(as_variant_type);
errdefer command.clear();
for (arguments) |argument| {
const bytes = glib.Bytes.new(argument.ptr, argument.len + 1);
defer bytes.unref();
const string = glib.Variant.newFromBytes(s_variant_type, bytes, @intFromBool(true));
command.addValue(string);
}
dbus.addParameter(command.end());
}
if (gio.Application.idIsValid(bus_name.ptr) == 0) {
try stderr.print("D-Bus bus name is not valid: {s}\n", .{bus_name});
return error.IPCFailed;
}
if (glib.Variant.isObjectPath(object_path.ptr) == 0) {
try stderr.print("D-Bus object path is not valid: {s}\n", .{object_path});
return error.IPCFailed;
}
const dbus = dbus: {
var err_: ?*glib.Error = null;
defer if (err_) |err| err.free();
const dbus_ = gio.busGetSync(.session, null, &err_);
if (err_) |err| {
try stderr.print(
"Unable to establish connection to D-Bus session bus: {s}\n",
.{err.f_message orelse "(unknown)"},
);
return error.IPCFailed;
}
break :dbus dbus_ orelse {
try stderr.print("gio.busGetSync returned null\n", .{});
return error.IPCFailed;
};
};
defer dbus.unref();
// use a builder to create the D-Bus method call payload
const payload = payload: {
const payload_variant_type = glib.VariantType.new("(sava{sv})");
defer glib.free(payload_variant_type);
// Initialize our builder to build up our parameters
var builder: glib.VariantBuilder = undefined;
builder.init(payload_variant_type);
errdefer builder.clear();
// action
if (value.arguments == null) {
builder.add("s", "new-window");
} else {
builder.add("s", "new-window-command");
}
// parameters
{
const av_variant_type = glib.VariantType.new("av");
defer av_variant_type.free();
var parameters: glib.VariantBuilder = undefined;
parameters.init(av_variant_type);
errdefer parameters.clear();
if (value.arguments) |arguments| {
// If `-e` was specified on the command line, the first
// parameter is an array of strings that contain the arguments
// that came after `-e`, which will be interpreted as a command
// to run.
{
const as = glib.VariantType.new("as");
defer as.free();
var command: glib.VariantBuilder = undefined;
command.init(as);
errdefer command.clear();
for (arguments) |argument| {
command.add("s", argument.ptr);
}
parameters.add("v", command.end());
}
}
builder.addValue(parameters.end());
}
{
const platform_data_variant_type = glib.VariantType.new("a{sv}");
defer platform_data_variant_type.free();
builder.open(platform_data_variant_type);
defer builder.close();
// we have no platform data
}
break :payload builder.end();
};
{
var err_: ?*glib.Error = null;
defer if (err_) |err| err.free();
const result_ = dbus.callSync(
bus_name,
object_path,
"org.gtk.Actions",
"Activate",
payload,
null, // We don't care about the return type, we don't do anything with it.
.{}, // no flags
-1, // default timeout
null, // not cancellable
&err_,
);
defer if (result_) |result| result.unref();
if (err_) |err| {
try stderr.print(
"D-Bus method call returned an error err={s}\n",
.{err.f_message orelse "(unknown)"},
);
return error.IPCFailed;
}
}
try dbus.send();
return true;
}

View File

@ -1,18 +1,10 @@
using Gtk 4.0;
using Adw 1;
template $GhosttyInspectorWidget: Adw.Bin {
template $GhosttyInspectorWidget: $GhosttyImguiWidget {
styles [
"inspector",
]
hexpand: true;
vexpand: true;
Adw.Bin {
$GhosttyImguiWidget imgui_widget {
render => $imgui_render();
setup => $imgui_setup();
}
}
}

View File

@ -77,7 +77,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void {
const f = self.data_f orelse return;
var r = std.io.bufferedReader(f.reader());
var p: terminalpkg.Parser = .{};
var p: terminalpkg.Parser = .init();
var buf: [4096]u8 = undefined;
while (true) {

View File

@ -62,7 +62,7 @@ pub fn create(
.cols = opts.@"terminal-cols",
}),
.handler = .{ .t = &ptr.terminal },
.stream = .{ .handler = &ptr.handler },
.stream = .init(&ptr.handler),
};
return ptr;

View File

@ -53,6 +53,7 @@ patch_rpath: ?[]const u8 = null,
flatpak: bool = false,
emit_bench: bool = false,
emit_docs: bool = false,
emit_exe: bool = false,
emit_helpgen: bool = false,
emit_macos_app: bool = false,
emit_terminfo: bool = false,
@ -286,6 +287,12 @@ pub fn init(b: *std.Build) !Config {
//---------------------------------------------------------------
// Artifacts to Emit
config.emit_exe = b.option(
bool,
"emit-exe",
"Build and install main executables with 'build'",
) orelse true;
config.emit_test_exe = b.option(
bool,
"emit-test-exe",
@ -460,6 +467,22 @@ pub fn addOptions(self: *const Config, step: *std.Build.Step.Options) !void {
);
}
/// Returns a baseline CPU target retaining all the other CPU configs.
pub fn baselineTarget(self: *const Config) std.Build.ResolvedTarget {
// Set our cpu model as baseline. There may need to be other modifications
// we need to make such as resetting CPU features but for now this works.
var q = self.target.query;
q.cpu_model = .baseline;
// Same logic as build.resolveTargetQuery but we don't need to
// handle the native case.
return .{
.query = q,
.result = std.zig.system.resolveTargetQuery(q) catch
@panic("unable to resolve baseline query"),
};
}
/// Rehydrate our Config from the comptime options. Note that not all
/// options are available at comptime, so look closely at this implementation
/// to see what is and isn't available.

View File

@ -17,6 +17,8 @@ const zf = @import("zf");
// scroll position for larger lists.
const SMALL_LIST_THRESHOLD = 10;
const ColorScheme = enum { all, dark, light };
pub const Options = struct {
/// If true, print the full path to the theme.
path: bool = false,
@ -25,7 +27,7 @@ pub const Options = struct {
plain: bool = false,
/// Specifies the color scheme of the themes to include in the list.
color: enum { all, dark, light } = .all,
color: ColorScheme = .all,
pub fn deinit(self: Options) void {
_ = self;
@ -146,28 +148,11 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
count += 1;
const path = try std.fs.path.join(alloc, &.{ loc.dir, entry.name });
// if there is no need to filter just append the theme to the list
if (opts.color == .all) {
try themes.append(.{
.path = path,
.location = loc.location,
.theme = try alloc.dupe(u8, entry.name),
});
continue;
}
// otherwise check if the theme should be included based on the provided options
var config = try Config.default(alloc);
defer config.deinit();
try config.loadFile(config._arena.?.allocator(), path);
if (shouldIncludeTheme(opts, config)) {
try themes.append(.{
.path = path,
.location = loc.location,
.theme = try alloc.dupe(u8, entry.name),
});
}
try themes.append(.{
.path = path,
.location = loc.location,
.theme = try alloc.dupe(u8, entry.name),
});
},
else => {},
}
@ -182,7 +167,7 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
std.mem.sortUnstable(ThemeListElement, themes.items, {}, ThemeListElement.lessThan);
if (tui.can_pretty_print and !opts.plain and std.posix.isatty(std.io.getStdOut().handle)) {
try preview(gpa_alloc, themes.items);
try preview(gpa_alloc, themes.items, opts.color);
return 0;
}
@ -222,8 +207,9 @@ const Preview = struct {
},
color_scheme: vaxis.Color.Scheme,
text_input: vaxis.widgets.TextInput,
theme_filter: ColorScheme,
pub fn init(allocator: std.mem.Allocator, themes: []ThemeListElement) !*Preview {
pub fn init(allocator: std.mem.Allocator, themes: []ThemeListElement, theme_filter: ColorScheme) !*Preview {
const self = try allocator.create(Preview);
self.* = .{
@ -240,11 +226,10 @@ const Preview = struct {
.mode = .normal,
.color_scheme = .light,
.text_input = vaxis.widgets.TextInput.init(allocator, &self.vx.unicode),
.theme_filter = theme_filter,
};
for (0..themes.len) |i| {
try self.filtered.append(i);
}
try self.updateFiltered();
return self;
}
@ -308,6 +293,8 @@ const Preview = struct {
self.filtered.clearRetainingCapacity();
var theme_config = try Config.default(self.allocator);
defer theme_config.deinit();
if (self.text_input.buf.realLength() > 0) {
const first_half = self.text_input.buf.firstHalf();
const second_half = self.text_input.buf.secondHalf();
@ -328,6 +315,9 @@ const Preview = struct {
while (it.next()) |token| try tokens.append(token);
for (self.themes, 0..) |*theme, i| {
try theme_config.loadFile(theme_config._arena.?.allocator(), theme.path);
if (!shouldIncludeTheme(self.theme_filter, theme_config)) continue;
theme.rank = zf.rank(theme.theme, tokens.items, .{
.to_lower = true,
.plain = true,
@ -336,8 +326,11 @@ const Preview = struct {
}
} else {
for (self.themes, 0..) |*theme, i| {
try self.filtered.append(i);
theme.rank = null;
try theme_config.loadFile(theme_config._arena.?.allocator(), theme.path);
if (shouldIncludeTheme(self.theme_filter, theme_config)) {
try self.filtered.append(i);
theme.rank = null;
}
}
}
@ -438,6 +431,14 @@ const Preview = struct {
self.themes[self.filtered.items[self.current]].path,
alloc,
);
if (key.matches('f', .{})) {
switch (self.theme_filter) {
.all => self.theme_filter = .dark,
.dark => self.theme_filter = .light,
.light => self.theme_filter = .all,
}
try self.updateFiltered();
}
},
.help => {
if (key.matches('q', .{}))
@ -695,6 +696,7 @@ const Preview = struct {
const key_help = [_]struct { keys: []const u8, help: []const u8 }{
.{ .keys = "^C, q, ESC", .help = "Quit." },
.{ .keys = "F1, ?, ^H", .help = "Toggle help window." },
.{ .keys = "f", .help = "Cycle through theme filters." },
.{ .keys = "k, ↑", .help = "Move up 1 theme." },
.{ .keys = "ScrollUp", .help = "Move up 1 theme." },
.{ .keys = "PgUp", .help = "Move up 20 themes." },
@ -1615,18 +1617,17 @@ fn color(config: Config, palette: usize) vaxis.Color {
const lorem_ipsum = @embedFile("lorem_ipsum.txt");
fn preview(allocator: std.mem.Allocator, themes: []ThemeListElement) !void {
var app = try Preview.init(allocator, themes);
fn preview(allocator: std.mem.Allocator, themes: []ThemeListElement, theme_filter: ColorScheme) !void {
var app = try Preview.init(allocator, themes, theme_filter);
defer app.deinit();
try app.run();
}
fn shouldIncludeTheme(opts: Options, theme_config: Config) bool {
fn shouldIncludeTheme(theme_filter: ColorScheme, theme_config: Config) bool {
const rf = @as(f32, @floatFromInt(theme_config.background.r)) / 255.0;
const gf = @as(f32, @floatFromInt(theme_config.background.g)) / 255.0;
const bf = @as(f32, @floatFromInt(theme_config.background.b)) / 255.0;
const luminance = 0.2126 * rf + 0.7152 * gf + 0.0722 * bf;
const is_dark = luminance < 0.5;
return (opts.color == .dark and is_dark) or (opts.color == .light and !is_dark);
return (theme_filter == .all) or (theme_filter == .dark and is_dark) or (theme_filter == .light and !is_dark);
}

View File

@ -19,6 +19,7 @@ pub const ClipboardAccess = Config.ClipboardAccess;
pub const Command = Config.Command;
pub const ConfirmCloseSurface = Config.ConfirmCloseSurface;
pub const CopyOnSelect = Config.CopyOnSelect;
pub const RightClickAction = Config.RightClickAction;
pub const CustomShaderAnimation = Config.CustomShaderAnimation;
pub const FontSyntheticStyle = Config.FontSyntheticStyle;
pub const FontShapingBreak = Config.FontShapingBreak;

View File

@ -592,24 +592,24 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF },
///
/// * `contain`
///
/// Preserving the aspect ratio, scale the background image to the largest
/// size that can still be contained within the terminal, so that the whole
/// image is visible.
/// Preserving the aspect ratio, scale the background image to the largest
/// size that can still be contained within the terminal, so that the whole
/// image is visible.
///
/// * `cover`
///
/// Preserving the aspect ratio, scale the background image to the smallest
/// size that can completely cover the terminal. This may result in one or
/// more edges of the image being clipped by the edge of the terminal.
/// Preserving the aspect ratio, scale the background image to the smallest
/// size that can completely cover the terminal. This may result in one or
/// more edges of the image being clipped by the edge of the terminal.
///
/// * `stretch`
///
/// Stretch the background image to the full size of the terminal, without
/// preserving the aspect ratio.
/// Stretch the background image to the full size of the terminal, without
/// preserving the aspect ratio.
///
/// * `none`
///
/// Don't scale the background image.
/// Don't scale the background image.
///
/// The default value is `contain`.
///
@ -1330,53 +1330,59 @@ class: ?[:0]const u8 = null,
/// The keybind trigger can be prefixed with some special values to change
/// the behavior of the keybind. These are:
///
/// * `all:` - Make the keybind apply to all terminal surfaces. By default,
/// keybinds only apply to the focused terminal surface. If this is true,
/// then the keybind will be sent to all terminal surfaces. This only
/// applies to actions that are surface-specific. For actions that
/// are already global (e.g. `quit`), this prefix has no effect.
/// * `all:`
///
/// Available since: 1.0.0
/// Make the keybind apply to all terminal surfaces. By default,
/// keybinds only apply to the focused terminal surface. If this is true,
/// then the keybind will be sent to all terminal surfaces. This only
/// applies to actions that are surface-specific. For actions that
/// are already global (e.g. `quit`), this prefix has no effect.
///
/// * `global:` - Make the keybind global. By default, keybinds only work
/// within Ghostty and under the right conditions (application focused,
/// sometimes terminal focused, etc.). If you want a keybind to work
/// globally across your system (e.g. even when Ghostty is not focused),
/// specify this prefix. This prefix implies `all:`. Note: this does not
/// work in all environments; see the additional notes below for more
/// information.
/// Available since: 1.0.0
///
/// Available since: 1.0.0 (on macOS)
/// Available since: 1.2.0 (on GTK)
/// * `global:`
///
/// * `unconsumed:` - Do not consume the input. By default, a keybind
/// will consume the input, meaning that the associated encoding (if
/// any) will not be sent to the running program in the terminal. If
/// you wish to send the encoded value to the program, specify the
/// `unconsumed:` prefix before the entire keybind. For example:
/// `unconsumed:ctrl+a=reload_config`. `global:` and `all:`-prefixed
/// keybinds will always consume the input regardless of this setting.
/// Since they are not associated with a specific terminal surface,
/// they're never encoded.
/// Make the keybind global. By default, keybinds only work within Ghostty
/// and under the right conditions (application focused, sometimes terminal
/// focused, etc.). If you want a keybind to work globally across your system
/// (e.g. even when Ghostty is not focused), specify this prefix.
/// This prefix implies `all:`.
///
/// Available since: 1.0.0
/// Note: this does not work in all environments; see the additional notes
/// below for more information.
///
/// * `performable:` - Only consume the input if the action is able to be
/// performed. For example, the `copy_to_clipboard` action will only
/// consume the input if there is a selection to copy. If there is no
/// selection, Ghostty behaves as if the keybind was not set. This has
/// no effect with `global:` or `all:`-prefixed keybinds. For key
/// sequences, this will reset the sequence if the action is not
/// performable (acting identically to not having a keybind set at
/// all).
/// Available since: 1.0.0 on macOS, 1.2.0 on GTK
///
/// Performable keybinds will not appear as menu shortcuts in the
/// application menu. This is because the menu shortcuts force the
/// action to be performed regardless of the state of the terminal.
/// Performable keybinds will still work, they just won't appear as
/// a shortcut label in the menu.
/// * `unconsumed:`
///
/// Available since: 1.1.0
/// Do not consume the input. By default, a keybind will consume the input,
/// meaning that the associated encoding (if any) will not be sent to the
/// running program in the terminal. If you wish to send the encoded value
/// to the program, specify the `unconsumed:` prefix before the entire
/// keybind. For example: `unconsumed:ctrl+a=reload_config`. `global:` and
/// `all:`-prefixed keybinds will always consume the input regardless of
/// this setting. Since they are not associated with a specific terminal
/// surface, they're never encoded.
///
/// Available since: 1.0.0
///
/// * `performable:`
///
/// Only consume the input if the action is able to be performed.
/// For example, the `copy_to_clipboard` action will only consume the input
/// if there is a selection to copy. If there is no selection, Ghostty
/// behaves as if the keybind was not set. This has no effect with `global:`
/// or `all:`-prefixed keybinds. For key sequences, this will reset the
/// sequence if the action is not performable (acting identically to not
/// having a keybind set at all).
///
/// Performable keybinds will not appear as menu shortcuts in the
/// application menu. This is because the menu shortcuts force the
/// action to be performed regardless of the state of the terminal.
/// Performable keybinds will still work, they just won't appear as
/// a shortcut label in the menu.
///
/// Available since: 1.1.0
///
/// Keybind triggers are not unique per prefix combination. For example,
/// `ctrl+a` and `global:ctrl+a` are not two separate keybinds. The keybind
@ -1522,28 +1528,36 @@ keybind: Keybinds = .{},
///
/// Valid values:
///
/// * `none` - All window decorations will be disabled. Titlebar,
/// borders, etc. will not be shown. On macOS, this will also disable
/// tabs (enforced by the system).
/// * `none`
///
/// * `auto` - Automatically decide to use either client-side or server-side
/// decorations based on the detected preferences of the current OS and
/// desktop environment. This option usually makes Ghostty look the most
/// "native" for your desktop.
/// All window decorations will be disabled. Titlebar, borders, etc. will
/// not be shown. On macOS, this will also disable tabs (enforced by the
/// system).
///
/// * `client` - Prefer client-side decorations.
/// * `auto`
///
/// Available since: 1.1.0
/// Automatically decide to use either client-side or server-side
/// decorations based on the detected preferences of the current OS and
/// desktop environment. This option usually makes Ghostty look the most
/// "native" for your desktop.
///
/// * `server` - Prefer server-side decorations. This is only relevant
/// on Linux with GTK, either on X11, or Wayland on a compositor that
/// supports the `org_kde_kwin_server_decoration` protocol (e.g. KDE Plasma,
/// but almost any non-GNOME desktop supports this protocol).
/// * `client`
///
/// If `server` is set but the environment doesn't support server-side
/// decorations, client-side decorations will be used instead.
/// Prefer client-side decorations.
///
/// Available since: 1.1.0
/// Available since: 1.1.0
///
/// * `server`
///
/// Prefer server-side decorations. This is only relevant on Linux with GTK,
/// either on X11, or Wayland on a compositor that supports the
/// `org_kde_kwin_server_decoration` protocol (e.g. KDE Plasma, but almost
/// any non-GNOME desktop supports this protocol).
///
/// If `server` is set but the environment doesn't support server-side
/// decorations, client-side decorations will be used instead.
///
/// Available since: 1.1.0
///
/// The default value is `auto`.
///
@ -1886,6 +1900,19 @@ keybind: Keybinds = .{},
else => .false,
},
/// The action to take when the user right-clicks on the terminal surface.
///
/// Valid values:
/// * `context-menu` - Show the context menu.
/// * `paste` - Paste the contents of the clipboard.
/// * `copy` - Copy the selected text to the clipboard.
/// * `copy-or-paste` - If there is a selection, copy the selected text to
/// the clipboard; otherwise, paste the contents of the clipboard.
/// * `ignore` - Do nothing, ignore the right-click.
///
/// The default value is `context-menu`.
@"right-click-action": RightClickAction = .@"context-menu",
/// The time in milliseconds between clicks to consider a click a repeat
/// (double, triple, etc.) or an entirely new single click. A value of zero will
/// use a platform-specific default. The default on macOS is determined by the
@ -2316,9 +2343,9 @@ keybind: Keybinds = .{},
///
/// * `sampler2D iChannel0` - Input texture.
///
/// A texture containing the current terminal screen. If multiple custom
/// shaders are specified, the output of previous shaders is written to
/// this texture, to allow combining multiple effects.
/// A texture containing the current terminal screen. If multiple custom
/// shaders are specified, the output of previous shaders is written to
/// this texture, to allow combining multiple effects.
///
/// * `vec3 iResolution` - Output texture size, `[width, height, 1]` (in px).
///
@ -2604,6 +2631,21 @@ keybind: Keybinds = .{},
/// editor, etc.
@"macos-titlebar-proxy-icon": MacTitlebarProxyIcon = .visible,
/// Controls the windowing behavior when dropping a file or folder
/// onto the Ghostty icon in the macOS dock.
///
/// Valid values are:
///
/// * `new-tab` - Create a new tab in the current window, or open
/// a new window if none exist.
/// * `new-window` - Create a new window unconditionally.
///
/// The default value is `new-tab`.
///
/// This setting is only supported on macOS and has no effect on other
/// platforms.
@"macos-dock-drop-behavior": MacOSDockDropBehavior = .@"new-tab",
/// macOS doesn't have a distinct "alt" key and instead has the "option"
/// key which behaves slightly differently. On macOS by default, the
/// option key plus a character will sometimes produce a Unicode character.
@ -2708,6 +2750,8 @@ keybind: Keybinds = .{},
/// * `blueprint`, `chalkboard`, `microchip`, `glass`, `holographic`,
/// `paper`, `retro`, `xray` - Official variants of the Ghostty icon
/// hand-created by artists (no AI).
/// * `custom` - Use a completely custom icon. The location must be specified
/// using the additional `macos-custom-icon` configuration
/// * `custom-style` - Use the official Ghostty icon but with custom
/// styles applied to various layers. The custom styles must be
/// specified using the additional `macos-icon`-prefixed configurations.
@ -2726,6 +2770,15 @@ keybind: Keybinds = .{},
/// effort.
@"macos-icon": MacAppIcon = .official,
/// The absolute path to the custom icon file.
/// Supported formats include PNG, JPEG, and ICNS.
///
/// Defaults to `~/.config/ghostty/Ghostty.icns`
///
/// Note: This configuration is required when `macos-icon` is set to
/// `custom`
@"macos-custom-icon": ?[]const u8 = null,
/// The material to use for the frame of the macOS app icon.
///
/// Valid values:
@ -6695,6 +6748,25 @@ pub const CopyOnSelect = enum {
clipboard,
};
/// Options for right-click actions.
pub const RightClickAction = enum {
/// No action is taken on right-click.
ignore,
/// Pastes from the system clipboard.
paste,
/// Copies the selected text to the system clipboard.
copy,
/// Copies the selected text to the system clipboard and
/// pastes the clipboard if no text is selected.
@"copy-or-paste",
/// Shows a context menu with options.
@"context-menu",
};
/// Shell integration values
pub const ShellIntegration = enum {
none,
@ -6929,6 +7001,7 @@ pub const MacAppIcon = enum {
paper,
retro,
xray,
custom,
@"custom-style",
};
@ -7024,6 +7097,12 @@ pub const WindowNewTabPosition = enum {
end,
};
/// See macos-dock-drop-behavior
pub const MacOSDockDropBehavior = enum {
@"new-tab",
window,
};
/// See window-show-tab-bar
pub const WindowShowTabBar = enum {
always,

View File

@ -147,6 +147,8 @@ pub const FileFormatter = struct {
opts: std.fmt.FormatOptions,
writer: anytype,
) !void {
@setEvalBranchQuota(10_000);
_ = layout;
_ = opts;

View File

@ -937,6 +937,9 @@ test init {
}
test "add full" {
// This test is way too slow to run under Valgrind, unfortunately.
if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;
const testFont = font.embedded.regular;

View File

@ -413,6 +413,7 @@ test "fontconfig" {
// Get a deferred face from fontconfig
var def = def: {
var fc = discovery.Fontconfig.init();
defer fc.deinit();
var it = try fc.discover(alloc, .{ .family = "monospace", .size = 12 });
defer it.deinit();
break :def (try it.next()).?;

View File

@ -897,6 +897,7 @@ test "fontconfig" {
const alloc = testing.allocator;
var fc = Fontconfig.init();
defer fc.deinit();
var it = try fc.discover(alloc, .{ .family = "monospace", .size = 12 });
defer it.deinit();
}
@ -908,12 +909,14 @@ test "fontconfig codepoint" {
const alloc = testing.allocator;
var fc = Fontconfig.init();
defer fc.deinit();
var it = try fc.discover(alloc, .{ .codepoint = 'A', .size = 12 });
defer it.deinit();
// The first result should have the codepoint. Later ones may not
// because fontconfig returns all fonts sorted.
const face = (try it.next()).?;
var face = (try it.next()).?;
defer face.deinit();
try testing.expect(face.hasCodepoint('A', null));
// Should have other codepoints too

View File

@ -408,18 +408,15 @@ pub const Face = struct {
const px_x: i32 = @intFromFloat(@floor(x));
const px_y: i32 = @intFromFloat(@floor(y));
// We offset our glyph by its bearings when we draw it, so that it's
// rendered fully inside our canvas area, but we make sure to keep the
// fractional pixel offset so that we rasterize with the appropriate
// sub-pixel position.
// We keep track of the fractional part of the pixel bearings, which
// we will add as an offset when rasterizing to make sure we get the
// correct sub-pixel position.
const frac_x = x - @floor(x);
const frac_y = y - @floor(y);
const draw_x = -rect.origin.x + frac_x;
const draw_y = -rect.origin.y + frac_y;
// Add the fractional pixel to the width and height and take
// the ceiling to get a canvas size that will definitely fit
// our drawn glyph.
// our drawn glyph, including the fractional offset.
const px_width: u32 = @intFromFloat(@ceil(width + frac_x));
const px_height: u32 = @intFromFloat(@ceil(height + frac_y));
@ -525,6 +522,17 @@ pub const Face = struct {
context.setLineWidth(ctx, line_width);
}
// Translate our drawing context so that when we draw our
// glyph the bottom/left edge is at the correct sub-pixel
// position. The bottom/left edges are guaranteed to be at
// exactly [0, 0] relative to this because when we call to
// `drawGlyphs`, we pass the negated bearings.
context.translateCTM(
ctx,
frac_x,
frac_y,
);
// Scale the drawing context so that when we draw
// our glyph it's stretched to the constrained size.
context.scaleCTM(
@ -534,7 +542,15 @@ pub const Face = struct {
);
// Draw our glyph.
self.font.drawGlyphs(&glyphs, &.{.{ .x = draw_x, .y = draw_y }}, ctx);
//
// We offset the position by the negated bearings so that the
// glyph is drawn at exactly [0, 0], which is then offset to
// the appropriate fractional position by the translation we
// did before scaling.
self.font.drawGlyphs(&glyphs, &.{.{
.x = -rect.origin.x,
.y = -rect.origin.y,
}}, ctx);
// Write our rasterized glyph to the atlas.
const region = try atlas.reserve(alloc, px_width, px_height);

View File

@ -511,6 +511,9 @@ fn testDrawRanges(
}
test "sprite face render all sprites" {
// This test is way too slow to run under Valgrind, unfortunately.
if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest;
// Renders all sprites to an atlas and compares
// it to a ground truth for regression testing.

View File

@ -589,6 +589,84 @@ pub const Key = enum(c_int) {
};
}
/// Whether this key should be remappable by the operating system.
///
/// On certain OSes (namely Linux and the BSDs) certain keys like the
/// functional keys are expected to be remappable by the user, such as
/// in the very common use case of swapping the Caps Lock key with the
/// Escape key with the XKB option `caps:swapescape`.
///
/// However, the way XKB implements this is by essentially acting as a
/// software key remapper that destroys all information about the original
/// physical key, leading to very annoying bugs like #7309 where the
/// physical key `XKB_KEY_c` gets remapped into `XKB_KEY_Cyrillic_tse`,
/// which causes all of our physical key handling to completely break down.
/// _Very naughty._
///
/// As a compromise, given that writing system keys (§3.1.1) comprise the
/// majority of keys that "change meaning [...] based on the current locale
/// and keyboard layout", we allow all other keys to be remapped by default
/// since they should be fairly harmless. We might consider making this
/// configurable, but for now this should at least placate most people.
pub fn shouldBeRemappable(self: Key) bool {
return switch (self) {
// "Writing System Keys" § 3.1.1
.backquote,
.backslash,
.bracket_left,
.bracket_right,
.comma,
.digit_0,
.digit_1,
.digit_2,
.digit_3,
.digit_4,
.digit_5,
.digit_6,
.digit_7,
.digit_8,
.digit_9,
.equal,
.intl_backslash,
.intl_ro,
.intl_yen,
.key_a,
.key_b,
.key_c,
.key_d,
.key_e,
.key_f,
.key_g,
.key_h,
.key_i,
.key_j,
.key_k,
.key_l,
.key_m,
.key_n,
.key_o,
.key_p,
.key_q,
.key_r,
.key_s,
.key_t,
.key_u,
.key_v,
.key_w,
.key_x,
.key_y,
.key_z,
.minus,
.period,
.quote,
.semicolon,
.slash,
=> false,
else => true,
};
}
/// Returns true if this is a keypad key.
pub fn keypad(self: Key) bool {
return switch (self) {

View File

@ -172,13 +172,10 @@ pub fn init(surface: *Surface) !Inspector {
.surface = surface,
.key_events = key_buf,
.vt_events = vt_events,
.vt_stream = .{
.handler = vt_handler,
.parser = .{
.osc_parser = .{
.alloc = surface.alloc,
},
},
.vt_stream = stream: {
var s: inspector.termio.Stream = .init(vt_handler);
s.parser.osc_parser.alloc = surface.alloc;
break :stream s;
},
};
}

View File

@ -29,14 +29,15 @@ fi
# Use try-always to have the right error code.
{
# Zsh treats empty $ZDOTDIR as if it was "/". We do the same.
# Zsh treats unset ZDOTDIR as if it was HOME. We do the same.
#
# Source the user's zshenv before sourcing ghostty.zsh because the former
# might set fpath and other things without which ghostty.zsh won't work.
# Source the user's .zshenv before sourcing ghostty-integration because the
# former might set fpath and other things without which ghostty-integration
# won't work.
#
# Use typeset in case we are in a function with warn_create_global in
# effect. Unlikely but better safe than sorry.
'builtin' 'typeset' _ghostty_file=${ZDOTDIR-~}"/.zshenv"
'builtin' 'typeset' _ghostty_file=${ZDOTDIR-$HOME}"/.zshenv"
# Zsh ignores unreadable rc files. We do the same.
# Zsh ignores rc files that are directories, and so does source.
[[ ! -r "$_ghostty_file" ]] || 'builtin' 'source' '--' "$_ghostty_file"
@ -45,6 +46,7 @@ fi
'builtin' 'autoload' '--' 'is-at-least'
'is-at-least' "5.1" || {
builtin echo "ZSH ${ZSH_VERSION} is too old for ghostty shell integration" > /dev/stderr
'builtin' 'unset' '_ghostty_file'
return
}
# ${(%):-%x} is the path to the current file.

View File

@ -196,7 +196,7 @@ test "OSC generator valid" {
};
for (0..50) |_| {
const seq = try gen.next(&buf);
var parser: terminal.osc.Parser = .{};
var parser: terminal.osc.Parser = .init();
for (seq[2 .. seq.len - 1]) |c| parser.next(c);
try testing.expect(parser.end(null) != null);
}
@ -214,7 +214,7 @@ test "OSC generator invalid" {
};
for (0..50) |_| {
const seq = try gen.next(&buf);
var parser: terminal.osc.Parser = .{};
var parser: terminal.osc.Parser = .init();
for (seq[2 .. seq.len - 1]) |c| parser.next(c);
try testing.expect(parser.end(null) == null);
}

View File

@ -1004,23 +1004,34 @@ const ReflowCursor = struct {
// Copy the graphemes
const cps = src_page.lookupGrapheme(cell).?;
// If our page can't support an additional cell with
// graphemes then we create a new page for this row.
// If our page can't support an additional cell
// with graphemes then we increase capacity.
if (self.page.graphemeCount() >= self.page.graphemeCapacity()) {
try self.moveLastRowToNewPage(list, cap);
} else {
// Attempt to allocate the space that would be required for
// these graphemes, and if it's not available, create a new
// page for this row.
if (self.page.grapheme_alloc.alloc(
u21,
self.page.memory,
cps.len,
)) |slice| {
self.page.grapheme_alloc.free(self.page.memory, slice);
} else |_| {
try self.moveLastRowToNewPage(list, cap);
try self.adjustCapacity(list, .{
.hyperlink_bytes = cap.grapheme_bytes * 2,
});
}
// Attempt to allocate the space that would be required
// for these graphemes, and if it's not available, then
// increase capacity.
if (self.page.grapheme_alloc.alloc(
u21,
self.page.memory,
cps.len,
)) |slice| {
self.page.grapheme_alloc.free(self.page.memory, slice);
} else |_| {
// Grow our capacity until we can
// definitely fit the extra bytes.
const required = cps.len * @sizeOf(u21);
var new_grapheme_capacity: usize = cap.grapheme_bytes;
while (new_grapheme_capacity - cap.grapheme_bytes < required) {
new_grapheme_capacity *= 2;
}
try self.adjustCapacity(list, .{
.grapheme_bytes = new_grapheme_capacity,
});
}
// This shouldn't fail since we made sure we have space above.
@ -1032,25 +1043,67 @@ const ReflowCursor = struct {
const src_id = src_page.lookupHyperlink(cell).?;
const src_link = src_page.hyperlink_set.get(src_page.memory, src_id);
// If our page can't support an additional cell with
// a hyperlink ID then we create a new page for this row.
// If our page can't support an additional cell
// with a hyperlink then we increase capacity.
if (self.page.hyperlinkCount() >= self.page.hyperlinkCapacity()) {
try self.moveLastRowToNewPage(list, cap);
try self.adjustCapacity(list, .{
.hyperlink_bytes = cap.hyperlink_bytes * 2,
});
}
// Ensure that the string alloc has sufficient capacity
// to dupe the link (and the ID if it's not implicit).
const additional_required_string_capacity =
src_link.uri.len +
switch (src_link.id) {
.explicit => |v| v.len,
.implicit => 0,
};
if (self.page.string_alloc.alloc(
u8,
self.page.memory,
additional_required_string_capacity,
)) |slice| {
// We have enough capacity, free the test alloc.
self.page.string_alloc.free(self.page.memory, slice);
} else |_| {
// Grow our capacity until we can
// definitely fit the extra bytes.
var new_string_capacity: usize = cap.string_bytes;
while (new_string_capacity - cap.string_bytes < additional_required_string_capacity) {
new_string_capacity *= 2;
}
try self.adjustCapacity(list, .{
.string_bytes = new_string_capacity,
});
}
const dst_id = self.page.hyperlink_set.addWithIdContext(
self.page.memory,
// We made sure there was enough capacity for this above.
try src_link.dupe(src_page, self.page),
src_id,
.{ .page = self.page },
) catch id: {
// We have no space for this link,
// so make a new page for this row.
try self.moveLastRowToNewPage(list, cap);
) catch |err| id: {
// If the add failed then either the set needs to grow
// or it needs to be rehashed. Either one of those can
// be accomplished by adjusting capacity, either with
// no actual change or with an increased hyperlink cap.
try self.adjustCapacity(list, switch (err) {
error.OutOfMemory => .{
.hyperlink_bytes = cap.hyperlink_bytes * 2,
},
error.NeedsRehash => .{},
});
break :id try self.page.hyperlink_set.addContext(
// We assume this one will succeed. We dupe the link
// again, and don't have to worry about the other one
// because adjusting the capacity naturally clears up
// any managed memory not associated with a cell yet.
break :id try self.page.hyperlink_set.addWithIdContext(
self.page.memory,
try src_link.dupe(src_page, self.page),
src_id,
.{ .page = self.page },
);
} orelse src_id;
@ -1075,14 +1128,23 @@ const ReflowCursor = struct {
self.page.memory,
style,
cell.style_id,
) catch id: {
// We have no space for this style,
// so make a new page for this row.
try self.moveLastRowToNewPage(list, cap);
) catch |err| id: {
// If the add failed then either the set needs to grow
// or it needs to be rehashed. Either one of those can
// be accomplished by adjusting capacity, either with
// no actual change or with an increased style cap.
try self.adjustCapacity(list, switch (err) {
error.OutOfMemory => .{
.styles = cap.styles * 2,
},
error.NeedsRehash => .{},
});
break :id try self.page.styles.add(
// We assume this one will succeed.
break :id try self.page.styles.addWithId(
self.page.memory,
style,
cell.style_id,
);
} orelse cell.style_id;
@ -1150,6 +1212,22 @@ const ReflowCursor = struct {
}
}
/// Adjust the capacity of the current page.
fn adjustCapacity(
self: *ReflowCursor,
list: *PageList,
adjustment: AdjustCapacity,
) !void {
const old_x = self.x;
const old_y = self.y;
self.* = .init(try list.adjustCapacity(
self.node,
adjustment,
));
self.cursorAbsolute(old_x, old_y);
}
/// True if this cursor is at the bottom of the page by capacity,
/// i.e. we can't scroll anymore.
fn bottom(self: *const ReflowCursor) bool {
@ -2317,8 +2395,8 @@ pub fn eraseRows(
break;
}
self.erasePage(chunk.node);
erased += chunk.node.data.size.rows;
self.erasePage(chunk.node);
continue;
}
@ -7029,6 +7107,7 @@ test "PageList resize reflow less cols wrap across page boundary cursor in secon
try testing.expect(!cells[3].hasText());
}
}
test "PageList resize reflow more cols cursor in wrapped row" {
const testing = std.testing;
const alloc = testing.allocator;
@ -7222,6 +7301,296 @@ test "PageList resize reflow more cols no reflow preserves semantic prompt" {
}
}
test "PageList resize reflow exceeds hyperlink memory forcing capacity increase" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 2, 10, 0);
defer s.deinit();
try testing.expectEqual(@as(usize, 1), s.totalPages());
// Grow to the capacity of the first page and add
// one more row so that we have two pages total.
{
const page = &s.pages.first.?.data;
page.pauseIntegrityChecks(true);
for (page.size.rows..page.capacity.rows) |_| {
_ = try s.grow();
}
page.pauseIntegrityChecks(false);
try testing.expectEqual(@as(usize, 1), s.totalPages());
try s.growRows(1);
try testing.expectEqual(@as(usize, 2), s.totalPages());
// We now have two pages.
try std.testing.expect(s.pages.first.? != s.pages.last.?);
try std.testing.expectEqual(s.pages.last.?, s.pages.first.?.next);
}
// We use almost all string alloc capacity with a hyperlink in the final
// row of the first page, and do the same on the first row of the second
// page. We also mark the row as wrapped so that when we resize with more
// cols the row unwraps and we have a single row that requires almost two
// times the base string alloc capacity.
//
// This forces the reflow to increase capacity.
//
// +--+ = PAGE 0
// : :
// | X <- where X is hyperlinked with almost all string cap.
// +--+
// +--+ = PAGE 1
// X | <- X here also almost hits string cap with a hyperlink.
// +--+
// Almost hit string alloc cap in bottom right of first page.
// Mark the final row as wrapped.
{
const page = &s.pages.first.?.data;
const id = try page.insertHyperlink(.{
.id = .{ .implicit = 0 },
.uri = "a" ** (pagepkg.string_bytes_default - 1),
});
const rac = page.getRowAndCell(page.size.cols - 1, page.size.rows - 1);
rac.row.wrap = true;
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = 'X' },
};
try page.setHyperlink(rac.row, rac.cell, id);
try std.testing.expectError(
error.StringsOutOfMemory,
page.insertHyperlink(.{
.id = .{ .implicit = 1 },
.uri = "AAAAAAAAAAAAAAAAAAAAAAAAAA",
}),
);
}
// Almost hit string alloc cap in top left of second page.
// Mark the first row as a wrap continuation.
{
const page = &s.pages.last.?.data;
const id = try page.insertHyperlink(.{
.id = .{ .implicit = 1 },
.uri = "a" ** (pagepkg.string_bytes_default - 1),
});
const rac = page.getRowAndCell(0, 0);
rac.row.wrap_continuation = true;
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = 'X' },
};
try page.setHyperlink(rac.row, rac.cell, id);
try std.testing.expectError(
error.StringsOutOfMemory,
page.insertHyperlink(.{
.id = .{ .implicit = 2 },
.uri = "AAAAAAAAAAAAAAAAAAAAAAAAAA",
}),
);
}
// Resize to 1 column wider, unwrapping the row.
try s.resize(.{ .cols = s.cols + 1, .reflow = true });
}
test "PageList resize reflow exceeds grapheme memory forcing capacity increase" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 2, 10, 0);
defer s.deinit();
try testing.expectEqual(@as(usize, 1), s.totalPages());
// Grow to the capacity of the first page and add
// one more row so that we have two pages total.
{
const page = &s.pages.first.?.data;
page.pauseIntegrityChecks(true);
for (page.size.rows..page.capacity.rows) |_| {
_ = try s.grow();
}
page.pauseIntegrityChecks(false);
try testing.expectEqual(@as(usize, 1), s.totalPages());
try s.growRows(1);
try testing.expectEqual(@as(usize, 2), s.totalPages());
// We now have two pages.
try std.testing.expect(s.pages.first.? != s.pages.last.?);
try std.testing.expectEqual(s.pages.last.?, s.pages.first.?.next);
}
// We use almost all grapheme alloc capacity with a grapheme in the final
// row of the first page, and do the same on the first row of the second
// page. We also mark the row as wrapped so that when we resize with more
// cols the row unwraps and we have a single row that requires almost two
// times the base grapheme alloc capacity.
//
// This forces the reflow to increase capacity.
//
// +--+ = PAGE 0
// : :
// | X <- where X is a grapheme which uses almost all the capacity.
// +--+
// +--+ = PAGE 1
// X | <- X here also almost hits grapheme cap.
// +--+
// Almost hit grapheme alloc cap in bottom right of first page.
// Mark the final row as wrapped.
{
const page = &s.pages.first.?.data;
const rac = page.getRowAndCell(page.size.cols - 1, page.size.rows - 1);
rac.row.wrap = true;
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = 'X' },
};
try page.setGraphemes(
rac.row,
rac.cell,
&@as(
[
@divFloor(
pagepkg.grapheme_bytes_default - 1,
@sizeOf(u21),
)
]u21,
@splat('a'),
),
);
try std.testing.expectError(
error.OutOfMemory,
page.grapheme_alloc.alloc(
u21,
page.memory,
16,
),
);
}
// Almost hit grapheme alloc cap in top left of second page.
// Mark the first row as a wrap continuation.
{
const page = &s.pages.last.?.data;
const rac = page.getRowAndCell(0, 0);
rac.row.wrap = true;
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = 'X' },
};
try page.setGraphemes(
rac.row,
rac.cell,
&@as(
[
@divFloor(
pagepkg.grapheme_bytes_default - 1,
@sizeOf(u21),
)
]u21,
@splat('a'),
),
);
try std.testing.expectError(
error.OutOfMemory,
page.grapheme_alloc.alloc(
u21,
page.memory,
16,
),
);
}
// Resize to 1 column wider, unwrapping the row.
try s.resize(.{ .cols = s.cols + 1, .reflow = true });
}
test "PageList resize reflow exceeds style memory forcing capacity increase" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, pagepkg.std_capacity.styles - 1, 10, 0);
defer s.deinit();
try testing.expectEqual(@as(usize, 1), s.totalPages());
// Grow to the capacity of the first page and add
// one more row so that we have two pages total.
{
const page = &s.pages.first.?.data;
page.pauseIntegrityChecks(true);
for (page.size.rows..page.capacity.rows) |_| {
_ = try s.grow();
}
page.pauseIntegrityChecks(false);
try testing.expectEqual(@as(usize, 1), s.totalPages());
try s.growRows(1);
try testing.expectEqual(@as(usize, 2), s.totalPages());
// We now have two pages.
try std.testing.expect(s.pages.first.? != s.pages.last.?);
try std.testing.expectEqual(s.pages.last.?, s.pages.first.?.next);
}
// Give each cell in the final row of the first page a unique style.
// Mark the final row as wrapped.
{
const page = &s.pages.first.?.data;
for (0..s.cols) |x| {
const id = page.styles.add(
page.memory,
.{
.bg_color = .{ .rgb = .{
.r = @truncate(x),
.g = @truncate(x >> 8),
.b = @truncate(x >> 16),
} },
},
) catch break;
const rac = page.getRowAndCell(x, page.size.rows - 1);
rac.row.wrap = true;
rac.row.styled = true;
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = 'X' },
.style_id = id,
};
}
}
// Do the same for the first row of the second page.
// Mark the first row as a wrap continuation.
{
const page = &s.pages.last.?.data;
for (0..s.cols) |x| {
const id = page.styles.add(
page.memory,
.{
.fg_color = .{ .rgb = .{
.r = @truncate(x),
.g = @truncate(x >> 8),
.b = @truncate(x >> 16),
} },
},
) catch break;
const rac = page.getRowAndCell(x, 0);
rac.row.wrap_continuation = true;
rac.row.styled = true;
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = 'X' },
.style_id = id,
};
}
}
// Resize to twice as wide, fully unwrapping the row.
try s.resize(.{ .cols = s.cols * 2, .reflow = true });
}
test "PageList resize reflow more cols unwrap wide spacer head" {
const testing = std.testing;
const alloc = testing.allocator;
@ -7767,6 +8136,7 @@ test "PageList resize reflow less cols wrapped rows with graphemes" {
try testing.expectEqual(@as(u21, 'A'), cps[0]);
}
}
test "PageList resize reflow less cols cursor in wrapped row" {
const testing = std.testing;
const alloc = testing.allocator;

View File

@ -209,24 +209,42 @@ const MAX_INTERMEDIATE = 4;
const MAX_PARAMS = 24;
/// Current state of the state machine
state: State = .ground,
state: State,
/// Intermediate tracking.
intermediates: [MAX_INTERMEDIATE]u8 = undefined,
intermediates_idx: u8 = 0,
intermediates: [MAX_INTERMEDIATE]u8,
intermediates_idx: u8,
/// Param tracking, building
params: [MAX_PARAMS]u16 = undefined,
params_sep: Action.CSI.SepList = .initEmpty(),
params_idx: u8 = 0,
param_acc: u16 = 0,
param_acc_idx: u8 = 0,
params: [MAX_PARAMS]u16,
params_sep: Action.CSI.SepList,
params_idx: u8,
param_acc: u16,
param_acc_idx: u8,
/// Parser for OSC sequences
osc_parser: osc.Parser = .{},
osc_parser: osc.Parser,
pub fn init() Parser {
return .{};
var result: Parser = .{
.state = .ground,
.intermediates_idx = 0,
.params_sep = .initEmpty(),
.params_idx = 0,
.param_acc = 0,
.param_acc_idx = 0,
.osc_parser = .init(),
.intermediates = undefined,
.params = undefined,
};
if (std.valgrind.runningOnValgrind() > 0) {
// Initialize our undefined fields so Valgrind can catch it.
// https://github.com/ziglang/zig/issues/19148
result.intermediates = undefined;
result.params = undefined;
}
return result;
}
pub fn deinit(self: *Parser) void {

View File

@ -233,6 +233,11 @@ pub fn deinit(self: *Screen) void {
/// ensure they're also calling page integrity checks if necessary.
pub fn assertIntegrity(self: *const Screen) void {
if (build_config.slow_runtime_safety) {
// We don't run integrity checks on Valgrind because its soooooo slow,
// Valgrind is our integrity checker, and we run these during unit
// tests (non-Valgrind) anyways so we're verifying anyways.
if (std.valgrind.runningOnValgrind() > 0) return;
assert(self.cursor.x < self.pages.cols);
assert(self.cursor.y < self.pages.rows);

View File

@ -129,6 +129,7 @@ pub fn resize(self: *Tabstops, alloc: Allocator, cols: usize) !void {
// Note: we can probably try to realloc here but I'm not sure it matters.
const new = try alloc.alloc(Unit, size);
@memset(new, 0);
if (self.dynamic_stops.len > 0) {
fastmem.copy(Unit, new, self.dynamic_stops);
alloc.free(self.dynamic_stops);

View File

@ -2824,14 +2824,21 @@ test "Terminal: input glitch text" {
var t = try init(alloc, .{ .cols = 30, .rows = 30 });
defer t.deinit(alloc);
const page = t.screen.pages.pages.first.?;
const grapheme_cap = page.data.capacity.grapheme_bytes;
// Get our initial grapheme capacity.
const grapheme_cap = cap: {
const page = t.screen.pages.pages.first.?;
break :cap page.data.capacity.grapheme_bytes;
};
while (page.data.capacity.grapheme_bytes == grapheme_cap) {
// Print glitch text until our capacity changes
while (true) {
const page = t.screen.pages.pages.first.?;
if (page.data.capacity.grapheme_bytes != grapheme_cap) break;
try t.printString(glitch);
}
// We're testing to make sure that grapheme capacity gets increased.
const page = t.screen.pages.pages.first.?;
try testing.expect(page.data.capacity.grapheme_bytes > grapheme_cap);
}

View File

@ -108,28 +108,59 @@ pub fn BitmapAllocator(comptime chunk_size: comptime_int) type {
const chunks = self.chunks.ptr(base);
const chunk_idx = @divExact(@intFromPtr(slice.ptr) - @intFromPtr(chunks), chunk_size);
// From the chunk index, we can find the starting bitmap index
// and the bit within the last bitmap.
var bitmap_idx = @divFloor(chunk_idx, 64);
const bitmap_bit = chunk_idx % 64;
const bitmaps = self.bitmap.ptr(base);
// If our chunk count is over 64 then we need to handle the
// case where we have to mark multiple bitmaps.
if (chunk_count > 64) {
const bitmaps_full = @divFloor(chunk_count, 64);
for (0..bitmaps_full) |i| bitmaps[bitmap_idx + i] = std.math.maxInt(u64);
bitmap_idx += bitmaps_full;
// Current bitmap index.
var i: usize = @divFloor(chunk_idx, 64);
// Number of chunks we still have to mark as free.
var rem: usize = chunk_count;
// Mark any bits in the starting bitmap that need to be marked.
{
// Bit index.
const bit = chunk_idx % 64;
// Number of bits we need to mark in this bitmap.
const bits = @min(rem, 64 - bit);
bitmaps[i] |= ~@as(u64, 0) >> @intCast(64 - bits) << @intCast(bit);
rem -= bits;
}
// Set the bitmap to mark the chunks as free. Note we have to
// do chunk_count % 64 to handle the case where our chunk count
// is using multiple bitmaps.
const bitmap = &bitmaps[bitmap_idx];
for (0..chunk_count % 64) |i| {
const mask = @as(u64, 1) << @intCast(bitmap_bit + i);
bitmap.* |= mask;
// Mark any full bitmaps worth of bits that need to be marked.
i += 1;
while (rem > 64) : (i += 1) {
bitmaps[i] = std.math.maxInt(u64);
rem -= 64;
}
// Mark any bits at the start of this last bitmap if it needs it.
if (rem > 0) {
bitmaps[i] |= ~@as(u64, 0) >> @intCast(64 - rem);
}
}
/// For testing only.
fn isAllocated(self: *Self, base: anytype, slice: anytype) bool {
comptime assert(@import("builtin").is_test);
const bytes = std.mem.sliceAsBytes(slice);
const aligned_len = std.mem.alignForward(usize, bytes.len, chunk_size);
const chunk_count = @divExact(aligned_len, chunk_size);
const chunks = self.chunks.ptr(base);
const chunk_idx = @divExact(@intFromPtr(slice.ptr) - @intFromPtr(chunks), chunk_size);
const bitmaps = self.bitmap.ptr(base);
for (chunk_idx..chunk_idx + chunk_count) |i| {
const bitmap = @divFloor(i, bitmap_bit_size);
const bit = i % bitmap_bit_size;
if (bitmaps[bitmap] & (@as(u64, 1) << @intCast(bit)) != 0) {
return false;
}
}
return true;
}
/// For debugging
@ -188,50 +219,56 @@ fn findFreeChunks(bitmaps: []u64, n: usize) ?usize {
// I'm not a bit twiddling expert. Perhaps even SIMD could be used here
// but unsure. Contributor friendly: let's benchmark and improve this!
// Large chunks require special handling. In this case we look for
// divFloor sequential chunks that are maxInt, then look for the mod
// normally in the next bitmap.
// Large chunks require special handling.
if (n > @bitSizeOf(u64)) {
const div = @divFloor(n, @bitSizeOf(u64));
const mod = n % @bitSizeOf(u64);
var seq: usize = 0;
for (bitmaps, 0..) |*bitmap, idx| {
// If we aren't fully empty then reset the sequence
if (bitmap.* != std.math.maxInt(u64)) {
seq = 0;
var i: usize = 0;
search: while (i < bitmaps.len) {
// Number of chunks available at the end of this bitmap.
const prefix = @clz(~bitmaps[i]);
// If there are no chunks available at the end of this bitmap
// then we can't start in it, so we'll try the next one.
if (prefix == 0) {
i += 1;
continue;
}
// If we haven't reached the sequence count we're looking for
// then add one and continue, we're still accumulating blanks.
if (seq != div) {
seq += 1;
if (seq != div or mod > 0) continue;
// Starting position if we manage to find the span we need here.
const start_bitmap = i;
const start_bit = 64 - prefix;
// The remaining number of sequential free chunks we need to find.
var rem: usize = n - prefix;
i += 1;
while (rem > 64) : (i += 1) {
// We ran out of bitmaps, there's no sufficiently large gap.
if (i >= bitmaps.len) return null;
// There's more than 64 remaining chunks and this bitmap has
// content in it, so we try starting again with this bitmap.
if (bitmaps[i] != std.math.maxInt(u64)) continue :search;
// This bitmap is completely free, we can subtract 64 from
// our remaining number.
rem -= 64;
}
// We've reached the seq count see if this has mod starting empty
// blanks.
if (mod > 0) {
const final = @as(u64, std.math.maxInt(u64)) >> @intCast(64 - mod);
if (bitmap.* & final == 0) {
// No blanks, reset.
seq = 0;
continue;
}
// If the number of available chunks at the start of this bitmap
// is less than the remaining required, we have to try again.
if (@ctz(~bitmaps[i]) < rem) continue;
bitmap.* ^= final;
const suffix = (n - prefix) % 64;
// Found! Mark everything between our start and end as full.
bitmaps[start_bitmap] ^= ~@as(u64, 0) >> @intCast(start_bit) << @intCast(start_bit);
const full_bitmaps = @divFloor(n - prefix - suffix, 64);
for (bitmaps[start_bitmap + 1 ..][0..full_bitmaps]) |*bitmap| {
bitmap.* = 0;
}
if (suffix > 0) bitmaps[i] ^= ~@as(u64, 0) >> @intCast(64 - suffix);
// Found! Set all in our sequence to full and mask our final.
// The "zero_mod" modifier below handles the case where we have
// a perfectly divisible number of chunks so we don't have to
// mark the trailing bitmap.
const zero_mod = @intFromBool(mod == 0);
const start_idx = idx - (seq - zero_mod);
const end_idx = idx + zero_mod;
for (start_idx..end_idx) |i| bitmaps[i] = 0;
return (start_idx * 64);
return start_bitmap * 64 + start_bit;
}
return null;
@ -349,18 +386,18 @@ test "findFreeChunks larger than 64 chunks not at beginning" {
};
const idx = findFreeChunks(&bitmaps, 65).?;
try testing.expectEqual(
0b11111111_00000000_00000000_00000000_00000000_00000000_00000000_00000000,
0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000,
bitmaps[0],
);
try testing.expectEqual(
0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000,
0b11111110_00000000_00000000_00000000_00000000_00000000_00000000_00000000,
bitmaps[1],
);
try testing.expectEqual(
0b11111111_11111111_11111111_11111111_11111111_11111111_11111111_11111110,
0b11111111_11111111_11111111_11111111_11111111_11111111_11111111_11111111,
bitmaps[2],
);
try testing.expectEqual(@as(usize, 64), idx);
try testing.expectEqual(@as(usize, 56), idx);
}
test "findFreeChunks larger than 64 chunks exact" {
@ -483,3 +520,438 @@ test "BitmapAllocator alloc large" {
ptr[0] = 'A';
bm.free(buf, ptr);
}
test "BitmapAllocator alloc and free one bitmap" {
const Alloc = BitmapAllocator(1);
// Capacity such that we'll have 3 bitmaps.
const cap = Alloc.bitmap_bit_size * 3;
const testing = std.testing;
const alloc = testing.allocator;
const layout = Alloc.layout(cap);
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
defer alloc.free(buf);
var bm = Alloc.init(.init(buf), layout);
// Allocate exactly one bitmap worth of bytes.
const slice = try bm.alloc(u8, buf, Alloc.bitmap_bit_size);
try testing.expectEqual(Alloc.bitmap_bit_size, slice.len);
@memset(slice, 0x11);
try testing.expectEqualSlices(
u8,
&@as([Alloc.bitmap_bit_size]u8, @splat(0x11)),
slice,
);
// Free it
try testing.expect(bm.isAllocated(buf, slice));
bm.free(buf, slice);
try testing.expect(!bm.isAllocated(buf, slice));
// All of our bitmaps should be free.
try testing.expectEqualSlices(
u64,
&@as([3]u64, @splat(~@as(u64, 0))),
bm.bitmap.ptr(buf)[0..3],
);
}
test "BitmapAllocator alloc and free half bitmap" {
const Alloc = BitmapAllocator(1);
// Capacity such that we'll have 3 bitmaps.
const cap = Alloc.bitmap_bit_size * 3;
const testing = std.testing;
const alloc = testing.allocator;
const layout = Alloc.layout(cap);
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
defer alloc.free(buf);
var bm = Alloc.init(.init(buf), layout);
// Allocate exactly half a bitmap worth of bytes.
const slice = try bm.alloc(u8, buf, Alloc.bitmap_bit_size / 2);
try testing.expectEqual(Alloc.bitmap_bit_size / 2, slice.len);
@memset(slice, 0x11);
try testing.expectEqualSlices(
u8,
&@as([Alloc.bitmap_bit_size / 2]u8, @splat(0x11)),
slice,
);
// Free it
try testing.expect(bm.isAllocated(buf, slice));
bm.free(buf, slice);
try testing.expect(!bm.isAllocated(buf, slice));
// All of our bitmaps should be free.
try testing.expectEqualSlices(
u64,
&@as([3]u64, @splat(~@as(u64, 0))),
bm.bitmap.ptr(buf)[0..3],
);
}
test "BitmapAllocator alloc and free two half bitmaps" {
const Alloc = BitmapAllocator(1);
// Capacity such that we'll have 3 bitmaps.
const cap = Alloc.bitmap_bit_size * 3;
const testing = std.testing;
const alloc = testing.allocator;
const layout = Alloc.layout(cap);
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
defer alloc.free(buf);
var bm = Alloc.init(.init(buf), layout);
// Allocate exactly one bitmap worth of bytes across two allocations.
const slice = try bm.alloc(u8, buf, Alloc.bitmap_bit_size / 2);
try testing.expectEqual(Alloc.bitmap_bit_size / 2, slice.len);
@memset(slice, 0x11);
try testing.expectEqualSlices(
u8,
&@as([Alloc.bitmap_bit_size / 2]u8, @splat(0x11)),
slice,
);
const slice2 = try bm.alloc(u8, buf, Alloc.bitmap_bit_size / 2);
try testing.expectEqual(Alloc.bitmap_bit_size / 2, slice2.len);
@memset(slice2, 0x22);
try testing.expectEqualSlices(
u8,
&@as([Alloc.bitmap_bit_size / 2]u8, @splat(0x22)),
slice2,
);
try testing.expectEqualSlices(
u8,
&@as([Alloc.bitmap_bit_size / 2]u8, @splat(0x11)),
slice,
);
// Free them
try testing.expect(bm.isAllocated(buf, slice2));
bm.free(buf, slice2);
try testing.expect(!bm.isAllocated(buf, slice2));
try testing.expect(bm.isAllocated(buf, slice));
bm.free(buf, slice);
try testing.expect(!bm.isAllocated(buf, slice));
// All of our bitmaps should be free.
try testing.expectEqualSlices(
u64,
&@as([3]u64, @splat(~@as(u64, 0))),
bm.bitmap.ptr(buf)[0..3],
);
}
test "BitmapAllocator alloc and free 1.5 bitmaps" {
const Alloc = BitmapAllocator(1);
// Capacity such that we'll have 3 bitmaps.
const cap = Alloc.bitmap_bit_size * 3;
const testing = std.testing;
const alloc = testing.allocator;
const layout = Alloc.layout(cap);
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
defer alloc.free(buf);
var bm = Alloc.init(.init(buf), layout);
// Allocate exactly 1.5 bitmaps worth of bytes.
const slice = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 2);
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 2, slice.len);
@memset(slice, 0x11);
try testing.expectEqualSlices(
u8,
&@as([3 * Alloc.bitmap_bit_size / 2]u8, @splat(0x11)),
slice,
);
// Free them
try testing.expect(bm.isAllocated(buf, slice));
bm.free(buf, slice);
try testing.expect(!bm.isAllocated(buf, slice));
// All of our bitmaps should be free.
try testing.expectEqualSlices(
u64,
&@as([3]u64, @splat(~@as(u64, 0))),
bm.bitmap.ptr(buf)[0..3],
);
}
test "BitmapAllocator alloc and free two 1.5 bitmaps" {
const Alloc = BitmapAllocator(1);
// Capacity such that we'll have 3 bitmaps.
const cap = Alloc.bitmap_bit_size * 3;
const testing = std.testing;
const alloc = testing.allocator;
const layout = Alloc.layout(cap);
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
defer alloc.free(buf);
var bm = Alloc.init(.init(buf), layout);
// Allocate exactly 3 bitmaps worth of bytes across two allocations.
const slice = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 2);
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 2, slice.len);
@memset(slice, 0x11);
try testing.expectEqualSlices(
u8,
&@as([3 * Alloc.bitmap_bit_size / 2]u8, @splat(0x11)),
slice,
);
const slice2 = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 2);
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 2, slice2.len);
@memset(slice2, 0x22);
try testing.expectEqualSlices(
u8,
&@as([3 * Alloc.bitmap_bit_size / 2]u8, @splat(0x22)),
slice2,
);
try testing.expectEqualSlices(
u8,
&@as([3 * Alloc.bitmap_bit_size / 2]u8, @splat(0x11)),
slice,
);
// Free them
try testing.expect(bm.isAllocated(buf, slice2));
bm.free(buf, slice2);
try testing.expect(!bm.isAllocated(buf, slice2));
try testing.expect(bm.isAllocated(buf, slice));
bm.free(buf, slice);
try testing.expect(!bm.isAllocated(buf, slice));
// All of our bitmaps should be free.
try testing.expectEqualSlices(
u64,
&@as([3]u64, @splat(~@as(u64, 0))),
bm.bitmap.ptr(buf)[0..3],
);
}
test "BitmapAllocator alloc and free 1.5 bitmaps offset by 0.75" {
const Alloc = BitmapAllocator(1);
// Capacity such that we'll have 3 bitmaps.
const cap = Alloc.bitmap_bit_size * 3;
const testing = std.testing;
const alloc = testing.allocator;
const layout = Alloc.layout(cap);
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
defer alloc.free(buf);
var bm = Alloc.init(.init(buf), layout);
// Allocate three quarters of a bitmap first.
const slice = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 4);
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 4, slice.len);
@memset(slice, 0x11);
try testing.expectEqualSlices(
u8,
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x11)),
slice,
);
// Then a 1.5 bitmap sized allocation, so that it spans
// from 0.75 to 2.25, occupying bits in 3 different bitmaps.
const slice2 = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 2);
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 2, slice2.len);
@memset(slice2, 0x22);
try testing.expectEqualSlices(
u8,
&@as([3 * Alloc.bitmap_bit_size / 2]u8, @splat(0x22)),
slice2,
);
try testing.expectEqualSlices(
u8,
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x11)),
slice,
);
// Free them
try testing.expect(bm.isAllocated(buf, slice2));
bm.free(buf, slice2);
try testing.expect(!bm.isAllocated(buf, slice2));
try testing.expect(bm.isAllocated(buf, slice));
bm.free(buf, slice);
try testing.expect(!bm.isAllocated(buf, slice));
// All of our bitmaps should be free.
try testing.expectEqualSlices(
u64,
&@as([3]u64, @splat(~@as(u64, 0))),
bm.bitmap.ptr(buf)[0..3],
);
}
test "BitmapAllocator alloc and free three 0.75 bitmaps" {
const Alloc = BitmapAllocator(1);
// Capacity such that we'll have 3 bitmaps.
const cap = Alloc.bitmap_bit_size * 3;
const testing = std.testing;
const alloc = testing.allocator;
const layout = Alloc.layout(cap);
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
defer alloc.free(buf);
var bm = Alloc.init(.init(buf), layout);
// Allocate three quarters of a bitmap three times.
const slice = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 4);
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 4, slice.len);
@memset(slice, 0x11);
try testing.expectEqualSlices(
u8,
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x11)),
slice,
);
const slice2 = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 4);
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 4, slice2.len);
@memset(slice2, 0x22);
try testing.expectEqualSlices(
u8,
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x22)),
slice2,
);
try testing.expectEqualSlices(
u8,
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x11)),
slice,
);
const slice3 = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 4);
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 4, slice3.len);
@memset(slice3, 0x33);
try testing.expectEqualSlices(
u8,
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x33)),
slice3,
);
try testing.expectEqualSlices(
u8,
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x22)),
slice2,
);
try testing.expectEqualSlices(
u8,
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x11)),
slice,
);
// Free them
try testing.expect(bm.isAllocated(buf, slice2));
bm.free(buf, slice2);
try testing.expect(!bm.isAllocated(buf, slice2));
try testing.expect(bm.isAllocated(buf, slice));
bm.free(buf, slice);
try testing.expect(!bm.isAllocated(buf, slice));
try testing.expect(bm.isAllocated(buf, slice3));
bm.free(buf, slice3);
try testing.expect(!bm.isAllocated(buf, slice3));
// All of our bitmaps should be free.
try testing.expectEqualSlices(
u64,
&@as([3]u64, @splat(~@as(u64, 0))),
bm.bitmap.ptr(buf)[0..3],
);
}
test "BitmapAllocator alloc and free two 1.5 bitmaps offset 0.75" {
const Alloc = BitmapAllocator(1);
// Capacity such that we'll have 4 bitmaps.
const cap = Alloc.bitmap_bit_size * 4;
const testing = std.testing;
const alloc = testing.allocator;
const layout = Alloc.layout(cap);
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
defer alloc.free(buf);
var bm = Alloc.init(.init(buf), layout);
// First allocate a 0.75 bitmap
const slice = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 4);
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 4, slice.len);
@memset(slice, 0x11);
try testing.expectEqualSlices(
u8,
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x11)),
slice,
);
// Then two 1.5 bitmaps
const slice2 = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 2);
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 2, slice2.len);
@memset(slice2, 0x22);
try testing.expectEqualSlices(
u8,
&@as([3 * Alloc.bitmap_bit_size / 2]u8, @splat(0x22)),
slice2,
);
try testing.expectEqualSlices(
u8,
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x11)),
slice,
);
const slice3 = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 2);
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 2, slice3.len);
@memset(slice3, 0x33);
try testing.expectEqualSlices(
u8,
&@as([3 * Alloc.bitmap_bit_size / 2]u8, @splat(0x33)),
slice3,
);
try testing.expectEqualSlices(
u8,
&@as([3 * Alloc.bitmap_bit_size / 2]u8, @splat(0x22)),
slice2,
);
try testing.expectEqualSlices(
u8,
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x11)),
slice,
);
// Free them
try testing.expect(bm.isAllocated(buf, slice2));
bm.free(buf, slice2);
try testing.expect(!bm.isAllocated(buf, slice2));
try testing.expect(bm.isAllocated(buf, slice));
bm.free(buf, slice);
try testing.expect(!bm.isAllocated(buf, slice));
try testing.expect(bm.isAllocated(buf, slice3));
bm.free(buf, slice3);
try testing.expect(!bm.isAllocated(buf, slice3));
// All of our bitmaps should be free.
try testing.expectEqualSlices(
u64,
&@as([4]u64, @splat(~@as(u64, 0))),
bm.bitmap.ptr(buf)[0..4],
);
}

View File

@ -185,6 +185,25 @@ pub const PageEntry = struct {
other.uri.offset.ptr(other_base)[0..other.uri.len],
);
}
/// Free the memory for this entry from its page.
pub fn free(
self: *const PageEntry,
page: *Page,
) void {
const alloc = &page.string_alloc;
switch (self.id) {
.implicit => {},
.explicit => |v| alloc.free(
page.memory,
v.offset.ptr(page.memory)[0..v.len],
),
}
alloc.free(
page.memory,
self.uri.offset.ptr(page.memory)[0..self.uri.len],
);
}
};
/// The set of hyperlinks. This is ref-counted so that a set of cells
@ -215,19 +234,7 @@ pub const Set = RefCountedSet(
}
pub fn deleted(self: *const @This(), link: PageEntry) void {
const page = self.page.?;
const alloc = &page.string_alloc;
switch (link.id) {
.implicit => {},
.explicit => |v| alloc.free(
page.memory,
v.offset.ptr(page.memory)[0..v.len],
),
}
alloc.free(
page.memory,
link.uri.offset.ptr(page.memory)[0..link.uri.len],
);
link.free(self.page.?);
}
},
);

View File

@ -21,14 +21,14 @@ pub const Parser = struct {
arena: ArenaAllocator,
/// This is the list of KV pairs that we're building up.
kv: KV = .{},
kv: KV,
/// This is used as a buffer to store the key/value of a KV pair. The value
/// of a KV pair is at most a 32-bit integer which at most is 10 characters
/// (4294967295), plus one character for the sign bit on signed ints.
kv_temp: [11]u8 = undefined,
kv_temp_len: u4 = 0,
kv_current: u8 = 0, // Current kv key
kv_temp: [11]u8,
kv_temp_len: u4,
kv_current: u8, // Current kv key
/// This is the list we use to collect the bytes from the data payload.
/// The Kitty Graphics protocol specification seems to imply that the
@ -38,7 +38,7 @@ pub const Parser = struct {
data: std.ArrayList(u8),
/// Internal state for parsing.
state: State = .control_key,
state: State,
const State = enum {
/// Parsing k/v pairs. The "ignore" variants are in that state
@ -57,10 +57,22 @@ pub const Parser = struct {
pub fn init(alloc: Allocator) Parser {
var arena = ArenaAllocator.init(alloc);
errdefer arena.deinit();
return .{
var result: Parser = .{
.arena = arena,
.data = std.ArrayList(u8).init(alloc),
.kv = .{},
.kv_temp_len = 0,
.kv_current = 0,
.state = .control_key,
.kv_temp = undefined,
};
if (std.valgrind.runningOnValgrind() > 0) {
// Initialize our undefined fields so Valgrind can catch it.
// https://github.com/ziglang/zig/issues/19148
result.kv_temp = undefined;
}
return result;
}
pub fn deinit(self: *Parser) void {

View File

@ -16,6 +16,10 @@ const kitty = @import("kitty.zig");
const log = std.log.scoped(.osc);
pub const Command = union(enum) {
/// This generally shouldn't ever be set except as an initial zero value.
/// Ignore it.
invalid,
/// Set the window title of the terminal
///
/// If title mode 0 is set text is expect to be hex encoded (i.e. utf-8
@ -282,23 +286,23 @@ pub const Parser = struct {
/// Optional allocator used to accept data longer than MAX_BUF.
/// This only applies to some commands (e.g. OSC 52) that can
/// reasonably exceed MAX_BUF.
alloc: ?Allocator = null,
alloc: ?Allocator,
/// Current state of the parser.
state: State = .empty,
state: State,
/// Current command of the parser, this accumulates.
command: Command = undefined,
command: Command,
/// Buffer that stores the input we see for a single OSC command.
/// Slices in Command are offsets into this buffer.
buf: [MAX_BUF]u8 = undefined,
buf_start: usize = 0,
buf_idx: usize = 0,
buf_dynamic: ?*std.ArrayListUnmanaged(u8) = null,
buf: [MAX_BUF]u8,
buf_start: usize,
buf_idx: usize,
buf_dynamic: ?*std.ArrayListUnmanaged(u8),
/// True when a command is complete/valid to return.
complete: bool = false,
complete: bool,
/// Temporary state that is dependent on the current state.
temp_state: union {
@ -310,7 +314,7 @@ pub const Parser = struct {
/// Temporary state for key/value pairs
key: []const u8,
} = undefined,
},
// Maximum length of a single OSC command. This is the full OSC command
// sequence length (excluding ESC ]). This is arbitrary, I couldn't find
@ -429,6 +433,37 @@ pub const Parser = struct {
conemu_progress_value,
};
pub fn init() Parser {
var result: Parser = .{
.alloc = null,
.state = .empty,
.command = .invalid,
.buf_start = 0,
.buf_idx = 0,
.buf_dynamic = null,
.complete = false,
// Keeping all our undefined values together so we can
// visually easily duplicate them in the Valgrind check below.
.buf = undefined,
.temp_state = undefined,
};
if (std.valgrind.runningOnValgrind() > 0) {
// Initialize our undefined fields so Valgrind can catch it.
// https://github.com/ziglang/zig/issues/19148
result.buf = undefined;
result.temp_state = undefined;
}
return result;
}
pub fn initAlloc(alloc: Allocator) Parser {
var result: Parser = .init();
result.alloc = alloc;
return result;
}
/// This must be called to clean up any allocated memory.
pub fn deinit(self: *Parser) void {
self.reset();
@ -446,9 +481,17 @@ pub const Parser = struct {
return;
}
// Some commands have their own memory management we need to clear.
switch (self.command) {
.kitty_color_protocol => |*v| v.list.deinit(),
.color_operation => |*v| v.operations.deinit(self.alloc.?),
else => {},
}
self.state = .empty;
self.buf_start = 0;
self.buf_idx = 0;
self.command = .invalid;
self.complete = false;
if (self.buf_dynamic) |ptr| {
const alloc = self.alloc.?;
@ -456,22 +499,6 @@ pub const Parser = struct {
alloc.destroy(ptr);
self.buf_dynamic = null;
}
// Some commands have their own memory management we need to clear.
// After cleaning up these command, we reset the command to
// some nonsense (but valid) command so we don't double free.
const default: Command = .{ .hyperlink_end = {} };
switch (self.command) {
.kitty_color_protocol => |*v| {
v.list.deinit();
self.command = default;
},
.color_operation => |*v| {
v.operations.deinit(self.alloc.?);
self.command = default;
},
else => {},
}
}
/// Consume the next character c and advance the parser state.
@ -1590,7 +1617,7 @@ pub const Parser = struct {
test "OSC: change_window_title" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
p.next('0');
p.next(';');
p.next('a');
@ -1603,7 +1630,7 @@ test "OSC: change_window_title" {
test "OSC: change_window_title with 2" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
p.next('2');
p.next(';');
p.next('a');
@ -1616,7 +1643,7 @@ test "OSC: change_window_title with 2" {
test "OSC: change_window_title with utf8" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
p.next('2');
p.next(';');
// '—' EM DASH U+2014 (E2 80 94)
@ -1638,7 +1665,7 @@ test "OSC: change_window_title with utf8" {
test "OSC: change_window_title empty" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
p.next('2');
p.next(';');
const cmd = p.end(null).?;
@ -1649,7 +1676,7 @@ test "OSC: change_window_title empty" {
test "OSC: change_window_icon" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
p.next('1');
p.next(';');
p.next('a');
@ -1662,7 +1689,7 @@ test "OSC: change_window_icon" {
test "OSC: prompt_start" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "133;A";
for (input) |ch| p.next(ch);
@ -1676,7 +1703,7 @@ test "OSC: prompt_start" {
test "OSC: prompt_start with single option" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "133;A;aid=14";
for (input) |ch| p.next(ch);
@ -1689,7 +1716,7 @@ test "OSC: prompt_start with single option" {
test "OSC: prompt_start with redraw disabled" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "133;A;redraw=0";
for (input) |ch| p.next(ch);
@ -1702,7 +1729,7 @@ test "OSC: prompt_start with redraw disabled" {
test "OSC: prompt_start with redraw invalid value" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "133;A;redraw=42";
for (input) |ch| p.next(ch);
@ -1716,7 +1743,7 @@ test "OSC: prompt_start with redraw invalid value" {
test "OSC: prompt_start with continuation" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "133;A;k=c";
for (input) |ch| p.next(ch);
@ -1729,7 +1756,7 @@ test "OSC: prompt_start with continuation" {
test "OSC: prompt_start with secondary" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "133;A;k=s";
for (input) |ch| p.next(ch);
@ -1742,7 +1769,7 @@ test "OSC: prompt_start with secondary" {
test "OSC: end_of_command no exit code" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "133;D";
for (input) |ch| p.next(ch);
@ -1754,7 +1781,7 @@ test "OSC: end_of_command no exit code" {
test "OSC: end_of_command with exit code" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "133;D;25";
for (input) |ch| p.next(ch);
@ -1767,7 +1794,7 @@ test "OSC: end_of_command with exit code" {
test "OSC: prompt_end" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "133;B";
for (input) |ch| p.next(ch);
@ -1779,7 +1806,7 @@ test "OSC: prompt_end" {
test "OSC: end_of_input" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "133;C";
for (input) |ch| p.next(ch);
@ -1791,7 +1818,7 @@ test "OSC: end_of_input" {
test "OSC: OSC110: reset foreground color" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "110";
@ -1817,7 +1844,7 @@ test "OSC: OSC110: reset foreground color" {
test "OSC: OSC111: reset background color" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "111";
@ -1843,7 +1870,7 @@ test "OSC: OSC111: reset background color" {
test "OSC: OSC112: reset cursor color" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "112";
@ -1869,7 +1896,7 @@ test "OSC: OSC112: reset cursor color" {
test "OSC: OSC112: reset cursor color with semicolon" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "112;";
@ -1896,7 +1923,7 @@ test "OSC: OSC112: reset cursor color with semicolon" {
test "OSC: get/set clipboard" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "52;s;?";
for (input) |ch| p.next(ch);
@ -1910,7 +1937,7 @@ test "OSC: get/set clipboard" {
test "OSC: get/set clipboard (optional parameter)" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "52;;?";
for (input) |ch| p.next(ch);
@ -1924,7 +1951,7 @@ test "OSC: get/set clipboard (optional parameter)" {
test "OSC: get/set clipboard with allocator" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "52;s;?";
@ -1939,7 +1966,7 @@ test "OSC: get/set clipboard with allocator" {
test "OSC: clear clipboard" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .init();
defer p.deinit();
const input = "52;;";
@ -1954,7 +1981,7 @@ test "OSC: clear clipboard" {
test "OSC: report pwd" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "7;file:///tmp/example";
for (input) |ch| p.next(ch);
@ -1967,7 +1994,7 @@ test "OSC: report pwd" {
test "OSC: report pwd empty" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "7;";
for (input) |ch| p.next(ch);
@ -1979,7 +2006,7 @@ test "OSC: report pwd empty" {
test "OSC: pointer cursor" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "22;pointer";
for (input) |ch| p.next(ch);
@ -1992,7 +2019,7 @@ test "OSC: pointer cursor" {
test "OSC: longer than buffer" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "0;" ++ "a" ** (Parser.MAX_BUF + 2);
for (input) |ch| p.next(ch);
@ -2004,7 +2031,7 @@ test "OSC: longer than buffer" {
test "OSC: OSC10: report foreground color" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "10;?";
@ -2032,7 +2059,7 @@ test "OSC: OSC10: report foreground color" {
test "OSC: OSC10: set foreground color" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "10;rgbi:0.0/0.5/1.0";
@ -2062,7 +2089,7 @@ test "OSC: OSC10: set foreground color" {
test "OSC: OSC11: report background color" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "11;?";
@ -2090,7 +2117,7 @@ test "OSC: OSC11: report background color" {
test "OSC: OSC11: set background color" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "11;rgb:f/ff/ffff";
@ -2120,7 +2147,7 @@ test "OSC: OSC11: set background color" {
test "OSC: OSC12: report cursor color" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "12;?";
@ -2148,7 +2175,7 @@ test "OSC: OSC12: report cursor color" {
test "OSC: OSC12: set cursor color" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "12;rgb:f/ff/ffff";
@ -2178,7 +2205,7 @@ test "OSC: OSC12: set cursor color" {
test "OSC: OSC4: get palette color 1" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "4;1;?";
@ -2204,7 +2231,7 @@ test "OSC: OSC4: get palette color 1" {
test "OSC: OSC4: get palette color 2" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "4;1;?;2;?";
@ -2238,7 +2265,7 @@ test "OSC: OSC4: get palette color 2" {
test "OSC: OSC4: set palette color 1" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "4;17;rgb:aa/bb/cc";
@ -2267,7 +2294,7 @@ test "OSC: OSC4: set palette color 1" {
test "OSC: OSC4: set palette color 2" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "4;17;rgb:aa/bb/cc;1;rgb:00/11/22";
@ -2308,7 +2335,7 @@ test "OSC: OSC4: set palette color 2" {
test "OSC: OSC4: get with invalid index 1" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "4;1111;?;1;?";
@ -2333,7 +2360,7 @@ test "OSC: OSC4: get with invalid index 1" {
test "OSC: OSC4: get with invalid index 2" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "4;5;?;1111;?;1;?";
@ -2367,7 +2394,7 @@ test "OSC: OSC4: get with invalid index 2" {
test "OSC: OSC4: multiple get 8a" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?";
@ -2449,7 +2476,7 @@ test "OSC: OSC4: multiple get 8a" {
test "OSC: OSC4: multiple get 8b" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "4;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?";
@ -2530,7 +2557,7 @@ test "OSC: OSC4: multiple get 8b" {
test "OSC: OSC4: set with invalid index" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "4;256;#ffffff;1;#aabbcc";
@ -2559,7 +2586,7 @@ test "OSC: OSC4: set with invalid index" {
test "OSC: OSC4: mix get/set palette color" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "4;17;rgb:aa/bb/cc;254;?";
@ -2596,7 +2623,7 @@ test "OSC: OSC4: mix get/set palette color" {
test "OSC: OSC4: incomplete color/spec 1" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "4;17";
@ -2613,7 +2640,7 @@ test "OSC: OSC4: incomplete color/spec 1" {
test "OSC: OSC4: incomplete color/spec 2" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "4;17;?;42";
@ -2638,7 +2665,7 @@ test "OSC: OSC4: incomplete color/spec 2" {
test "OSC: OSC104: reset palette color 1" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "104;17";
@ -2663,7 +2690,7 @@ test "OSC: OSC104: reset palette color 1" {
test "OSC: OSC104: reset palette color 2" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "104;17;111";
@ -2696,7 +2723,7 @@ test "OSC: OSC104: reset palette color 2" {
test "OSC: OSC104: invalid palette index" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "104;ffff;111";
@ -2721,7 +2748,7 @@ test "OSC: OSC104: invalid palette index" {
test "OSC: OSC104: empty palette index" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "104;;111";
@ -2746,7 +2773,7 @@ test "OSC: OSC104: empty palette index" {
test "OSC: conemu sleep" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;1;420";
for (input) |ch| p.next(ch);
@ -2760,7 +2787,7 @@ test "OSC: conemu sleep" {
test "OSC: conemu sleep with no value default to 100ms" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;1;";
for (input) |ch| p.next(ch);
@ -2774,7 +2801,7 @@ test "OSC: conemu sleep with no value default to 100ms" {
test "OSC: conemu sleep cannot exceed 10000ms" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;1;12345";
for (input) |ch| p.next(ch);
@ -2788,7 +2815,7 @@ test "OSC: conemu sleep cannot exceed 10000ms" {
test "OSC: conemu sleep invalid input" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;1;foo";
for (input) |ch| p.next(ch);
@ -2802,7 +2829,7 @@ test "OSC: conemu sleep invalid input" {
test "OSC: show desktop notification" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;Hello world";
for (input) |ch| p.next(ch);
@ -2816,7 +2843,7 @@ test "OSC: show desktop notification" {
test "OSC: show desktop notification with title" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "777;notify;Title;Body";
for (input) |ch| p.next(ch);
@ -2830,7 +2857,7 @@ test "OSC: show desktop notification with title" {
test "OSC: conemu message box" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;2;hello world";
for (input) |ch| p.next(ch);
@ -2843,7 +2870,7 @@ test "OSC: conemu message box" {
test "OSC: conemu message box invalid input" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;2";
for (input) |ch| p.next(ch);
@ -2855,7 +2882,7 @@ test "OSC: conemu message box invalid input" {
test "OSC: conemu message box empty message" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;2;";
for (input) |ch| p.next(ch);
@ -2868,7 +2895,7 @@ test "OSC: conemu message box empty message" {
test "OSC: conemu message box spaces only message" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;2; ";
for (input) |ch| p.next(ch);
@ -2881,7 +2908,7 @@ test "OSC: conemu message box spaces only message" {
test "OSC: conemu change tab title" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;3;foo bar";
for (input) |ch| p.next(ch);
@ -2894,7 +2921,7 @@ test "OSC: conemu change tab title" {
test "OSC: conemu change tab reset title" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;3;";
for (input) |ch| p.next(ch);
@ -2908,7 +2935,7 @@ test "OSC: conemu change tab reset title" {
test "OSC: conemu change tab spaces only title" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;3; ";
for (input) |ch| p.next(ch);
@ -2922,7 +2949,7 @@ test "OSC: conemu change tab spaces only title" {
test "OSC: conemu change tab invalid input" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;3";
for (input) |ch| p.next(ch);
@ -2934,7 +2961,7 @@ test "OSC: conemu change tab invalid input" {
test "OSC: OSC9 progress set" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;4;1;100";
for (input) |ch| p.next(ch);
@ -2948,7 +2975,7 @@ test "OSC: OSC9 progress set" {
test "OSC: OSC9 progress set overflow" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;4;1;900";
for (input) |ch| p.next(ch);
@ -2962,7 +2989,7 @@ test "OSC: OSC9 progress set overflow" {
test "OSC: OSC9 progress set single digit" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;4;1;9";
for (input) |ch| p.next(ch);
@ -2976,7 +3003,7 @@ test "OSC: OSC9 progress set single digit" {
test "OSC: OSC9 progress set double digit" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;4;1;94";
for (input) |ch| p.next(ch);
@ -2990,7 +3017,7 @@ test "OSC: OSC9 progress set double digit" {
test "OSC: OSC9 progress set extra semicolon ignored" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;4;1;100";
for (input) |ch| p.next(ch);
@ -3004,7 +3031,7 @@ test "OSC: OSC9 progress set extra semicolon ignored" {
test "OSC: OSC9 progress remove with no progress" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;4;0;";
for (input) |ch| p.next(ch);
@ -3018,7 +3045,7 @@ test "OSC: OSC9 progress remove with no progress" {
test "OSC: OSC9 progress remove with double semicolon" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;4;0;;";
for (input) |ch| p.next(ch);
@ -3032,7 +3059,7 @@ test "OSC: OSC9 progress remove with double semicolon" {
test "OSC: OSC9 progress remove ignores progress" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;4;0;100";
for (input) |ch| p.next(ch);
@ -3046,7 +3073,7 @@ test "OSC: OSC9 progress remove ignores progress" {
test "OSC: OSC9 progress remove extra semicolon" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;4;0;100;";
for (input) |ch| p.next(ch);
@ -3059,7 +3086,7 @@ test "OSC: OSC9 progress remove extra semicolon" {
test "OSC: OSC9 progress error" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;4;2";
for (input) |ch| p.next(ch);
@ -3073,7 +3100,7 @@ test "OSC: OSC9 progress error" {
test "OSC: OSC9 progress error with progress" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;4;2;100";
for (input) |ch| p.next(ch);
@ -3087,7 +3114,7 @@ test "OSC: OSC9 progress error with progress" {
test "OSC: OSC9 progress pause" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;4;4";
for (input) |ch| p.next(ch);
@ -3101,7 +3128,7 @@ test "OSC: OSC9 progress pause" {
test "OSC: OSC9 progress pause with progress" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;4;4;100";
for (input) |ch| p.next(ch);
@ -3115,7 +3142,7 @@ test "OSC: OSC9 progress pause with progress" {
test "OSC: OSC9 conemu wait input" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;5";
for (input) |ch| p.next(ch);
@ -3127,7 +3154,7 @@ test "OSC: OSC9 conemu wait input" {
test "OSC: OSC9 conemu wait ignores trailing characters" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "9;5;foo";
for (input) |ch| p.next(ch);
@ -3139,7 +3166,7 @@ test "OSC: OSC9 conemu wait ignores trailing characters" {
test "OSC: empty param" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "4;;";
for (input) |ch| p.next(ch);
@ -3151,7 +3178,7 @@ test "OSC: empty param" {
test "OSC: hyperlink" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "8;;http://example.com";
for (input) |ch| p.next(ch);
@ -3164,7 +3191,7 @@ test "OSC: hyperlink" {
test "OSC: hyperlink with id set" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "8;id=foo;http://example.com";
for (input) |ch| p.next(ch);
@ -3178,7 +3205,7 @@ test "OSC: hyperlink with id set" {
test "OSC: hyperlink with empty id" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "8;id=;http://example.com";
for (input) |ch| p.next(ch);
@ -3192,7 +3219,7 @@ test "OSC: hyperlink with empty id" {
test "OSC: hyperlink with incomplete key" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "8;id;http://example.com";
for (input) |ch| p.next(ch);
@ -3206,7 +3233,7 @@ test "OSC: hyperlink with incomplete key" {
test "OSC: hyperlink with empty key" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "8;=value;http://example.com";
for (input) |ch| p.next(ch);
@ -3220,7 +3247,7 @@ test "OSC: hyperlink with empty key" {
test "OSC: hyperlink with empty key and id" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "8;=value:id=foo;http://example.com";
for (input) |ch| p.next(ch);
@ -3234,7 +3261,7 @@ test "OSC: hyperlink with empty key and id" {
test "OSC: hyperlink with empty uri" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "8;id=foo;";
for (input) |ch| p.next(ch);
@ -3246,7 +3273,7 @@ test "OSC: hyperlink with empty uri" {
test "OSC: hyperlink end" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
const input = "8;;";
for (input) |ch| p.next(ch);
@ -3259,7 +3286,7 @@ test "OSC: kitty color protocol" {
const testing = std.testing;
const Kind = kitty.color.Kind;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0";
@ -3330,7 +3357,7 @@ test "OSC: kitty color protocol" {
test "OSC: kitty color protocol without allocator" {
const testing = std.testing;
var p: Parser = .{};
var p: Parser = .init();
defer p.deinit();
const input = "21;foreground=?";
@ -3341,7 +3368,7 @@ test "OSC: kitty color protocol without allocator" {
test "OSC: kitty color protocol double reset" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0";
@ -3357,7 +3384,7 @@ test "OSC: kitty color protocol double reset" {
test "OSC: kitty color protocol reset after invalid" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0";
@ -3378,7 +3405,7 @@ test "OSC: kitty color protocol reset after invalid" {
test "OSC: kitty color protocol no key" {
const testing = std.testing;
var p: Parser = .{ .alloc = testing.allocator };
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "21;";

View File

@ -34,7 +34,7 @@ const grapheme_chunk_len = 4;
const grapheme_chunk = grapheme_chunk_len * @sizeOf(u21);
const GraphemeAlloc = BitmapAllocator(grapheme_chunk);
const grapheme_count_default = GraphemeAlloc.bitmap_bit_size;
const grapheme_bytes_default = grapheme_count_default * grapheme_chunk;
pub const grapheme_bytes_default = grapheme_count_default * grapheme_chunk;
const GraphemeMap = AutoOffsetHashMap(Offset(Cell), Offset(u21).Slice);
/// The allocator used for shared utf8-encoded strings within a page.
@ -53,7 +53,7 @@ const string_chunk_len = 32;
const string_chunk = string_chunk_len * @sizeOf(u8);
const StringAlloc = BitmapAllocator(string_chunk);
const string_count_default = StringAlloc.bitmap_bit_size;
const string_bytes_default = string_count_default * string_chunk;
pub const string_bytes_default = string_count_default * string_chunk;
/// Default number of hyperlinks we support.
///
@ -346,6 +346,11 @@ pub const Page = struct {
// used for the same reason as styles above.
//
// We don't run integrity checks on Valgrind because its soooooo slow,
// Valgrind is our integrity checker, and we run these during unit
// tests (non-Valgrind) anyways so we're verifying anyways.
if (std.valgrind.runningOnValgrind() > 0) return;
if (build_config.slow_runtime_safety) {
if (self.pause_integrity_checks > 0) return;
}
@ -2038,10 +2043,13 @@ pub const Cell = packed struct(u64) {
/// Helper to make a cell that just has a codepoint.
pub fn init(cp: u21) Cell {
return .{
.content_tag = .codepoint,
.content = .{ .codepoint = cp },
};
// We have to use this bitCast here to ensure that our memory is
// zeroed. Otherwise, the content below will leave some uninitialized
// memory in the packed union. Valgrind verifies this.
var cell: Cell = @bitCast(@as(u64, 0));
cell.content_tag = .codepoint;
cell.content = .{ .codepoint = cp };
return cell;
}
pub fn isZero(self: Cell) bool {
@ -3034,6 +3042,10 @@ test "Page moveCells graphemes" {
}
test "Page verifyIntegrity graphemes good" {
// Too slow, and not really necessary because the integrity tests are
// only run in debug builds and unit tests verify they work well enough.
if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest;
var page = try Page.init(.{
.cols = 10,
.rows = 10,
@ -3055,6 +3067,10 @@ test "Page verifyIntegrity graphemes good" {
}
test "Page verifyIntegrity grapheme row not marked" {
// Too slow, and not really necessary because the integrity tests are
// only run in debug builds and unit tests verify they work well enough.
if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest;
var page = try Page.init(.{
.cols = 10,
.rows = 10,
@ -3082,6 +3098,10 @@ test "Page verifyIntegrity grapheme row not marked" {
}
test "Page verifyIntegrity styles good" {
// Too slow, and not really necessary because the integrity tests are
// only run in debug builds and unit tests verify they work well enough.
if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest;
var page = try Page.init(.{
.cols = 10,
.rows = 10,
@ -3114,6 +3134,10 @@ test "Page verifyIntegrity styles good" {
}
test "Page verifyIntegrity styles ref count mismatch" {
// Too slow, and not really necessary because the integrity tests are
// only run in debug builds and unit tests verify they work well enough.
if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest;
var page = try Page.init(.{
.cols = 10,
.rows = 10,
@ -3152,6 +3176,10 @@ test "Page verifyIntegrity styles ref count mismatch" {
}
test "Page verifyIntegrity zero rows" {
// Too slow, and not really necessary because the integrity tests are
// only run in debug builds and unit tests verify they work well enough.
if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest;
var page = try Page.init(.{
.cols = 10,
.rows = 10,
@ -3166,6 +3194,10 @@ test "Page verifyIntegrity zero rows" {
}
test "Page verifyIntegrity zero cols" {
// Too slow, and not really necessary because the integrity tests are
// only run in debug builds and unit tests verify they work well enough.
if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest;
var page = try Page.init(.{
.cols = 10,
.rows = 10,

View File

@ -454,6 +454,11 @@ const SlidingWindow = struct {
fn assertIntegrity(self: *const SlidingWindow) void {
if (comptime !std.debug.runtime_safety) return;
// We don't run integrity checks on Valgrind because its soooooo slow,
// Valgrind is our integrity checker, and we run these during unit
// tests (non-Valgrind) anyways so we're verifying anyways.
if (std.valgrind.runningOnValgrind() > 0) return;
// Integrity check: verify our data matches our metadata exactly.
var meta_it = self.meta.iterator(.forward);
var data_len: usize = 0;

View File

@ -47,8 +47,16 @@ pub fn Stream(comptime Handler: type) type {
};
handler: Handler,
parser: Parser = .{},
utf8decoder: UTF8Decoder = .{},
parser: Parser,
utf8decoder: UTF8Decoder,
pub fn init(h: Handler) Self {
return .{
.handler = h,
.parser = .init(),
.utf8decoder = .{},
};
}
pub fn deinit(self: *Self) void {
self.parser.deinit();
@ -1600,6 +1608,12 @@ pub fn Stream(comptime Handler: type) type {
.sleep, .show_message_box, .change_conemu_tab_title, .wait_input => {
log.warn("unimplemented OSC callback: {}", .{cmd});
},
.invalid => {
// This is an invalid internal state, not an invalid OSC
// string being parsed. We shouldn't see this.
log.warn("invalid OSC, should never happen", .{});
},
}
// Fall through for when we don't have a handler.
@ -1842,7 +1856,7 @@ test "stream: print" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.next('x');
try testing.expectEqual(@as(u21, 'x'), s.handler.c.?);
}
@ -1856,7 +1870,7 @@ test "simd: print invalid utf-8" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice(&.{0xFF});
try testing.expectEqual(@as(u21, 0xFFFD), s.handler.c.?);
}
@ -1870,7 +1884,7 @@ test "simd: complete incomplete utf-8" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice(&.{0xE0}); // 3 byte
try testing.expect(s.handler.c == null);
try s.nextSlice(&.{0xA0}); // still incomplete
@ -1888,7 +1902,7 @@ test "stream: cursor right (CUF)" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1B[C");
try testing.expectEqual(@as(u16, 1), s.handler.amount);
@ -1913,7 +1927,7 @@ test "stream: dec set mode (SM) and reset mode (RM)" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1B[?6h");
try testing.expectEqual(@as(modes.Mode, .origin), s.handler.mode);
@ -1935,7 +1949,7 @@ test "stream: ansi set mode (SM) and reset mode (RM)" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1B[4h");
try testing.expectEqual(@as(modes.Mode, .insert), s.handler.mode.?);
@ -1957,7 +1971,7 @@ test "stream: ansi set mode (SM) and reset mode (RM) with unknown value" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1B[6h");
try testing.expect(s.handler.mode == null);
@ -1977,7 +1991,7 @@ test "stream: restore mode" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
for ("\x1B[?42r") |c| try s.next(c);
try testing.expect(!s.handler.called);
}
@ -1992,7 +2006,7 @@ test "stream: pop kitty keyboard with no params defaults to 1" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
for ("\x1B[<u") |c| try s.next(c);
try testing.expectEqual(@as(u16, 1), s.handler.n);
}
@ -2007,7 +2021,7 @@ test "stream: DECSCA" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
{
for ("\x1B[\"q") |c| try s.next(c);
try testing.expectEqual(ansi.ProtectedMode.off, s.handler.v.?);
@ -2042,7 +2056,7 @@ test "stream: DECED, DECSED" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
{
for ("\x1B[?J") |c| try s.next(c);
try testing.expectEqual(csi.EraseDisplay.below, s.handler.mode.?);
@ -2118,7 +2132,7 @@ test "stream: DECEL, DECSEL" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
{
for ("\x1B[?K") |c| try s.next(c);
try testing.expectEqual(csi.EraseLine.right, s.handler.mode.?);
@ -2177,7 +2191,7 @@ test "stream: DECSCUSR" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1B[ q");
try testing.expect(s.handler.style.? == .default);
@ -2198,7 +2212,7 @@ test "stream: DECSCUSR without space" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1B[q");
try testing.expect(s.handler.style == null);
@ -2215,7 +2229,7 @@ test "stream: XTSHIFTESCAPE" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1B[>2s");
try testing.expect(s.handler.escape == null);
@ -2245,13 +2259,13 @@ test "stream: change window title with invalid utf-8" {
};
{
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1b]2;abc\x1b\\");
try testing.expect(s.handler.seen);
}
{
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1b]2;abc\xc0\x1b\\");
try testing.expect(!s.handler.seen);
}
@ -2268,7 +2282,7 @@ test "stream: insert characters" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
for ("\x1B[42@") |c| try s.next(c);
try testing.expect(s.handler.called);
@ -2294,7 +2308,7 @@ test "stream: SCOSC" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
for ("\x1B[s") |c| try s.next(c);
try testing.expect(s.handler.called);
}
@ -2309,7 +2323,7 @@ test "stream: SCORC" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
for ("\x1B[u") |c| try s.next(c);
try testing.expect(s.handler.called);
}
@ -2323,7 +2337,7 @@ test "stream: too many csi params" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1B[1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1C");
}
@ -2335,7 +2349,7 @@ test "stream: csi param too long" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1B[1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111C");
}
@ -2348,7 +2362,7 @@ test "stream: send report with CSI t" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1b[14t");
try testing.expectEqual(csi.SizeReportStyle.csi_14_t, s.handler.style);
@ -2372,7 +2386,7 @@ test "stream: invalid CSI t" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1b[19t");
try testing.expectEqual(null, s.handler.style);
@ -2387,7 +2401,7 @@ test "stream: CSI t push title" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1b[22;0t");
try testing.expectEqual(csi.TitlePushPop{
@ -2405,7 +2419,7 @@ test "stream: CSI t push title with explicit window" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1b[22;2t");
try testing.expectEqual(csi.TitlePushPop{
@ -2423,7 +2437,7 @@ test "stream: CSI t push title with explicit icon" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1b[22;1t");
try testing.expectEqual(null, s.handler.op);
@ -2438,7 +2452,7 @@ test "stream: CSI t push title with index" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1b[22;0;5t");
try testing.expectEqual(csi.TitlePushPop{
@ -2456,7 +2470,7 @@ test "stream: CSI t pop title" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1b[23;0t");
try testing.expectEqual(csi.TitlePushPop{
@ -2474,7 +2488,7 @@ test "stream: CSI t pop title with explicit window" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1b[23;2t");
try testing.expectEqual(csi.TitlePushPop{
@ -2492,7 +2506,7 @@ test "stream: CSI t pop title with explicit icon" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1b[23;1t");
try testing.expectEqual(null, s.handler.op);
@ -2507,7 +2521,7 @@ test "stream: CSI t pop title with index" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1b[23;0;5t");
try testing.expectEqual(csi.TitlePushPop{
@ -2525,7 +2539,7 @@ test "stream CSI W clear tab stops" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1b[2W");
try testing.expectEqual(csi.TabClear.current, s.handler.op.?);
@ -2543,7 +2557,7 @@ test "stream CSI W tab set" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1b[W");
try testing.expect(s.handler.called);
@ -2570,7 +2584,7 @@ test "stream CSI ? W reset tab stops" {
}
};
var s: Stream(H) = .{ .handler = .{} };
var s: Stream(H) = .init(.{});
try s.nextSlice("\x1b[?2W");
try testing.expect(!s.handler.reset);

View File

@ -313,15 +313,12 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
.size = opts.size,
.backend = backend,
.mailbox = opts.mailbox,
.terminal_stream = .{
.handler = handler,
.parser = .{
.osc_parser = .{
// Populate the OSC parser allocator (optional) because
// we want to support large OSC payloads such as OSC 52.
.alloc = alloc,
},
},
.terminal_stream = stream: {
var s: terminalpkg.Stream(StreamHandler) = .init(handler);
// Populate the OSC parser allocator (optional) because
// we want to support large OSC payloads such as OSC 52.
s.parser.osc_parser.alloc = alloc;
break :stream s;
},
.thread_enter_state = thread_enter_state,
};

View File

@ -670,3 +670,27 @@ fn setupZsh(
);
try env.put("ZDOTDIR", integ_dir);
}
test "zsh" {
const testing = std.testing;
var env = EnvMap.init(testing.allocator);
defer env.deinit();
try setupZsh(".", &env);
try testing.expectEqualStrings("./shell-integration/zsh", env.get("ZDOTDIR").?);
try testing.expect(env.get("GHOSTTY_ZSH_ZDOTDIR") == null);
}
test "zsh: ZDOTDIR" {
const testing = std.testing;
var env = EnvMap.init(testing.allocator);
defer env.deinit();
try env.put("ZDOTDIR", "$HOME/.config/zsh");
try setupZsh(".", &env);
try testing.expectEqualStrings("./shell-integration/zsh", env.get("ZDOTDIR").?);
try testing.expectEqualStrings("$HOME/.config/zsh", env.get("GHOSTTY_ZSH_ZDOTDIR").?);
}

View File

@ -574,6 +574,18 @@
...
}
{
pango language
Memcheck:Leak
match-leak-kinds: possible
fun:calloc
fun:g_malloc0
fun:pango_language_from_string
fun:pango_language_get_default
fun:gtk_get_locale_direction
fun:gtk_init_check
...
}
{
Adwaita Stylesheet Load
Memcheck:Leak