summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/release-pr.yml18
-rw-r--r--.github/workflows/release-tag.yml15
-rw-r--r--.github/workflows/release-tip.yml30
-rw-r--r--.github/workflows/test.yml105
-rw-r--r--CONTRIBUTING.md56
-rw-r--r--build.zig58
-rw-r--r--build.zig.zon4
-rw-r--r--build.zig.zon.json6
-rw-r--r--build.zig.zon.nix6
-rw-r--r--build.zig.zon.txt2
-rw-r--r--flatpak/com.mitchellh.ghostty-debug.yml4
-rw-r--r--flatpak/com.mitchellh.ghostty.yml4
-rw-r--r--flatpak/dependencies.yml18
-rw-r--r--flatpak/zig-packages.json6
-rw-r--r--macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved4
-rw-r--r--macos/Sources/App/macOS/AppDelegate.swift35
-rw-r--r--macos/Sources/Features/Terminal/TerminalController.swift8
-rw-r--r--macos/Sources/Ghostty/Ghostty.Config.swift36
-rw-r--r--macos/Sources/Ghostty/Package.swift1
-rw-r--r--macos/Sources/Ghostty/SurfaceView_AppKit.swift2
-rw-r--r--pkg/wuffs/build.zig12
-rw-r--r--src/Surface.zig245
-rw-r--r--src/apprt/gtk-ng/class.zig106
-rw-r--r--src/apprt/gtk-ng/class/application.zig2
-rw-r--r--src/apprt/gtk-ng/class/imgui_widget.zig104
-rw-r--r--src/apprt/gtk-ng/class/inspector_widget.zig53
-rw-r--r--src/apprt/gtk-ng/class/surface.zig37
-rw-r--r--src/apprt/gtk-ng/class/window.zig52
-rw-r--r--src/apprt/gtk-ng/ext/actions.zig7
-rw-r--r--src/apprt/gtk-ng/ipc/DBus.zig189
-rw-r--r--src/apprt/gtk-ng/ipc/new_window.zig174
-rw-r--r--src/apprt/gtk-ng/ui/1.5/inspector-widget.blp10
-rw-r--r--src/benchmark/TerminalParser.zig2
-rw-r--r--src/benchmark/TerminalStream.zig2
-rw-r--r--src/build/Config.zig23
-rw-r--r--src/cli/list_themes.zig71
-rw-r--r--src/config.zig1
-rw-r--r--src/config/Config.zig229
-rw-r--r--src/config/formatter.zig2
-rw-r--r--src/font/Collection.zig3
-rw-r--r--src/font/DeferredFace.zig1
-rw-r--r--src/font/discovery.zig5
-rw-r--r--src/font/face/coretext.zig32
-rw-r--r--src/font/sprite/Face.zig3
-rw-r--r--src/input/key.zig78
-rw-r--r--src/inspector/Inspector.zig11
-rw-r--r--src/shell-integration/zsh/.zshenv10
-rw-r--r--src/synthetic/Osc.zig4
-rw-r--r--src/terminal/PageList.zig432
-rw-r--r--src/terminal/Parser.zig38
-rw-r--r--src/terminal/Screen.zig5
-rw-r--r--src/terminal/Tabstops.zig1
-rw-r--r--src/terminal/Terminal.zig13
-rw-r--r--src/terminal/bitmap_allocator.zig584
-rw-r--r--src/terminal/hyperlink.zig33
-rw-r--r--src/terminal/kitty/graphics_command.zig24
-rw-r--r--src/terminal/osc.zig261
-rw-r--r--src/terminal/page.zig44
-rw-r--r--src/terminal/search.zig5
-rw-r--r--src/terminal/stream.zig88
-rw-r--r--src/termio/Termio.zig15
-rw-r--r--src/termio/shell_integration.zig24
-rw-r--r--valgrind.supp12
63 files changed, 2532 insertions, 933 deletions
diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml
index 6fa813c31..8c42a2a55 100644
--- a/.github/workflows/release-pr.yml
+++ b/.github/workflows/release-pr.yml
@@ -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.
diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml
index 9c92d45a9..f7fb72f65 100644
--- a/.github/workflows/release-tag.yml
+++ b/.github/workflows/release-tag.yml
@@ -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
diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml
index 58e114f1b..bae096054 100644
--- a/.github/workflows/release-tip.yml
+++ b/.github/workflows/release-tip.yml
@@ -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
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index c00816b38..8910d8c07 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -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
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 18cd7ca90..0e988704b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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
diff --git a/build.zig b/build.zig
index 4acca53cc..38cfd0e56 100644
--- a/build.zig
+++ b/build.zig
@@ -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"
diff --git a/build.zig.zon b/build.zig.zon
index 60b96ee18..2a44c0220 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -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,
},
},
diff --git a/build.zig.zon.json b/build.zig.zon.json
index 1e31b72a2..8b35e80e9 100644
--- a/build.zig.zon.json
+++ b/build.zig.zon.json
@@ -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",
diff --git a/build.zig.zon.nix b/build.zig.zon.nix
index 0cb79329b..50f2438c1 100644
--- a/build.zig.zon.nix
+++ b/build.zig.zon.nix
@@ -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=";
};
}
{
diff --git a/build.zig.zon.txt b/build.zig.zon.txt
index e2c578ea5..4a22d98df 100644
--- a/build.zig.zon.txt
+++ b/build.zig.zon.txt
@@ -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
diff --git a/flatpak/com.mitchellh.ghostty-debug.yml b/flatpak/com.mitchellh.ghostty-debug.yml
index fe4722ef5..51c41931b 100644
--- a/flatpak/com.mitchellh.ghostty-debug.yml
+++ b/flatpak/com.mitchellh.ghostty-debug.yml
@@ -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
diff --git a/flatpak/com.mitchellh.ghostty.yml b/flatpak/com.mitchellh.ghostty.yml
index 1b119c11b..f5af4235d 100644
--- a/flatpak/com.mitchellh.ghostty.yml
+++ b/flatpak/com.mitchellh.ghostty.yml
@@ -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
diff --git a/flatpak/dependencies.yml b/flatpak/dependencies.yml
index efb5851e9..0ff0784c2 100644
--- a/flatpak/dependencies.yml
+++ b/flatpak/dependencies.yml
@@ -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:
diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json
index cdf566fda..02d5ea1c7 100644
--- a/flatpak/zig-packages.json
+++ b/flatpak/zig-packages.json
@@ -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",
diff --git a/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 5ace476e0..7ecedbc14 100644
--- a/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -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"
}
}
],
diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift
index 5940547b5..c00025bf5 100644
--- a/macos/Sources/App/macOS/AppDelegate.swift
+++ b/macos/Sources/App/macOS/AppDelegate.swift
@@ -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?) {
diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift
index c5e1c413f..ec56fb934 100644
--- a/macos/Sources/Features/Terminal/TerminalController.swift
+++ b/macos/Sources/Features/Terminal/TerminalController.swift
@@ -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
diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift
index 241c10632..6992f59f6 100644
--- a/macos/Sources/Ghostty/Ghostty.Config.swift
+++ b/macos/Sources/Ghostty/Ghostty.Config.swift
@@ -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
diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift
index 9b05934df..73487f1bd 100644
--- a/macos/Sources/Ghostty/Package.swift
+++ b/macos/Sources/Ghostty/Package.swift
@@ -280,6 +280,7 @@ extension Ghostty {
case paper
case retro
case xray
+ case custom
case customStyle = "custom-style"
}
diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift
index 4b47949e2..97637e737 100644
--- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift
+++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift
@@ -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: "")
diff --git a/pkg/wuffs/build.zig b/pkg/wuffs/build.zig
index b151892ed..57d89e6b6 100644
--- a/pkg/wuffs/build.zig
+++ b/pkg/wuffs/build.zig
@@ -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| {
diff --git a/src/Surface.zig b/src/Surface.zig
index 866505717..2ab265cda 100644
--- a/src/Surface.zig
+++ b/src/Surface.zig
@@ -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;
diff --git a/src/apprt/gtk-ng/class.zig b/src/apprt/gtk-ng/class.zig
index 68879d19c..4b46f8365 100644
--- a/src/apprt/gtk-ng/class.zig
+++ b/src/apprt/gtk-ng/class.zig
@@ -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 object’s
+ /// instance structure) contains in its first bytes a pointer to the
+ /// object’s 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
+ /// structure’s fields in memory. This means that the first field of
+ /// an instance of an object B is A’s first field which in turn is
+ /// GTypeInstance‘s first field which in turn is g_class, a pointer
+ /// to B’s 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.
diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig
index bfecab3e1..29a124798 100644
--- a/src/apprt/gtk-ng/class/application.zig
+++ b/src/apprt/gtk-ng/class/application.zig
@@ -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;
};
diff --git a/src/apprt/gtk-ng/class/imgui_widget.zig b/src/apprt/gtk-ng/class/imgui_widget.zig
index 1522f2bc1..854dec20b 100644
--- a/src/apprt/gtk-ng/class/imgui_widget.zig
+++ b/src/apprt/gtk-ng/class/imgui_widget.zig
@@ -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,
- );
- };
-
- /// 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 signals = struct {};
+
+ 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 {
@@ -114,6 +96,25 @@ pub const ImguiWidget = extern struct {
}
//---------------------------------------------------------------
+ // 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
/// Set our imgui context to be current, or return an error. This must be
@@ -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);
diff --git a/src/apprt/gtk-ng/class/inspector_widget.zig b/src/apprt/gtk-ng/class/inspector_widget.zig
index f71970a88..4321dcd57 100644
--- a/src/apprt/gtk-ng/class/inspector_widget.zig
+++ b/src/apprt/gtk-ng/class/inspector_widget.zig
@@ -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);
}
diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig
index 580436bd3..2debff93b 100644
--- a/src/apprt/gtk-ng/class/surface.zig
+++ b/src/apprt/gtk-ng/class/surface.zig
@@ -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(
diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig
index 82d961e17..91e65731b 100644
--- a/src/apprt/gtk-ng/class/window.zig
+++ b/src/apprt/gtk-ng/class/window.zig
@@ -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();
diff --git a/src/apprt/gtk-ng/ext/actions.zig b/src/apprt/gtk-ng/ext/actions.zig
index 9f724c850..8499e7de8 100644
--- a/src/apprt/gtk-ng/ext/actions.zig
+++ b/src/apprt/gtk-ng/ext/actions.zig
@@ -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) {}
}
diff --git a/src/apprt/gtk-ng/ipc/DBus.zig b/src/apprt/gtk-ng/ipc/DBus.zig
new file mode 100644
index 000000000..d14d86ce6
--- /dev/null
+++ b/src/apprt/gtk-ng/ipc/DBus.zig
@@ -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();
+}
diff --git a/src/apprt/gtk-ng/ipc/new_window.zig b/src/apprt/gtk-ng/ipc/new_window.zig
index f67498ae1..55e2e0e01 100644
--- a/src/apprt/gtk-ng/ipc/new_window.zig
+++ b/src/apprt/gtk-ng/ipc/new_window.zig
@@ -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();
-
- // 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 },
- };
- defer {
- switch (target) {
- .class => alloc.free(object_path),
- .detect => {},
+ var dbus = try DBus.init(
+ alloc,
+ target,
+ if (value.arguments == null)
+ "new-window"
+ else
+ "new-window-command",
+ );
+ defer dbus.deinit(alloc);
+
+ if (value.arguments) |arguments| {
+ // If `-e` was specified on the command line, the first
+ // parameter is an array of strings that contain the arguments
+ // that came after `-e`, which will be interpreted as a command
+ // to run.
+ const as_variant_type = glib.VariantType.new("as");
+ defer as_variant_type.free();
+
+ const s_variant_type = glib.VariantType.new("s");
+ defer s_variant_type.free();
+
+ var command: glib.VariantBuilder = undefined;
+ command.init(as_variant_type);
+ errdefer command.clear();
+
+ for (arguments) |argument| {
+ const bytes = glib.Bytes.new(argument.ptr, argument.len + 1);
+ defer bytes.unref();
+ const string = glib.Variant.newFromBytes(s_variant_type, bytes, @intFromBool(true));
+ command.addValue(string);
}
- }
- 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;
+ dbus.addParameter(command.end());
}
- 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;
}
diff --git a/src/apprt/gtk-ng/ui/1.5/inspector-widget.blp b/src/apprt/gtk-ng/ui/1.5/inspector-widget.blp
index 985a7ed23..ac15ab4e5 100644
--- a/src/apprt/gtk-ng/ui/1.5/inspector-widget.blp
+++ b/src/apprt/gtk-ng/ui/1.5/inspector-widget.blp
@@ -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();
- }
- }
}
diff --git a/src/benchmark/TerminalParser.zig b/src/benchmark/TerminalParser.zig
index 9107d4555..002af4831 100644
--- a/src/benchmark/TerminalParser.zig
+++ b/src/benchmark/TerminalParser.zig
@@ -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) {
diff --git a/src/benchmark/TerminalStream.zig b/src/benchmark/TerminalStream.zig
index 5d235c4ee..28a95226c 100644
--- a/src/benchmark/TerminalStream.zig
+++ b/src/benchmark/TerminalStream.zig
@@ -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;
diff --git a/src/build/Config.zig b/src/build/Config.zig
index 175745dc6..fd892f16c 100644
--- a/src/build/Config.zig
+++ b/src/build/Config.zig
@@ -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.
diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig
index b85f98445..f05a689c6 100644
--- a/src/cli/list_themes.zig
+++ b/src/cli/list_themes.zig
@@ -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);
}
diff --git a/src/config.zig b/src/config.zig
index df4eee791..bcb48214d 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -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;
diff --git a/src/config/Config.zig b/src/config/Config.zig
index 2f6643c7d..d8fcfa1d7 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -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.
-///
-/// Available since: 1.0.0
-///
-/// * `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 (on macOS)
-/// Available since: 1.2.0 (on GTK)
-///
-/// * `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.
-///
-/// 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
+/// * `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.
+///
+/// Available since: 1.0.0
+///
+/// * `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 on macOS, 1.2.0 on GTK
+///
+/// * `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.
+///
+/// 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`
+///
+/// All window decorations will be disabled. Titlebar, borders, etc. will
+/// not be shown. On macOS, this will also disable tabs (enforced by the
+/// system).
///
-/// * `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.
+/// * `auto`
///
-/// * `client` - Prefer client-side decorations.
+/// 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.
///
-/// Available since: 1.1.0
+/// * `client`
///
-/// * `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).
+/// Prefer client-side decorations.
///
-/// 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
///
-/// 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,
diff --git a/src/config/formatter.zig b/src/config/formatter.zig
index cabf80953..a42395c19 100644
--- a/src/config/formatter.zig
+++ b/src/config/formatter.zig
@@ -147,6 +147,8 @@ pub const FileFormatter = struct {
opts: std.fmt.FormatOptions,
writer: anytype,
) !void {
+ @setEvalBranchQuota(10_000);
+
_ = layout;
_ = opts;
diff --git a/src/font/Collection.zig b/src/font/Collection.zig
index ef508b346..2b5f591a5 100644
--- a/src/font/Collection.zig
+++ b/src/font/Collection.zig
@@ -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;
diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig
index f9ce0bff5..290a01d74 100644
--- a/src/font/DeferredFace.zig
+++ b/src/font/DeferredFace.zig
@@ -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()).?;
diff --git a/src/font/discovery.zig b/src/font/discovery.zig
index 6f51379b4..390465916 100644
--- a/src/font/discovery.zig
+++ b/src/font/discovery.zig
@@ -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
diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig
index 1b1c559fb..cb6e6b1f7 100644
--- a/src/font/face/coretext.zig
+++ b/src/font/face/coretext.zig
@@ -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);
diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig
index cb335dff6..5442890bf 100644
--- a/src/font/sprite/Face.zig
+++ b/src/font/sprite/Face.zig
@@ -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.
diff --git a/src/input/key.zig b/src/input/key.zig
index 28aa3ccf4..a3814fb55 100644
--- a/src/input/key.zig
+++ b/src/input/key.zig
@@ -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) {
diff --git a/src/inspector/Inspector.zig b/src/inspector/Inspector.zig
index d3e7fcaaa..27abb8657 100644
--- a/src/inspector/Inspector.zig
+++ b/src/inspector/Inspector.zig
@@ -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;
},
};
}
diff --git a/src/shell-integration/zsh/.zshenv b/src/shell-integration/zsh/.zshenv
index 7fbfad659..3332b1c1f 100644
--- a/src/shell-integration/zsh/.zshenv
+++ b/src/shell-integration/zsh/.zshenv
@@ -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.
diff --git a/src/synthetic/Osc.zig b/src/synthetic/Osc.zig
index e0a6b42a0..8d5d7d3a2 100644
--- a/src/synthetic/Osc.zig
+++ b/src/synthetic/Osc.zig
@@ -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);
}
diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig
index a7563ac8c..a4136d7f3 100644
--- a/src/terminal/PageList.zig
+++ b/src/terminal/PageList.zig
@@ -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);
-
- break :id try self.page.hyperlink_set.addContext(
+ ) 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 => .{},
+ });
+
+ // 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);
-
- break :id try self.page.styles.add(
+ ) 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 => .{},
+ });
+
+ // 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;
diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig
index ec3f322f6..0223545e5 100644
--- a/src/terminal/Parser.zig
+++ b/src/terminal/Parser.zig
@@ -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 {
diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig
index 079df37db..67769923f 100644
--- a/src/terminal/Screen.zig
+++ b/src/terminal/Screen.zig
@@ -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);
diff --git a/src/terminal/Tabstops.zig b/src/terminal/Tabstops.zig
index 4ab5133d9..c352cb351 100644
--- a/src/terminal/Tabstops.zig
+++ b/src/terminal/Tabstops.zig
@@ -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);
diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig
index d08c31b34..c46488f98 100644
--- a/src/terminal/Terminal.zig
+++ b/src/terminal/Terminal.zig
@@ -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);
}
diff --git a/src/terminal/bitmap_allocator.zig b/src/terminal/bitmap_allocator.zig
index 68d968768..724c71be5 100644
--- a/src/terminal/bitmap_allocator.zig
+++ b/src/terminal/bitmap_allocator.zig
@@ -108,30 +108,61 @@ 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;
+ }
+
+ // 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;
}
- // 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 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
fn dumpBitmaps(self: *Self, base: anytype) void {
const bitmaps = self.bitmap.ptr(base);
@@ -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;
- // 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;
- }
+ // 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;
- bitmap.* ^= final;
+ // This bitmap is completely free, we can subtract 64 from
+ // our remaining number.
+ rem -= 64;
}
- // 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;
+ // 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;
- return (start_idx * 64);
+ 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);
+
+ 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],
+ );
+}
diff --git a/src/terminal/hyperlink.zig b/src/terminal/hyperlink.zig
index bb9e78ca6..c608321b1 100644
--- a/src/terminal/hyperlink.zig
+++ b/src/terminal/hyperlink.zig
@@ -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.?);
}
},
);
diff --git a/src/terminal/kitty/graphics_command.zig b/src/terminal/kitty/graphics_command.zig
index adc6edafe..dcb4850c9 100644
--- a/src/terminal/kitty/graphics_command.zig
+++ b/src/terminal/kitty/graphics_command.zig
@@ -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 {
diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig
index 7619c82c1..6090166da 100644
--- a/src/terminal/osc.zig
+++ b/src/terminal/osc.zig
@@ -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;";
diff --git a/src/terminal/page.zig b/src/terminal/page.zig
index fea16c28b..d870bd160 100644
--- a/src/terminal/page.zig
+++ b/src/terminal/page.zig
@@ -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,
diff --git a/src/terminal/search.zig b/src/terminal/search.zig
index 2f87f894b..b3c6494a3 100644
--- a/src/terminal/search.zig
+++ b/src/terminal/search.zig
@@ -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;
diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig
index ec7296490..f40fc4c94 100644
--- a/src/terminal/stream.zig
+++ b/src/terminal/stream.zig
@@ -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);
diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig
index 4b5b93641..e41fe33a9 100644
--- a/src/termio/Termio.zig
+++ b/src/termio/Termio.zig
@@ -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,
};
diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig
index 438c2a0ea..30519b6e2 100644
--- a/src/termio/shell_integration.zig
+++ b/src/termio/shell_integration.zig
@@ -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").?);
+}
diff --git a/valgrind.supp b/valgrind.supp
index 162f3393a..bfc78bcff 100644
--- a/valgrind.supp
+++ b/valgrind.supp
@@ -575,6 +575,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
match-leak-kinds: definite