Merge remote-tracking branch 'upstream/main' into jacob/uucode
commit
90832d89b3
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
58
build.zig
58
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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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?) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -280,6 +280,7 @@ extension Ghostty {
|
|||
case paper
|
||||
case retro
|
||||
case xray
|
||||
case custom
|
||||
case customStyle = "custom-style"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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: "")
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
|
|
|
|||
245
src/Surface.zig
245
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;
|
||||
|
|
|
|||
|
|
@ -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(¶ms, 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 = ¶ms,
|
||||
} });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -35,36 +35,18 @@ pub const ImguiWidget = extern struct {
|
|||
|
||||
pub const properties = struct {};
|
||||
|
||||
pub const signals = struct {
|
||||
/// Emitted when the child widget should render. During the callback,
|
||||
/// the Imgui context is valid.
|
||||
pub const render = struct {
|
||||
pub const name = "render";
|
||||
pub const connect = impl.connect;
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{},
|
||||
void,
|
||||
);
|
||||
};
|
||||
pub const signals = struct {};
|
||||
|
||||
/// Emitted when first realized to allow the embedded ImGui application
|
||||
/// to initialize itself. When this is called, the ImGui context
|
||||
/// is properly set.
|
||||
///
|
||||
/// This might be called multiple times, but each time it is
|
||||
/// called a new Imgui context will be created.
|
||||
pub const setup = struct {
|
||||
pub const name = "setup";
|
||||
pub const connect = impl.connect;
|
||||
const impl = gobject.ext.defineSignal(
|
||||
name,
|
||||
Self,
|
||||
&.{},
|
||||
void,
|
||||
);
|
||||
};
|
||||
pub const virtual_methods = struct {
|
||||
/// This virtual method will be called to allow the Dear ImGui
|
||||
/// application to do one-time setup of the context. The correct context
|
||||
/// will be current when the virtual method is called.
|
||||
pub const setup = C.defineVirtualMethod("setup");
|
||||
|
||||
/// This virtual method will be called at each frame to allow the Dear
|
||||
/// ImGui application to draw the application. The correct context will
|
||||
/// be current when the virtual method is called.
|
||||
pub const render = C.defineVirtualMethod("render");
|
||||
};
|
||||
|
||||
const Private = struct {
|
||||
|
|
@ -113,6 +95,25 @@ pub const ImguiWidget = extern struct {
|
|||
priv.gl_area.queueRender();
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Public wrappers for virtual methods
|
||||
|
||||
/// This virtual method will be called to allow the Dear ImGui application
|
||||
/// to do one-time setup of the context. The correct context will be current
|
||||
/// when the virtual method is called.
|
||||
pub fn setup(self: *Self) callconv(.c) void {
|
||||
const class = self.getClass() orelse return;
|
||||
virtual_methods.setup.call(class, self, .{});
|
||||
}
|
||||
|
||||
/// This virtual method will be called at each frame to allow the Dear ImGui
|
||||
/// application to draw the application. The correct context will be current
|
||||
/// when the virtual method is called.
|
||||
pub fn render(self: *Self) callconv(.c) void {
|
||||
const class = self.getClass() orelse return;
|
||||
virtual_methods.render.call(class, self, .{});
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Private Methods
|
||||
|
||||
|
|
@ -232,13 +233,8 @@ pub const ImguiWidget = extern struct {
|
|||
// initialize the ImgUI OpenGL backend for our context.
|
||||
_ = cimgui.ImGui_ImplOpenGL3_Init(null);
|
||||
|
||||
// Setup our app
|
||||
signals.setup.impl.emit(
|
||||
self,
|
||||
null,
|
||||
.{},
|
||||
null,
|
||||
);
|
||||
// Call the virtual method to setup the UI.
|
||||
self.setup();
|
||||
}
|
||||
|
||||
/// Handle a request to unrealize the GLArea
|
||||
|
|
@ -279,13 +275,8 @@ pub const ImguiWidget = extern struct {
|
|||
self.newFrame();
|
||||
cimgui.c.igNewFrame();
|
||||
|
||||
// Use the callback to draw the UI.
|
||||
signals.render.impl.emit(
|
||||
self,
|
||||
null,
|
||||
.{},
|
||||
null,
|
||||
);
|
||||
// Call the virtual method to draw the UI.
|
||||
self.render();
|
||||
|
||||
// Render
|
||||
cimgui.c.igRender();
|
||||
|
|
@ -422,15 +413,34 @@ pub const ImguiWidget = extern struct {
|
|||
cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, bytes);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Default virtual method handlers
|
||||
|
||||
/// Default setup function. Does nothing but log a warning.
|
||||
fn defaultSetup(_: *Self) callconv(.c) void {
|
||||
log.warn("default Dear ImGui setup called, this is a bug.", .{});
|
||||
}
|
||||
|
||||
/// Default render function. Does nothing but log a warning.
|
||||
fn defaultRender(_: *Self) callconv(.c) void {
|
||||
log.warn("default Dear ImGui render called, this is a bug.", .{});
|
||||
}
|
||||
|
||||
const C = Common(Self, Private);
|
||||
pub const as = C.as;
|
||||
pub const ref = C.ref;
|
||||
pub const refSink = C.refSink;
|
||||
pub const unref = C.unref;
|
||||
pub const getClass = C.getClass;
|
||||
const private = C.private;
|
||||
|
||||
pub const Class = extern struct {
|
||||
parent_class: Parent.Class,
|
||||
|
||||
/// Function pointers for virtual methods.
|
||||
setup: ?*const fn (*Self) callconv(.c) void,
|
||||
render: ?*const fn (*Self) callconv(.c) void,
|
||||
|
||||
var parent: *Parent.Class = undefined;
|
||||
pub const Instance = Self;
|
||||
|
||||
|
|
@ -444,6 +454,10 @@ pub const ImguiWidget = extern struct {
|
|||
}),
|
||||
);
|
||||
|
||||
// Initialize our virtual methods with default functions.
|
||||
class.setup = defaultSetup;
|
||||
class.render = defaultRender;
|
||||
|
||||
// Bindings
|
||||
class.bindTemplateChildPrivate("gl_area", .{});
|
||||
class.bindTemplateChildPrivate("im_context", .{});
|
||||
|
|
@ -464,8 +478,6 @@ pub const ImguiWidget = extern struct {
|
|||
class.bindTemplateCallback("im_commit", &imCommit);
|
||||
|
||||
// Signals
|
||||
signals.render.impl.register(.{});
|
||||
signals.setup.impl.register(.{});
|
||||
|
||||
// Virtual methods
|
||||
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
const ApprtApp = @import("../App.zig");
|
||||
const DBus = @import("DBus.zig");
|
||||
|
||||
// Use a D-Bus method call to open a new window on GTK.
|
||||
// See: https://wiki.gnome.org/Projects/GLib/GApplication/DBusAPI
|
||||
|
|
@ -22,149 +21,42 @@ const ApprtApp = @import("../App.zig");
|
|||
// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["echo" "hello"]>]' []
|
||||
// ```
|
||||
pub fn newWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Action.NewWindow) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool {
|
||||
const stderr = std.io.getStdErr().writer();
|
||||
var dbus = try DBus.init(
|
||||
alloc,
|
||||
target,
|
||||
if (value.arguments == null)
|
||||
"new-window"
|
||||
else
|
||||
"new-window-command",
|
||||
);
|
||||
defer dbus.deinit(alloc);
|
||||
|
||||
// Get the appropriate bus name and object path for contacting the
|
||||
// Ghostty instance we're interested in.
|
||||
const bus_name: [:0]const u8, const object_path: [:0]const u8 = switch (target) {
|
||||
.class => |class| result: {
|
||||
// Force the usage of the class specified on the CLI to determine the
|
||||
// bus name and object path.
|
||||
const object_path = try std.fmt.allocPrintZ(alloc, "/{s}", .{class});
|
||||
if (value.arguments) |arguments| {
|
||||
// If `-e` was specified on the command line, the first
|
||||
// parameter is an array of strings that contain the arguments
|
||||
// that came after `-e`, which will be interpreted as a command
|
||||
// to run.
|
||||
const as_variant_type = glib.VariantType.new("as");
|
||||
defer as_variant_type.free();
|
||||
|
||||
std.mem.replaceScalar(u8, object_path, '.', '/');
|
||||
std.mem.replaceScalar(u8, object_path, '-', '_');
|
||||
const s_variant_type = glib.VariantType.new("s");
|
||||
defer s_variant_type.free();
|
||||
|
||||
break :result .{ class, object_path };
|
||||
},
|
||||
.detect => .{ ApprtApp.application_id, ApprtApp.object_path },
|
||||
};
|
||||
defer {
|
||||
switch (target) {
|
||||
.class => alloc.free(object_path),
|
||||
.detect => {},
|
||||
var command: glib.VariantBuilder = undefined;
|
||||
command.init(as_variant_type);
|
||||
errdefer command.clear();
|
||||
|
||||
for (arguments) |argument| {
|
||||
const bytes = glib.Bytes.new(argument.ptr, argument.len + 1);
|
||||
defer bytes.unref();
|
||||
const string = glib.Variant.newFromBytes(s_variant_type, bytes, @intFromBool(true));
|
||||
command.addValue(string);
|
||||
}
|
||||
|
||||
dbus.addParameter(command.end());
|
||||
}
|
||||
|
||||
if (gio.Application.idIsValid(bus_name.ptr) == 0) {
|
||||
try stderr.print("D-Bus bus name is not valid: {s}\n", .{bus_name});
|
||||
return error.IPCFailed;
|
||||
}
|
||||
|
||||
if (glib.Variant.isObjectPath(object_path.ptr) == 0) {
|
||||
try stderr.print("D-Bus object path is not valid: {s}\n", .{object_path});
|
||||
return error.IPCFailed;
|
||||
}
|
||||
|
||||
const dbus = dbus: {
|
||||
var err_: ?*glib.Error = null;
|
||||
defer if (err_) |err| err.free();
|
||||
|
||||
const dbus_ = gio.busGetSync(.session, null, &err_);
|
||||
if (err_) |err| {
|
||||
try stderr.print(
|
||||
"Unable to establish connection to D-Bus session bus: {s}\n",
|
||||
.{err.f_message orelse "(unknown)"},
|
||||
);
|
||||
return error.IPCFailed;
|
||||
}
|
||||
|
||||
break :dbus dbus_ orelse {
|
||||
try stderr.print("gio.busGetSync returned null\n", .{});
|
||||
return error.IPCFailed;
|
||||
};
|
||||
};
|
||||
defer dbus.unref();
|
||||
|
||||
// use a builder to create the D-Bus method call payload
|
||||
const payload = payload: {
|
||||
const payload_variant_type = glib.VariantType.new("(sava{sv})");
|
||||
defer glib.free(payload_variant_type);
|
||||
|
||||
// Initialize our builder to build up our parameters
|
||||
var builder: glib.VariantBuilder = undefined;
|
||||
builder.init(payload_variant_type);
|
||||
errdefer builder.clear();
|
||||
|
||||
// action
|
||||
if (value.arguments == null) {
|
||||
builder.add("s", "new-window");
|
||||
} else {
|
||||
builder.add("s", "new-window-command");
|
||||
}
|
||||
|
||||
// parameters
|
||||
{
|
||||
const av_variant_type = glib.VariantType.new("av");
|
||||
defer av_variant_type.free();
|
||||
|
||||
var parameters: glib.VariantBuilder = undefined;
|
||||
parameters.init(av_variant_type);
|
||||
errdefer parameters.clear();
|
||||
|
||||
if (value.arguments) |arguments| {
|
||||
// If `-e` was specified on the command line, the first
|
||||
// parameter is an array of strings that contain the arguments
|
||||
// that came after `-e`, which will be interpreted as a command
|
||||
// to run.
|
||||
{
|
||||
const as = glib.VariantType.new("as");
|
||||
defer as.free();
|
||||
|
||||
var command: glib.VariantBuilder = undefined;
|
||||
command.init(as);
|
||||
errdefer command.clear();
|
||||
|
||||
for (arguments) |argument| {
|
||||
command.add("s", argument.ptr);
|
||||
}
|
||||
|
||||
parameters.add("v", command.end());
|
||||
}
|
||||
}
|
||||
|
||||
builder.addValue(parameters.end());
|
||||
}
|
||||
|
||||
{
|
||||
const platform_data_variant_type = glib.VariantType.new("a{sv}");
|
||||
defer platform_data_variant_type.free();
|
||||
|
||||
builder.open(platform_data_variant_type);
|
||||
defer builder.close();
|
||||
|
||||
// we have no platform data
|
||||
}
|
||||
|
||||
break :payload builder.end();
|
||||
};
|
||||
|
||||
{
|
||||
var err_: ?*glib.Error = null;
|
||||
defer if (err_) |err| err.free();
|
||||
|
||||
const result_ = dbus.callSync(
|
||||
bus_name,
|
||||
object_path,
|
||||
"org.gtk.Actions",
|
||||
"Activate",
|
||||
payload,
|
||||
null, // We don't care about the return type, we don't do anything with it.
|
||||
.{}, // no flags
|
||||
-1, // default timeout
|
||||
null, // not cancellable
|
||||
&err_,
|
||||
);
|
||||
defer if (result_) |result| result.unref();
|
||||
|
||||
if (err_) |err| {
|
||||
try stderr.print(
|
||||
"D-Bus method call returned an error err={s}\n",
|
||||
.{err.f_message orelse "(unknown)"},
|
||||
);
|
||||
return error.IPCFailed;
|
||||
}
|
||||
}
|
||||
try dbus.send();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -592,24 +592,24 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF },
|
|||
///
|
||||
/// * `contain`
|
||||
///
|
||||
/// Preserving the aspect ratio, scale the background image to the largest
|
||||
/// size that can still be contained within the terminal, so that the whole
|
||||
/// image is visible.
|
||||
/// Preserving the aspect ratio, scale the background image to the largest
|
||||
/// size that can still be contained within the terminal, so that the whole
|
||||
/// image is visible.
|
||||
///
|
||||
/// * `cover`
|
||||
///
|
||||
/// Preserving the aspect ratio, scale the background image to the smallest
|
||||
/// size that can completely cover the terminal. This may result in one or
|
||||
/// more edges of the image being clipped by the edge of the terminal.
|
||||
/// Preserving the aspect ratio, scale the background image to the smallest
|
||||
/// size that can completely cover the terminal. This may result in one or
|
||||
/// more edges of the image being clipped by the edge of the terminal.
|
||||
///
|
||||
/// * `stretch`
|
||||
///
|
||||
/// Stretch the background image to the full size of the terminal, without
|
||||
/// preserving the aspect ratio.
|
||||
/// Stretch the background image to the full size of the terminal, without
|
||||
/// preserving the aspect ratio.
|
||||
///
|
||||
/// * `none`
|
||||
///
|
||||
/// Don't scale the background image.
|
||||
/// Don't scale the background image.
|
||||
///
|
||||
/// The default value is `contain`.
|
||||
///
|
||||
|
|
@ -1330,53 +1330,59 @@ class: ?[:0]const u8 = null,
|
|||
/// The keybind trigger can be prefixed with some special values to change
|
||||
/// the behavior of the keybind. These are:
|
||||
///
|
||||
/// * `all:` - Make the keybind apply to all terminal surfaces. By default,
|
||||
/// keybinds only apply to the focused terminal surface. If this is true,
|
||||
/// then the keybind will be sent to all terminal surfaces. This only
|
||||
/// applies to actions that are surface-specific. For actions that
|
||||
/// are already global (e.g. `quit`), this prefix has no effect.
|
||||
/// * `all:`
|
||||
///
|
||||
/// Available since: 1.0.0
|
||||
/// Make the keybind apply to all terminal surfaces. By default,
|
||||
/// keybinds only apply to the focused terminal surface. If this is true,
|
||||
/// then the keybind will be sent to all terminal surfaces. This only
|
||||
/// applies to actions that are surface-specific. For actions that
|
||||
/// are already global (e.g. `quit`), this prefix has no effect.
|
||||
///
|
||||
/// * `global:` - Make the keybind global. By default, keybinds only work
|
||||
/// within Ghostty and under the right conditions (application focused,
|
||||
/// sometimes terminal focused, etc.). If you want a keybind to work
|
||||
/// globally across your system (e.g. even when Ghostty is not focused),
|
||||
/// specify this prefix. This prefix implies `all:`. Note: this does not
|
||||
/// work in all environments; see the additional notes below for more
|
||||
/// information.
|
||||
/// Available since: 1.0.0
|
||||
///
|
||||
/// Available since: 1.0.0 (on macOS)
|
||||
/// Available since: 1.2.0 (on GTK)
|
||||
/// * `global:`
|
||||
///
|
||||
/// * `unconsumed:` - Do not consume the input. By default, a keybind
|
||||
/// will consume the input, meaning that the associated encoding (if
|
||||
/// any) will not be sent to the running program in the terminal. If
|
||||
/// you wish to send the encoded value to the program, specify the
|
||||
/// `unconsumed:` prefix before the entire keybind. For example:
|
||||
/// `unconsumed:ctrl+a=reload_config`. `global:` and `all:`-prefixed
|
||||
/// keybinds will always consume the input regardless of this setting.
|
||||
/// Since they are not associated with a specific terminal surface,
|
||||
/// they're never encoded.
|
||||
/// Make the keybind global. By default, keybinds only work within Ghostty
|
||||
/// and under the right conditions (application focused, sometimes terminal
|
||||
/// focused, etc.). If you want a keybind to work globally across your system
|
||||
/// (e.g. even when Ghostty is not focused), specify this prefix.
|
||||
/// This prefix implies `all:`.
|
||||
///
|
||||
/// Available since: 1.0.0
|
||||
/// Note: this does not work in all environments; see the additional notes
|
||||
/// below for more information.
|
||||
///
|
||||
/// * `performable:` - Only consume the input if the action is able to be
|
||||
/// performed. For example, the `copy_to_clipboard` action will only
|
||||
/// consume the input if there is a selection to copy. If there is no
|
||||
/// selection, Ghostty behaves as if the keybind was not set. This has
|
||||
/// no effect with `global:` or `all:`-prefixed keybinds. For key
|
||||
/// sequences, this will reset the sequence if the action is not
|
||||
/// performable (acting identically to not having a keybind set at
|
||||
/// all).
|
||||
/// Available since: 1.0.0 on macOS, 1.2.0 on GTK
|
||||
///
|
||||
/// Performable keybinds will not appear as menu shortcuts in the
|
||||
/// application menu. This is because the menu shortcuts force the
|
||||
/// action to be performed regardless of the state of the terminal.
|
||||
/// Performable keybinds will still work, they just won't appear as
|
||||
/// a shortcut label in the menu.
|
||||
/// * `unconsumed:`
|
||||
///
|
||||
/// Available since: 1.1.0
|
||||
/// Do not consume the input. By default, a keybind will consume the input,
|
||||
/// meaning that the associated encoding (if any) will not be sent to the
|
||||
/// running program in the terminal. If you wish to send the encoded value
|
||||
/// to the program, specify the `unconsumed:` prefix before the entire
|
||||
/// keybind. For example: `unconsumed:ctrl+a=reload_config`. `global:` and
|
||||
/// `all:`-prefixed keybinds will always consume the input regardless of
|
||||
/// this setting. Since they are not associated with a specific terminal
|
||||
/// surface, they're never encoded.
|
||||
///
|
||||
/// Available since: 1.0.0
|
||||
///
|
||||
/// * `performable:`
|
||||
///
|
||||
/// Only consume the input if the action is able to be performed.
|
||||
/// For example, the `copy_to_clipboard` action will only consume the input
|
||||
/// if there is a selection to copy. If there is no selection, Ghostty
|
||||
/// behaves as if the keybind was not set. This has no effect with `global:`
|
||||
/// or `all:`-prefixed keybinds. For key sequences, this will reset the
|
||||
/// sequence if the action is not performable (acting identically to not
|
||||
/// having a keybind set at all).
|
||||
///
|
||||
/// Performable keybinds will not appear as menu shortcuts in the
|
||||
/// application menu. This is because the menu shortcuts force the
|
||||
/// action to be performed regardless of the state of the terminal.
|
||||
/// Performable keybinds will still work, they just won't appear as
|
||||
/// a shortcut label in the menu.
|
||||
///
|
||||
/// Available since: 1.1.0
|
||||
///
|
||||
/// Keybind triggers are not unique per prefix combination. For example,
|
||||
/// `ctrl+a` and `global:ctrl+a` are not two separate keybinds. The keybind
|
||||
|
|
@ -1522,28 +1528,36 @@ keybind: Keybinds = .{},
|
|||
///
|
||||
/// Valid values:
|
||||
///
|
||||
/// * `none` - All window decorations will be disabled. Titlebar,
|
||||
/// borders, etc. will not be shown. On macOS, this will also disable
|
||||
/// tabs (enforced by the system).
|
||||
/// * `none`
|
||||
///
|
||||
/// * `auto` - Automatically decide to use either client-side or server-side
|
||||
/// decorations based on the detected preferences of the current OS and
|
||||
/// desktop environment. This option usually makes Ghostty look the most
|
||||
/// "native" for your desktop.
|
||||
/// All window decorations will be disabled. Titlebar, borders, etc. will
|
||||
/// not be shown. On macOS, this will also disable tabs (enforced by the
|
||||
/// system).
|
||||
///
|
||||
/// * `client` - Prefer client-side decorations.
|
||||
/// * `auto`
|
||||
///
|
||||
/// Available since: 1.1.0
|
||||
/// Automatically decide to use either client-side or server-side
|
||||
/// decorations based on the detected preferences of the current OS and
|
||||
/// desktop environment. This option usually makes Ghostty look the most
|
||||
/// "native" for your desktop.
|
||||
///
|
||||
/// * `server` - Prefer server-side decorations. This is only relevant
|
||||
/// on Linux with GTK, either on X11, or Wayland on a compositor that
|
||||
/// supports the `org_kde_kwin_server_decoration` protocol (e.g. KDE Plasma,
|
||||
/// but almost any non-GNOME desktop supports this protocol).
|
||||
/// * `client`
|
||||
///
|
||||
/// If `server` is set but the environment doesn't support server-side
|
||||
/// decorations, client-side decorations will be used instead.
|
||||
/// Prefer client-side decorations.
|
||||
///
|
||||
/// Available since: 1.1.0
|
||||
/// Available since: 1.1.0
|
||||
///
|
||||
/// * `server`
|
||||
///
|
||||
/// Prefer server-side decorations. This is only relevant on Linux with GTK,
|
||||
/// either on X11, or Wayland on a compositor that supports the
|
||||
/// `org_kde_kwin_server_decoration` protocol (e.g. KDE Plasma, but almost
|
||||
/// any non-GNOME desktop supports this protocol).
|
||||
///
|
||||
/// If `server` is set but the environment doesn't support server-side
|
||||
/// decorations, client-side decorations will be used instead.
|
||||
///
|
||||
/// Available since: 1.1.0
|
||||
///
|
||||
/// The default value is `auto`.
|
||||
///
|
||||
|
|
@ -1886,6 +1900,19 @@ keybind: Keybinds = .{},
|
|||
else => .false,
|
||||
},
|
||||
|
||||
/// The action to take when the user right-clicks on the terminal surface.
|
||||
///
|
||||
/// Valid values:
|
||||
/// * `context-menu` - Show the context menu.
|
||||
/// * `paste` - Paste the contents of the clipboard.
|
||||
/// * `copy` - Copy the selected text to the clipboard.
|
||||
/// * `copy-or-paste` - If there is a selection, copy the selected text to
|
||||
/// the clipboard; otherwise, paste the contents of the clipboard.
|
||||
/// * `ignore` - Do nothing, ignore the right-click.
|
||||
///
|
||||
/// The default value is `context-menu`.
|
||||
@"right-click-action": RightClickAction = .@"context-menu",
|
||||
|
||||
/// The time in milliseconds between clicks to consider a click a repeat
|
||||
/// (double, triple, etc.) or an entirely new single click. A value of zero will
|
||||
/// use a platform-specific default. The default on macOS is determined by the
|
||||
|
|
@ -2316,9 +2343,9 @@ keybind: Keybinds = .{},
|
|||
///
|
||||
/// * `sampler2D iChannel0` - Input texture.
|
||||
///
|
||||
/// A texture containing the current terminal screen. If multiple custom
|
||||
/// shaders are specified, the output of previous shaders is written to
|
||||
/// this texture, to allow combining multiple effects.
|
||||
/// A texture containing the current terminal screen. If multiple custom
|
||||
/// shaders are specified, the output of previous shaders is written to
|
||||
/// this texture, to allow combining multiple effects.
|
||||
///
|
||||
/// * `vec3 iResolution` - Output texture size, `[width, height, 1]` (in px).
|
||||
///
|
||||
|
|
@ -2604,6 +2631,21 @@ keybind: Keybinds = .{},
|
|||
/// editor, etc.
|
||||
@"macos-titlebar-proxy-icon": MacTitlebarProxyIcon = .visible,
|
||||
|
||||
/// Controls the windowing behavior when dropping a file or folder
|
||||
/// onto the Ghostty icon in the macOS dock.
|
||||
///
|
||||
/// Valid values are:
|
||||
///
|
||||
/// * `new-tab` - Create a new tab in the current window, or open
|
||||
/// a new window if none exist.
|
||||
/// * `new-window` - Create a new window unconditionally.
|
||||
///
|
||||
/// The default value is `new-tab`.
|
||||
///
|
||||
/// This setting is only supported on macOS and has no effect on other
|
||||
/// platforms.
|
||||
@"macos-dock-drop-behavior": MacOSDockDropBehavior = .@"new-tab",
|
||||
|
||||
/// macOS doesn't have a distinct "alt" key and instead has the "option"
|
||||
/// key which behaves slightly differently. On macOS by default, the
|
||||
/// option key plus a character will sometimes produce a Unicode character.
|
||||
|
|
@ -2708,6 +2750,8 @@ keybind: Keybinds = .{},
|
|||
/// * `blueprint`, `chalkboard`, `microchip`, `glass`, `holographic`,
|
||||
/// `paper`, `retro`, `xray` - Official variants of the Ghostty icon
|
||||
/// hand-created by artists (no AI).
|
||||
/// * `custom` - Use a completely custom icon. The location must be specified
|
||||
/// using the additional `macos-custom-icon` configuration
|
||||
/// * `custom-style` - Use the official Ghostty icon but with custom
|
||||
/// styles applied to various layers. The custom styles must be
|
||||
/// specified using the additional `macos-icon`-prefixed configurations.
|
||||
|
|
@ -2726,6 +2770,15 @@ keybind: Keybinds = .{},
|
|||
/// effort.
|
||||
@"macos-icon": MacAppIcon = .official,
|
||||
|
||||
/// The absolute path to the custom icon file.
|
||||
/// Supported formats include PNG, JPEG, and ICNS.
|
||||
///
|
||||
/// Defaults to `~/.config/ghostty/Ghostty.icns`
|
||||
///
|
||||
/// Note: This configuration is required when `macos-icon` is set to
|
||||
/// `custom`
|
||||
@"macos-custom-icon": ?[]const u8 = null,
|
||||
|
||||
/// The material to use for the frame of the macOS app icon.
|
||||
///
|
||||
/// Valid values:
|
||||
|
|
@ -6695,6 +6748,25 @@ pub const CopyOnSelect = enum {
|
|||
clipboard,
|
||||
};
|
||||
|
||||
/// Options for right-click actions.
|
||||
pub const RightClickAction = enum {
|
||||
/// No action is taken on right-click.
|
||||
ignore,
|
||||
|
||||
/// Pastes from the system clipboard.
|
||||
paste,
|
||||
|
||||
/// Copies the selected text to the system clipboard.
|
||||
copy,
|
||||
|
||||
/// Copies the selected text to the system clipboard and
|
||||
/// pastes the clipboard if no text is selected.
|
||||
@"copy-or-paste",
|
||||
|
||||
/// Shows a context menu with options.
|
||||
@"context-menu",
|
||||
};
|
||||
|
||||
/// Shell integration values
|
||||
pub const ShellIntegration = enum {
|
||||
none,
|
||||
|
|
@ -6929,6 +7001,7 @@ pub const MacAppIcon = enum {
|
|||
paper,
|
||||
retro,
|
||||
xray,
|
||||
custom,
|
||||
@"custom-style",
|
||||
};
|
||||
|
||||
|
|
@ -7024,6 +7097,12 @@ pub const WindowNewTabPosition = enum {
|
|||
end,
|
||||
};
|
||||
|
||||
/// See macos-dock-drop-behavior
|
||||
pub const MacOSDockDropBehavior = enum {
|
||||
@"new-tab",
|
||||
window,
|
||||
};
|
||||
|
||||
/// See window-show-tab-bar
|
||||
pub const WindowShowTabBar = enum {
|
||||
always,
|
||||
|
|
|
|||
|
|
@ -147,6 +147,8 @@ pub const FileFormatter = struct {
|
|||
opts: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
@setEvalBranchQuota(10_000);
|
||||
|
||||
_ = layout;
|
||||
_ = opts;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()).?;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1004,23 +1004,34 @@ const ReflowCursor = struct {
|
|||
// Copy the graphemes
|
||||
const cps = src_page.lookupGrapheme(cell).?;
|
||||
|
||||
// If our page can't support an additional cell with
|
||||
// graphemes then we create a new page for this row.
|
||||
// If our page can't support an additional cell
|
||||
// with graphemes then we increase capacity.
|
||||
if (self.page.graphemeCount() >= self.page.graphemeCapacity()) {
|
||||
try self.moveLastRowToNewPage(list, cap);
|
||||
} else {
|
||||
// Attempt to allocate the space that would be required for
|
||||
// these graphemes, and if it's not available, create a new
|
||||
// page for this row.
|
||||
if (self.page.grapheme_alloc.alloc(
|
||||
u21,
|
||||
self.page.memory,
|
||||
cps.len,
|
||||
)) |slice| {
|
||||
self.page.grapheme_alloc.free(self.page.memory, slice);
|
||||
} else |_| {
|
||||
try self.moveLastRowToNewPage(list, cap);
|
||||
try self.adjustCapacity(list, .{
|
||||
.hyperlink_bytes = cap.grapheme_bytes * 2,
|
||||
});
|
||||
}
|
||||
|
||||
// Attempt to allocate the space that would be required
|
||||
// for these graphemes, and if it's not available, then
|
||||
// increase capacity.
|
||||
if (self.page.grapheme_alloc.alloc(
|
||||
u21,
|
||||
self.page.memory,
|
||||
cps.len,
|
||||
)) |slice| {
|
||||
self.page.grapheme_alloc.free(self.page.memory, slice);
|
||||
} else |_| {
|
||||
// Grow our capacity until we can
|
||||
// definitely fit the extra bytes.
|
||||
const required = cps.len * @sizeOf(u21);
|
||||
var new_grapheme_capacity: usize = cap.grapheme_bytes;
|
||||
while (new_grapheme_capacity - cap.grapheme_bytes < required) {
|
||||
new_grapheme_capacity *= 2;
|
||||
}
|
||||
try self.adjustCapacity(list, .{
|
||||
.grapheme_bytes = new_grapheme_capacity,
|
||||
});
|
||||
}
|
||||
|
||||
// This shouldn't fail since we made sure we have space above.
|
||||
|
|
@ -1032,25 +1043,67 @@ const ReflowCursor = struct {
|
|||
const src_id = src_page.lookupHyperlink(cell).?;
|
||||
const src_link = src_page.hyperlink_set.get(src_page.memory, src_id);
|
||||
|
||||
// If our page can't support an additional cell with
|
||||
// a hyperlink ID then we create a new page for this row.
|
||||
// If our page can't support an additional cell
|
||||
// with a hyperlink then we increase capacity.
|
||||
if (self.page.hyperlinkCount() >= self.page.hyperlinkCapacity()) {
|
||||
try self.moveLastRowToNewPage(list, cap);
|
||||
try self.adjustCapacity(list, .{
|
||||
.hyperlink_bytes = cap.hyperlink_bytes * 2,
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure that the string alloc has sufficient capacity
|
||||
// to dupe the link (and the ID if it's not implicit).
|
||||
const additional_required_string_capacity =
|
||||
src_link.uri.len +
|
||||
switch (src_link.id) {
|
||||
.explicit => |v| v.len,
|
||||
.implicit => 0,
|
||||
};
|
||||
if (self.page.string_alloc.alloc(
|
||||
u8,
|
||||
self.page.memory,
|
||||
additional_required_string_capacity,
|
||||
)) |slice| {
|
||||
// We have enough capacity, free the test alloc.
|
||||
self.page.string_alloc.free(self.page.memory, slice);
|
||||
} else |_| {
|
||||
// Grow our capacity until we can
|
||||
// definitely fit the extra bytes.
|
||||
var new_string_capacity: usize = cap.string_bytes;
|
||||
while (new_string_capacity - cap.string_bytes < additional_required_string_capacity) {
|
||||
new_string_capacity *= 2;
|
||||
}
|
||||
try self.adjustCapacity(list, .{
|
||||
.string_bytes = new_string_capacity,
|
||||
});
|
||||
}
|
||||
|
||||
const dst_id = self.page.hyperlink_set.addWithIdContext(
|
||||
self.page.memory,
|
||||
// We made sure there was enough capacity for this above.
|
||||
try src_link.dupe(src_page, self.page),
|
||||
src_id,
|
||||
.{ .page = self.page },
|
||||
) catch id: {
|
||||
// We have no space for this link,
|
||||
// so make a new page for this row.
|
||||
try self.moveLastRowToNewPage(list, cap);
|
||||
) catch |err| id: {
|
||||
// If the add failed then either the set needs to grow
|
||||
// or it needs to be rehashed. Either one of those can
|
||||
// be accomplished by adjusting capacity, either with
|
||||
// no actual change or with an increased hyperlink cap.
|
||||
try self.adjustCapacity(list, switch (err) {
|
||||
error.OutOfMemory => .{
|
||||
.hyperlink_bytes = cap.hyperlink_bytes * 2,
|
||||
},
|
||||
error.NeedsRehash => .{},
|
||||
});
|
||||
|
||||
break :id try self.page.hyperlink_set.addContext(
|
||||
// We assume this one will succeed. We dupe the link
|
||||
// again, and don't have to worry about the other one
|
||||
// because adjusting the capacity naturally clears up
|
||||
// any managed memory not associated with a cell yet.
|
||||
break :id try self.page.hyperlink_set.addWithIdContext(
|
||||
self.page.memory,
|
||||
try src_link.dupe(src_page, self.page),
|
||||
src_id,
|
||||
.{ .page = self.page },
|
||||
);
|
||||
} orelse src_id;
|
||||
|
|
@ -1075,14 +1128,23 @@ const ReflowCursor = struct {
|
|||
self.page.memory,
|
||||
style,
|
||||
cell.style_id,
|
||||
) catch id: {
|
||||
// We have no space for this style,
|
||||
// so make a new page for this row.
|
||||
try self.moveLastRowToNewPage(list, cap);
|
||||
) catch |err| id: {
|
||||
// If the add failed then either the set needs to grow
|
||||
// or it needs to be rehashed. Either one of those can
|
||||
// be accomplished by adjusting capacity, either with
|
||||
// no actual change or with an increased style cap.
|
||||
try self.adjustCapacity(list, switch (err) {
|
||||
error.OutOfMemory => .{
|
||||
.styles = cap.styles * 2,
|
||||
},
|
||||
error.NeedsRehash => .{},
|
||||
});
|
||||
|
||||
break :id try self.page.styles.add(
|
||||
// We assume this one will succeed.
|
||||
break :id try self.page.styles.addWithId(
|
||||
self.page.memory,
|
||||
style,
|
||||
cell.style_id,
|
||||
);
|
||||
} orelse cell.style_id;
|
||||
|
||||
|
|
@ -1150,6 +1212,22 @@ const ReflowCursor = struct {
|
|||
}
|
||||
}
|
||||
|
||||
/// Adjust the capacity of the current page.
|
||||
fn adjustCapacity(
|
||||
self: *ReflowCursor,
|
||||
list: *PageList,
|
||||
adjustment: AdjustCapacity,
|
||||
) !void {
|
||||
const old_x = self.x;
|
||||
const old_y = self.y;
|
||||
|
||||
self.* = .init(try list.adjustCapacity(
|
||||
self.node,
|
||||
adjustment,
|
||||
));
|
||||
self.cursorAbsolute(old_x, old_y);
|
||||
}
|
||||
|
||||
/// True if this cursor is at the bottom of the page by capacity,
|
||||
/// i.e. we can't scroll anymore.
|
||||
fn bottom(self: *const ReflowCursor) bool {
|
||||
|
|
@ -2317,8 +2395,8 @@ pub fn eraseRows(
|
|||
break;
|
||||
}
|
||||
|
||||
self.erasePage(chunk.node);
|
||||
erased += chunk.node.data.size.rows;
|
||||
self.erasePage(chunk.node);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -7029,6 +7107,7 @@ test "PageList resize reflow less cols wrap across page boundary cursor in secon
|
|||
try testing.expect(!cells[3].hasText());
|
||||
}
|
||||
}
|
||||
|
||||
test "PageList resize reflow more cols cursor in wrapped row" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
|
@ -7222,6 +7301,296 @@ test "PageList resize reflow more cols no reflow preserves semantic prompt" {
|
|||
}
|
||||
}
|
||||
|
||||
test "PageList resize reflow exceeds hyperlink memory forcing capacity increase" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 2, 10, 0);
|
||||
defer s.deinit();
|
||||
try testing.expectEqual(@as(usize, 1), s.totalPages());
|
||||
|
||||
// Grow to the capacity of the first page and add
|
||||
// one more row so that we have two pages total.
|
||||
{
|
||||
const page = &s.pages.first.?.data;
|
||||
page.pauseIntegrityChecks(true);
|
||||
for (page.size.rows..page.capacity.rows) |_| {
|
||||
_ = try s.grow();
|
||||
}
|
||||
page.pauseIntegrityChecks(false);
|
||||
try testing.expectEqual(@as(usize, 1), s.totalPages());
|
||||
try s.growRows(1);
|
||||
try testing.expectEqual(@as(usize, 2), s.totalPages());
|
||||
|
||||
// We now have two pages.
|
||||
try std.testing.expect(s.pages.first.? != s.pages.last.?);
|
||||
try std.testing.expectEqual(s.pages.last.?, s.pages.first.?.next);
|
||||
}
|
||||
|
||||
// We use almost all string alloc capacity with a hyperlink in the final
|
||||
// row of the first page, and do the same on the first row of the second
|
||||
// page. We also mark the row as wrapped so that when we resize with more
|
||||
// cols the row unwraps and we have a single row that requires almost two
|
||||
// times the base string alloc capacity.
|
||||
//
|
||||
// This forces the reflow to increase capacity.
|
||||
//
|
||||
// +--+ = PAGE 0
|
||||
// : :
|
||||
// | X… <- where X is hyperlinked with almost all string cap.
|
||||
// +--+
|
||||
// +--+ = PAGE 1
|
||||
// …X | <- X here also almost hits string cap with a hyperlink.
|
||||
// +--+
|
||||
|
||||
// Almost hit string alloc cap in bottom right of first page.
|
||||
// Mark the final row as wrapped.
|
||||
{
|
||||
const page = &s.pages.first.?.data;
|
||||
const id = try page.insertHyperlink(.{
|
||||
.id = .{ .implicit = 0 },
|
||||
.uri = "a" ** (pagepkg.string_bytes_default - 1),
|
||||
});
|
||||
const rac = page.getRowAndCell(page.size.cols - 1, page.size.rows - 1);
|
||||
rac.row.wrap = true;
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = 'X' },
|
||||
};
|
||||
try page.setHyperlink(rac.row, rac.cell, id);
|
||||
try std.testing.expectError(
|
||||
error.StringsOutOfMemory,
|
||||
page.insertHyperlink(.{
|
||||
.id = .{ .implicit = 1 },
|
||||
.uri = "AAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Almost hit string alloc cap in top left of second page.
|
||||
// Mark the first row as a wrap continuation.
|
||||
{
|
||||
const page = &s.pages.last.?.data;
|
||||
const id = try page.insertHyperlink(.{
|
||||
.id = .{ .implicit = 1 },
|
||||
.uri = "a" ** (pagepkg.string_bytes_default - 1),
|
||||
});
|
||||
const rac = page.getRowAndCell(0, 0);
|
||||
rac.row.wrap_continuation = true;
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = 'X' },
|
||||
};
|
||||
try page.setHyperlink(rac.row, rac.cell, id);
|
||||
try std.testing.expectError(
|
||||
error.StringsOutOfMemory,
|
||||
page.insertHyperlink(.{
|
||||
.id = .{ .implicit = 2 },
|
||||
.uri = "AAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Resize to 1 column wider, unwrapping the row.
|
||||
try s.resize(.{ .cols = s.cols + 1, .reflow = true });
|
||||
}
|
||||
|
||||
test "PageList resize reflow exceeds grapheme memory forcing capacity increase" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 2, 10, 0);
|
||||
defer s.deinit();
|
||||
try testing.expectEqual(@as(usize, 1), s.totalPages());
|
||||
|
||||
// Grow to the capacity of the first page and add
|
||||
// one more row so that we have two pages total.
|
||||
{
|
||||
const page = &s.pages.first.?.data;
|
||||
page.pauseIntegrityChecks(true);
|
||||
for (page.size.rows..page.capacity.rows) |_| {
|
||||
_ = try s.grow();
|
||||
}
|
||||
page.pauseIntegrityChecks(false);
|
||||
try testing.expectEqual(@as(usize, 1), s.totalPages());
|
||||
try s.growRows(1);
|
||||
try testing.expectEqual(@as(usize, 2), s.totalPages());
|
||||
|
||||
// We now have two pages.
|
||||
try std.testing.expect(s.pages.first.? != s.pages.last.?);
|
||||
try std.testing.expectEqual(s.pages.last.?, s.pages.first.?.next);
|
||||
}
|
||||
|
||||
// We use almost all grapheme alloc capacity with a grapheme in the final
|
||||
// row of the first page, and do the same on the first row of the second
|
||||
// page. We also mark the row as wrapped so that when we resize with more
|
||||
// cols the row unwraps and we have a single row that requires almost two
|
||||
// times the base grapheme alloc capacity.
|
||||
//
|
||||
// This forces the reflow to increase capacity.
|
||||
//
|
||||
// +--+ = PAGE 0
|
||||
// : :
|
||||
// | X… <- where X is a grapheme which uses almost all the capacity.
|
||||
// +--+
|
||||
// +--+ = PAGE 1
|
||||
// …X | <- X here also almost hits grapheme cap.
|
||||
// +--+
|
||||
|
||||
// Almost hit grapheme alloc cap in bottom right of first page.
|
||||
// Mark the final row as wrapped.
|
||||
{
|
||||
const page = &s.pages.first.?.data;
|
||||
const rac = page.getRowAndCell(page.size.cols - 1, page.size.rows - 1);
|
||||
rac.row.wrap = true;
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = 'X' },
|
||||
};
|
||||
try page.setGraphemes(
|
||||
rac.row,
|
||||
rac.cell,
|
||||
&@as(
|
||||
[
|
||||
@divFloor(
|
||||
pagepkg.grapheme_bytes_default - 1,
|
||||
@sizeOf(u21),
|
||||
)
|
||||
]u21,
|
||||
@splat('a'),
|
||||
),
|
||||
);
|
||||
try std.testing.expectError(
|
||||
error.OutOfMemory,
|
||||
page.grapheme_alloc.alloc(
|
||||
u21,
|
||||
page.memory,
|
||||
16,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Almost hit grapheme alloc cap in top left of second page.
|
||||
// Mark the first row as a wrap continuation.
|
||||
{
|
||||
const page = &s.pages.last.?.data;
|
||||
const rac = page.getRowAndCell(0, 0);
|
||||
rac.row.wrap = true;
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = 'X' },
|
||||
};
|
||||
try page.setGraphemes(
|
||||
rac.row,
|
||||
rac.cell,
|
||||
&@as(
|
||||
[
|
||||
@divFloor(
|
||||
pagepkg.grapheme_bytes_default - 1,
|
||||
@sizeOf(u21),
|
||||
)
|
||||
]u21,
|
||||
@splat('a'),
|
||||
),
|
||||
);
|
||||
try std.testing.expectError(
|
||||
error.OutOfMemory,
|
||||
page.grapheme_alloc.alloc(
|
||||
u21,
|
||||
page.memory,
|
||||
16,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Resize to 1 column wider, unwrapping the row.
|
||||
try s.resize(.{ .cols = s.cols + 1, .reflow = true });
|
||||
}
|
||||
|
||||
test "PageList resize reflow exceeds style memory forcing capacity increase" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, pagepkg.std_capacity.styles - 1, 10, 0);
|
||||
defer s.deinit();
|
||||
try testing.expectEqual(@as(usize, 1), s.totalPages());
|
||||
|
||||
// Grow to the capacity of the first page and add
|
||||
// one more row so that we have two pages total.
|
||||
{
|
||||
const page = &s.pages.first.?.data;
|
||||
page.pauseIntegrityChecks(true);
|
||||
for (page.size.rows..page.capacity.rows) |_| {
|
||||
_ = try s.grow();
|
||||
}
|
||||
page.pauseIntegrityChecks(false);
|
||||
try testing.expectEqual(@as(usize, 1), s.totalPages());
|
||||
try s.growRows(1);
|
||||
try testing.expectEqual(@as(usize, 2), s.totalPages());
|
||||
|
||||
// We now have two pages.
|
||||
try std.testing.expect(s.pages.first.? != s.pages.last.?);
|
||||
try std.testing.expectEqual(s.pages.last.?, s.pages.first.?.next);
|
||||
}
|
||||
|
||||
// Give each cell in the final row of the first page a unique style.
|
||||
// Mark the final row as wrapped.
|
||||
{
|
||||
const page = &s.pages.first.?.data;
|
||||
for (0..s.cols) |x| {
|
||||
const id = page.styles.add(
|
||||
page.memory,
|
||||
.{
|
||||
.bg_color = .{ .rgb = .{
|
||||
.r = @truncate(x),
|
||||
.g = @truncate(x >> 8),
|
||||
.b = @truncate(x >> 16),
|
||||
} },
|
||||
},
|
||||
) catch break;
|
||||
|
||||
const rac = page.getRowAndCell(x, page.size.rows - 1);
|
||||
rac.row.wrap = true;
|
||||
rac.row.styled = true;
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = 'X' },
|
||||
.style_id = id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Do the same for the first row of the second page.
|
||||
// Mark the first row as a wrap continuation.
|
||||
{
|
||||
const page = &s.pages.last.?.data;
|
||||
for (0..s.cols) |x| {
|
||||
const id = page.styles.add(
|
||||
page.memory,
|
||||
.{
|
||||
.fg_color = .{ .rgb = .{
|
||||
.r = @truncate(x),
|
||||
.g = @truncate(x >> 8),
|
||||
.b = @truncate(x >> 16),
|
||||
} },
|
||||
},
|
||||
) catch break;
|
||||
|
||||
const rac = page.getRowAndCell(x, 0);
|
||||
rac.row.wrap_continuation = true;
|
||||
rac.row.styled = true;
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = 'X' },
|
||||
.style_id = id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Resize to twice as wide, fully unwrapping the row.
|
||||
try s.resize(.{ .cols = s.cols * 2, .reflow = true });
|
||||
}
|
||||
|
||||
test "PageList resize reflow more cols unwrap wide spacer head" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
|
@ -7767,6 +8136,7 @@ test "PageList resize reflow less cols wrapped rows with graphemes" {
|
|||
try testing.expectEqual(@as(u21, 'A'), cps[0]);
|
||||
}
|
||||
}
|
||||
|
||||
test "PageList resize reflow less cols cursor in wrapped row" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -108,28 +108,59 @@ pub fn BitmapAllocator(comptime chunk_size: comptime_int) type {
|
|||
const chunks = self.chunks.ptr(base);
|
||||
const chunk_idx = @divExact(@intFromPtr(slice.ptr) - @intFromPtr(chunks), chunk_size);
|
||||
|
||||
// From the chunk index, we can find the starting bitmap index
|
||||
// and the bit within the last bitmap.
|
||||
var bitmap_idx = @divFloor(chunk_idx, 64);
|
||||
const bitmap_bit = chunk_idx % 64;
|
||||
const bitmaps = self.bitmap.ptr(base);
|
||||
|
||||
// If our chunk count is over 64 then we need to handle the
|
||||
// case where we have to mark multiple bitmaps.
|
||||
if (chunk_count > 64) {
|
||||
const bitmaps_full = @divFloor(chunk_count, 64);
|
||||
for (0..bitmaps_full) |i| bitmaps[bitmap_idx + i] = std.math.maxInt(u64);
|
||||
bitmap_idx += bitmaps_full;
|
||||
// Current bitmap index.
|
||||
var i: usize = @divFloor(chunk_idx, 64);
|
||||
// Number of chunks we still have to mark as free.
|
||||
var rem: usize = chunk_count;
|
||||
|
||||
// Mark any bits in the starting bitmap that need to be marked.
|
||||
{
|
||||
// Bit index.
|
||||
const bit = chunk_idx % 64;
|
||||
// Number of bits we need to mark in this bitmap.
|
||||
const bits = @min(rem, 64 - bit);
|
||||
|
||||
bitmaps[i] |= ~@as(u64, 0) >> @intCast(64 - bits) << @intCast(bit);
|
||||
rem -= bits;
|
||||
}
|
||||
|
||||
// Set the bitmap to mark the chunks as free. Note we have to
|
||||
// do chunk_count % 64 to handle the case where our chunk count
|
||||
// is using multiple bitmaps.
|
||||
const bitmap = &bitmaps[bitmap_idx];
|
||||
for (0..chunk_count % 64) |i| {
|
||||
const mask = @as(u64, 1) << @intCast(bitmap_bit + i);
|
||||
bitmap.* |= mask;
|
||||
// Mark any full bitmaps worth of bits that need to be marked.
|
||||
i += 1;
|
||||
while (rem > 64) : (i += 1) {
|
||||
bitmaps[i] = std.math.maxInt(u64);
|
||||
rem -= 64;
|
||||
}
|
||||
|
||||
// Mark any bits at the start of this last bitmap if it needs it.
|
||||
if (rem > 0) {
|
||||
bitmaps[i] |= ~@as(u64, 0) >> @intCast(64 - rem);
|
||||
}
|
||||
}
|
||||
|
||||
/// For testing only.
|
||||
fn isAllocated(self: *Self, base: anytype, slice: anytype) bool {
|
||||
comptime assert(@import("builtin").is_test);
|
||||
|
||||
const bytes = std.mem.sliceAsBytes(slice);
|
||||
const aligned_len = std.mem.alignForward(usize, bytes.len, chunk_size);
|
||||
const chunk_count = @divExact(aligned_len, chunk_size);
|
||||
|
||||
const chunks = self.chunks.ptr(base);
|
||||
const chunk_idx = @divExact(@intFromPtr(slice.ptr) - @intFromPtr(chunks), chunk_size);
|
||||
|
||||
const bitmaps = self.bitmap.ptr(base);
|
||||
|
||||
for (chunk_idx..chunk_idx + chunk_count) |i| {
|
||||
const bitmap = @divFloor(i, bitmap_bit_size);
|
||||
const bit = i % bitmap_bit_size;
|
||||
if (bitmaps[bitmap] & (@as(u64, 1) << @intCast(bit)) != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// For debugging
|
||||
|
|
@ -188,50 +219,56 @@ fn findFreeChunks(bitmaps: []u64, n: usize) ?usize {
|
|||
// I'm not a bit twiddling expert. Perhaps even SIMD could be used here
|
||||
// but unsure. Contributor friendly: let's benchmark and improve this!
|
||||
|
||||
// Large chunks require special handling. In this case we look for
|
||||
// divFloor sequential chunks that are maxInt, then look for the mod
|
||||
// normally in the next bitmap.
|
||||
// Large chunks require special handling.
|
||||
if (n > @bitSizeOf(u64)) {
|
||||
const div = @divFloor(n, @bitSizeOf(u64));
|
||||
const mod = n % @bitSizeOf(u64);
|
||||
var seq: usize = 0;
|
||||
for (bitmaps, 0..) |*bitmap, idx| {
|
||||
// If we aren't fully empty then reset the sequence
|
||||
if (bitmap.* != std.math.maxInt(u64)) {
|
||||
seq = 0;
|
||||
var i: usize = 0;
|
||||
search: while (i < bitmaps.len) {
|
||||
// Number of chunks available at the end of this bitmap.
|
||||
const prefix = @clz(~bitmaps[i]);
|
||||
|
||||
// If there are no chunks available at the end of this bitmap
|
||||
// then we can't start in it, so we'll try the next one.
|
||||
if (prefix == 0) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we haven't reached the sequence count we're looking for
|
||||
// then add one and continue, we're still accumulating blanks.
|
||||
if (seq != div) {
|
||||
seq += 1;
|
||||
if (seq != div or mod > 0) continue;
|
||||
// Starting position if we manage to find the span we need here.
|
||||
const start_bitmap = i;
|
||||
const start_bit = 64 - prefix;
|
||||
|
||||
// The remaining number of sequential free chunks we need to find.
|
||||
var rem: usize = n - prefix;
|
||||
|
||||
i += 1;
|
||||
while (rem > 64) : (i += 1) {
|
||||
// We ran out of bitmaps, there's no sufficiently large gap.
|
||||
if (i >= bitmaps.len) return null;
|
||||
|
||||
// There's more than 64 remaining chunks and this bitmap has
|
||||
// content in it, so we try starting again with this bitmap.
|
||||
if (bitmaps[i] != std.math.maxInt(u64)) continue :search;
|
||||
|
||||
// This bitmap is completely free, we can subtract 64 from
|
||||
// our remaining number.
|
||||
rem -= 64;
|
||||
}
|
||||
|
||||
// We've reached the seq count see if this has mod starting empty
|
||||
// blanks.
|
||||
if (mod > 0) {
|
||||
const final = @as(u64, std.math.maxInt(u64)) >> @intCast(64 - mod);
|
||||
if (bitmap.* & final == 0) {
|
||||
// No blanks, reset.
|
||||
seq = 0;
|
||||
continue;
|
||||
}
|
||||
// If the number of available chunks at the start of this bitmap
|
||||
// is less than the remaining required, we have to try again.
|
||||
if (@ctz(~bitmaps[i]) < rem) continue;
|
||||
|
||||
bitmap.* ^= final;
|
||||
const suffix = (n - prefix) % 64;
|
||||
|
||||
// Found! Mark everything between our start and end as full.
|
||||
bitmaps[start_bitmap] ^= ~@as(u64, 0) >> @intCast(start_bit) << @intCast(start_bit);
|
||||
const full_bitmaps = @divFloor(n - prefix - suffix, 64);
|
||||
for (bitmaps[start_bitmap + 1 ..][0..full_bitmaps]) |*bitmap| {
|
||||
bitmap.* = 0;
|
||||
}
|
||||
if (suffix > 0) bitmaps[i] ^= ~@as(u64, 0) >> @intCast(64 - suffix);
|
||||
|
||||
// Found! Set all in our sequence to full and mask our final.
|
||||
// The "zero_mod" modifier below handles the case where we have
|
||||
// a perfectly divisible number of chunks so we don't have to
|
||||
// mark the trailing bitmap.
|
||||
const zero_mod = @intFromBool(mod == 0);
|
||||
const start_idx = idx - (seq - zero_mod);
|
||||
const end_idx = idx + zero_mod;
|
||||
for (start_idx..end_idx) |i| bitmaps[i] = 0;
|
||||
|
||||
return (start_idx * 64);
|
||||
return start_bitmap * 64 + start_bit;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
@ -349,18 +386,18 @@ test "findFreeChunks larger than 64 chunks not at beginning" {
|
|||
};
|
||||
const idx = findFreeChunks(&bitmaps, 65).?;
|
||||
try testing.expectEqual(
|
||||
0b11111111_00000000_00000000_00000000_00000000_00000000_00000000_00000000,
|
||||
0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000,
|
||||
bitmaps[0],
|
||||
);
|
||||
try testing.expectEqual(
|
||||
0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000,
|
||||
0b11111110_00000000_00000000_00000000_00000000_00000000_00000000_00000000,
|
||||
bitmaps[1],
|
||||
);
|
||||
try testing.expectEqual(
|
||||
0b11111111_11111111_11111111_11111111_11111111_11111111_11111111_11111110,
|
||||
0b11111111_11111111_11111111_11111111_11111111_11111111_11111111_11111111,
|
||||
bitmaps[2],
|
||||
);
|
||||
try testing.expectEqual(@as(usize, 64), idx);
|
||||
try testing.expectEqual(@as(usize, 56), idx);
|
||||
}
|
||||
|
||||
test "findFreeChunks larger than 64 chunks exact" {
|
||||
|
|
@ -483,3 +520,438 @@ test "BitmapAllocator alloc large" {
|
|||
ptr[0] = 'A';
|
||||
bm.free(buf, ptr);
|
||||
}
|
||||
|
||||
test "BitmapAllocator alloc and free one bitmap" {
|
||||
const Alloc = BitmapAllocator(1);
|
||||
// Capacity such that we'll have 3 bitmaps.
|
||||
const cap = Alloc.bitmap_bit_size * 3;
|
||||
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const layout = Alloc.layout(cap);
|
||||
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
|
||||
defer alloc.free(buf);
|
||||
|
||||
var bm = Alloc.init(.init(buf), layout);
|
||||
|
||||
// Allocate exactly one bitmap worth of bytes.
|
||||
const slice = try bm.alloc(u8, buf, Alloc.bitmap_bit_size);
|
||||
try testing.expectEqual(Alloc.bitmap_bit_size, slice.len);
|
||||
|
||||
@memset(slice, 0x11);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([Alloc.bitmap_bit_size]u8, @splat(0x11)),
|
||||
slice,
|
||||
);
|
||||
|
||||
// Free it
|
||||
try testing.expect(bm.isAllocated(buf, slice));
|
||||
bm.free(buf, slice);
|
||||
try testing.expect(!bm.isAllocated(buf, slice));
|
||||
|
||||
// All of our bitmaps should be free.
|
||||
try testing.expectEqualSlices(
|
||||
u64,
|
||||
&@as([3]u64, @splat(~@as(u64, 0))),
|
||||
bm.bitmap.ptr(buf)[0..3],
|
||||
);
|
||||
}
|
||||
|
||||
test "BitmapAllocator alloc and free half bitmap" {
|
||||
const Alloc = BitmapAllocator(1);
|
||||
// Capacity such that we'll have 3 bitmaps.
|
||||
const cap = Alloc.bitmap_bit_size * 3;
|
||||
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const layout = Alloc.layout(cap);
|
||||
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
|
||||
defer alloc.free(buf);
|
||||
|
||||
var bm = Alloc.init(.init(buf), layout);
|
||||
|
||||
// Allocate exactly half a bitmap worth of bytes.
|
||||
const slice = try bm.alloc(u8, buf, Alloc.bitmap_bit_size / 2);
|
||||
try testing.expectEqual(Alloc.bitmap_bit_size / 2, slice.len);
|
||||
|
||||
@memset(slice, 0x11);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([Alloc.bitmap_bit_size / 2]u8, @splat(0x11)),
|
||||
slice,
|
||||
);
|
||||
|
||||
// Free it
|
||||
try testing.expect(bm.isAllocated(buf, slice));
|
||||
bm.free(buf, slice);
|
||||
try testing.expect(!bm.isAllocated(buf, slice));
|
||||
|
||||
// All of our bitmaps should be free.
|
||||
try testing.expectEqualSlices(
|
||||
u64,
|
||||
&@as([3]u64, @splat(~@as(u64, 0))),
|
||||
bm.bitmap.ptr(buf)[0..3],
|
||||
);
|
||||
}
|
||||
|
||||
test "BitmapAllocator alloc and free two half bitmaps" {
|
||||
const Alloc = BitmapAllocator(1);
|
||||
// Capacity such that we'll have 3 bitmaps.
|
||||
const cap = Alloc.bitmap_bit_size * 3;
|
||||
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const layout = Alloc.layout(cap);
|
||||
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
|
||||
defer alloc.free(buf);
|
||||
|
||||
var bm = Alloc.init(.init(buf), layout);
|
||||
|
||||
// Allocate exactly one bitmap worth of bytes across two allocations.
|
||||
const slice = try bm.alloc(u8, buf, Alloc.bitmap_bit_size / 2);
|
||||
try testing.expectEqual(Alloc.bitmap_bit_size / 2, slice.len);
|
||||
|
||||
@memset(slice, 0x11);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([Alloc.bitmap_bit_size / 2]u8, @splat(0x11)),
|
||||
slice,
|
||||
);
|
||||
|
||||
const slice2 = try bm.alloc(u8, buf, Alloc.bitmap_bit_size / 2);
|
||||
try testing.expectEqual(Alloc.bitmap_bit_size / 2, slice2.len);
|
||||
|
||||
@memset(slice2, 0x22);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([Alloc.bitmap_bit_size / 2]u8, @splat(0x22)),
|
||||
slice2,
|
||||
);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([Alloc.bitmap_bit_size / 2]u8, @splat(0x11)),
|
||||
slice,
|
||||
);
|
||||
|
||||
// Free them
|
||||
try testing.expect(bm.isAllocated(buf, slice2));
|
||||
bm.free(buf, slice2);
|
||||
try testing.expect(!bm.isAllocated(buf, slice2));
|
||||
try testing.expect(bm.isAllocated(buf, slice));
|
||||
bm.free(buf, slice);
|
||||
try testing.expect(!bm.isAllocated(buf, slice));
|
||||
|
||||
// All of our bitmaps should be free.
|
||||
try testing.expectEqualSlices(
|
||||
u64,
|
||||
&@as([3]u64, @splat(~@as(u64, 0))),
|
||||
bm.bitmap.ptr(buf)[0..3],
|
||||
);
|
||||
}
|
||||
|
||||
test "BitmapAllocator alloc and free 1.5 bitmaps" {
|
||||
const Alloc = BitmapAllocator(1);
|
||||
// Capacity such that we'll have 3 bitmaps.
|
||||
const cap = Alloc.bitmap_bit_size * 3;
|
||||
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const layout = Alloc.layout(cap);
|
||||
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
|
||||
defer alloc.free(buf);
|
||||
|
||||
var bm = Alloc.init(.init(buf), layout);
|
||||
|
||||
// Allocate exactly 1.5 bitmaps worth of bytes.
|
||||
const slice = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 2);
|
||||
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 2, slice.len);
|
||||
|
||||
@memset(slice, 0x11);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([3 * Alloc.bitmap_bit_size / 2]u8, @splat(0x11)),
|
||||
slice,
|
||||
);
|
||||
|
||||
// Free them
|
||||
try testing.expect(bm.isAllocated(buf, slice));
|
||||
bm.free(buf, slice);
|
||||
try testing.expect(!bm.isAllocated(buf, slice));
|
||||
|
||||
// All of our bitmaps should be free.
|
||||
try testing.expectEqualSlices(
|
||||
u64,
|
||||
&@as([3]u64, @splat(~@as(u64, 0))),
|
||||
bm.bitmap.ptr(buf)[0..3],
|
||||
);
|
||||
}
|
||||
|
||||
test "BitmapAllocator alloc and free two 1.5 bitmaps" {
|
||||
const Alloc = BitmapAllocator(1);
|
||||
// Capacity such that we'll have 3 bitmaps.
|
||||
const cap = Alloc.bitmap_bit_size * 3;
|
||||
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const layout = Alloc.layout(cap);
|
||||
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
|
||||
defer alloc.free(buf);
|
||||
|
||||
var bm = Alloc.init(.init(buf), layout);
|
||||
|
||||
// Allocate exactly 3 bitmaps worth of bytes across two allocations.
|
||||
const slice = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 2);
|
||||
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 2, slice.len);
|
||||
|
||||
@memset(slice, 0x11);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([3 * Alloc.bitmap_bit_size / 2]u8, @splat(0x11)),
|
||||
slice,
|
||||
);
|
||||
|
||||
const slice2 = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 2);
|
||||
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 2, slice2.len);
|
||||
|
||||
@memset(slice2, 0x22);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([3 * Alloc.bitmap_bit_size / 2]u8, @splat(0x22)),
|
||||
slice2,
|
||||
);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([3 * Alloc.bitmap_bit_size / 2]u8, @splat(0x11)),
|
||||
slice,
|
||||
);
|
||||
|
||||
// Free them
|
||||
try testing.expect(bm.isAllocated(buf, slice2));
|
||||
bm.free(buf, slice2);
|
||||
try testing.expect(!bm.isAllocated(buf, slice2));
|
||||
try testing.expect(bm.isAllocated(buf, slice));
|
||||
bm.free(buf, slice);
|
||||
try testing.expect(!bm.isAllocated(buf, slice));
|
||||
|
||||
// All of our bitmaps should be free.
|
||||
try testing.expectEqualSlices(
|
||||
u64,
|
||||
&@as([3]u64, @splat(~@as(u64, 0))),
|
||||
bm.bitmap.ptr(buf)[0..3],
|
||||
);
|
||||
}
|
||||
|
||||
test "BitmapAllocator alloc and free 1.5 bitmaps offset by 0.75" {
|
||||
const Alloc = BitmapAllocator(1);
|
||||
// Capacity such that we'll have 3 bitmaps.
|
||||
const cap = Alloc.bitmap_bit_size * 3;
|
||||
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const layout = Alloc.layout(cap);
|
||||
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
|
||||
defer alloc.free(buf);
|
||||
|
||||
var bm = Alloc.init(.init(buf), layout);
|
||||
|
||||
// Allocate three quarters of a bitmap first.
|
||||
const slice = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 4);
|
||||
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 4, slice.len);
|
||||
|
||||
@memset(slice, 0x11);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x11)),
|
||||
slice,
|
||||
);
|
||||
|
||||
// Then a 1.5 bitmap sized allocation, so that it spans
|
||||
// from 0.75 to 2.25, occupying bits in 3 different bitmaps.
|
||||
const slice2 = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 2);
|
||||
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 2, slice2.len);
|
||||
|
||||
@memset(slice2, 0x22);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([3 * Alloc.bitmap_bit_size / 2]u8, @splat(0x22)),
|
||||
slice2,
|
||||
);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x11)),
|
||||
slice,
|
||||
);
|
||||
|
||||
// Free them
|
||||
try testing.expect(bm.isAllocated(buf, slice2));
|
||||
bm.free(buf, slice2);
|
||||
try testing.expect(!bm.isAllocated(buf, slice2));
|
||||
try testing.expect(bm.isAllocated(buf, slice));
|
||||
bm.free(buf, slice);
|
||||
try testing.expect(!bm.isAllocated(buf, slice));
|
||||
|
||||
// All of our bitmaps should be free.
|
||||
try testing.expectEqualSlices(
|
||||
u64,
|
||||
&@as([3]u64, @splat(~@as(u64, 0))),
|
||||
bm.bitmap.ptr(buf)[0..3],
|
||||
);
|
||||
}
|
||||
|
||||
test "BitmapAllocator alloc and free three 0.75 bitmaps" {
|
||||
const Alloc = BitmapAllocator(1);
|
||||
// Capacity such that we'll have 3 bitmaps.
|
||||
const cap = Alloc.bitmap_bit_size * 3;
|
||||
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const layout = Alloc.layout(cap);
|
||||
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
|
||||
defer alloc.free(buf);
|
||||
|
||||
var bm = Alloc.init(.init(buf), layout);
|
||||
|
||||
// Allocate three quarters of a bitmap three times.
|
||||
const slice = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 4);
|
||||
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 4, slice.len);
|
||||
|
||||
@memset(slice, 0x11);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x11)),
|
||||
slice,
|
||||
);
|
||||
|
||||
const slice2 = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 4);
|
||||
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 4, slice2.len);
|
||||
|
||||
@memset(slice2, 0x22);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x22)),
|
||||
slice2,
|
||||
);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x11)),
|
||||
slice,
|
||||
);
|
||||
|
||||
const slice3 = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 4);
|
||||
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 4, slice3.len);
|
||||
|
||||
@memset(slice3, 0x33);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x33)),
|
||||
slice3,
|
||||
);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x22)),
|
||||
slice2,
|
||||
);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x11)),
|
||||
slice,
|
||||
);
|
||||
|
||||
// Free them
|
||||
try testing.expect(bm.isAllocated(buf, slice2));
|
||||
bm.free(buf, slice2);
|
||||
try testing.expect(!bm.isAllocated(buf, slice2));
|
||||
try testing.expect(bm.isAllocated(buf, slice));
|
||||
bm.free(buf, slice);
|
||||
try testing.expect(!bm.isAllocated(buf, slice));
|
||||
try testing.expect(bm.isAllocated(buf, slice3));
|
||||
bm.free(buf, slice3);
|
||||
try testing.expect(!bm.isAllocated(buf, slice3));
|
||||
|
||||
// All of our bitmaps should be free.
|
||||
try testing.expectEqualSlices(
|
||||
u64,
|
||||
&@as([3]u64, @splat(~@as(u64, 0))),
|
||||
bm.bitmap.ptr(buf)[0..3],
|
||||
);
|
||||
}
|
||||
|
||||
test "BitmapAllocator alloc and free two 1.5 bitmaps offset 0.75" {
|
||||
const Alloc = BitmapAllocator(1);
|
||||
// Capacity such that we'll have 4 bitmaps.
|
||||
const cap = Alloc.bitmap_bit_size * 4;
|
||||
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const layout = Alloc.layout(cap);
|
||||
const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size);
|
||||
defer alloc.free(buf);
|
||||
|
||||
var bm = Alloc.init(.init(buf), layout);
|
||||
|
||||
// First allocate a 0.75 bitmap
|
||||
const slice = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 4);
|
||||
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 4, slice.len);
|
||||
|
||||
@memset(slice, 0x11);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x11)),
|
||||
slice,
|
||||
);
|
||||
|
||||
// Then two 1.5 bitmaps
|
||||
const slice2 = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 2);
|
||||
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 2, slice2.len);
|
||||
|
||||
@memset(slice2, 0x22);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([3 * Alloc.bitmap_bit_size / 2]u8, @splat(0x22)),
|
||||
slice2,
|
||||
);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x11)),
|
||||
slice,
|
||||
);
|
||||
|
||||
const slice3 = try bm.alloc(u8, buf, 3 * Alloc.bitmap_bit_size / 2);
|
||||
try testing.expectEqual(3 * Alloc.bitmap_bit_size / 2, slice3.len);
|
||||
|
||||
@memset(slice3, 0x33);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([3 * Alloc.bitmap_bit_size / 2]u8, @splat(0x33)),
|
||||
slice3,
|
||||
);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([3 * Alloc.bitmap_bit_size / 2]u8, @splat(0x22)),
|
||||
slice2,
|
||||
);
|
||||
try testing.expectEqualSlices(
|
||||
u8,
|
||||
&@as([3 * Alloc.bitmap_bit_size / 4]u8, @splat(0x11)),
|
||||
slice,
|
||||
);
|
||||
|
||||
// Free them
|
||||
try testing.expect(bm.isAllocated(buf, slice2));
|
||||
bm.free(buf, slice2);
|
||||
try testing.expect(!bm.isAllocated(buf, slice2));
|
||||
try testing.expect(bm.isAllocated(buf, slice));
|
||||
bm.free(buf, slice);
|
||||
try testing.expect(!bm.isAllocated(buf, slice));
|
||||
try testing.expect(bm.isAllocated(buf, slice3));
|
||||
bm.free(buf, slice3);
|
||||
try testing.expect(!bm.isAllocated(buf, slice3));
|
||||
|
||||
// All of our bitmaps should be free.
|
||||
try testing.expectEqualSlices(
|
||||
u64,
|
||||
&@as([4]u64, @splat(~@as(u64, 0))),
|
||||
bm.bitmap.ptr(buf)[0..4],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.?);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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").?);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -574,6 +574,18 @@
|
|||
...
|
||||
}
|
||||
|
||||
{
|
||||
pango language
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
fun:calloc
|
||||
fun:g_malloc0
|
||||
fun:pango_language_from_string
|
||||
fun:pango_language_get_default
|
||||
fun:gtk_get_locale_direction
|
||||
fun:gtk_init_check
|
||||
...
|
||||
}
|
||||
{
|
||||
Adwaita Stylesheet Load
|
||||
Memcheck:Leak
|
||||
|
|
|
|||
Loading…
Reference in New Issue