diff --git a/.agents/commands/review-branch b/.agents/commands/review-branch new file mode 100755 index 000000000..edd8bcbd8 --- /dev/null +++ b/.agents/commands/review-branch @@ -0,0 +1,75 @@ +#!/usr/bin/env nu + +# A command to review the changes made in the current Git branch. +# +# IMPORTANT: This command is prompted to NOT write any code and to ONLY +# produce a review summary. You should still be vigilant when running this +# but that is the expected behavior. +# +# The optional `` parameter can be an issue number, PR number, +# or a full GitHub URL to provide additional context. +def main [ + issue?: any, # Optional GitHub issue/PR number or URL for context +] { + let issueContext = if $issue != null { + let data = gh issue view $issue --json author,title,number,body,comments | from json + let comments = if ($data.comments? != null) { + $data.comments | each { |comment| + let author = if ($comment.author?.login? != null) { $comment.author.login } else { "unknown" } + $" +### Comment by ($author) +($comment.body) +" | str trim + } | str join "\n\n" + } else { + "" + } + + $" +## Source Issue: ($data.title) \(#($data.number)\) + +### Description +($data.body) + +### Comments +($comments) +" + } else { + "" + } + + $" +# Branch Review + +Inspect the changes made in this Git branch. Identify any possible issues +and suggest improvements. Do not write code. Explain the problems clearly +and propose a brief plan for addressing them. +($issueContext) +## Your Tasks + +You are an experienced software developer with expertise in code review. + +Review the change history between the current branch and its +base branch. Analyze all relevant code for possible issues, including but +not limited to: + +- Code quality and readability +- Code style that matches or mimics the rest of the codebase +- Potential bugs or logical errors +- Edge cases that may not be handled +- Performance considerations +- Security vulnerabilities +- Backwards compatibility \(if applicable\) +- Test coverage and effectiveness + +For test coverage, consider if the changes are in an area of the codebase +that is testable. If so, check if there are appropriate tests added or +modified. Consider if the code itself should be modified to be more +testable. + +Think deeply about the implications of the changes here and proposed. +Consult the oracle if you have access to it. + +**ONLY CREATE A SUMMARY. DO NOT WRITE ANY CODE.** +" | str trim +} diff --git a/.editorconfig b/.editorconfig index 4e9bec6ce..b59747923 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,6 @@ root = true -[*.{sh,bash,elv}] +[*.{sh,bash,elv,nu}] indent_size = 2 indent_style = space diff --git a/.gitattributes b/.gitattributes index 87f1eb32e..9158b3979 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,11 +4,11 @@ build.zig.zon.json linguist-generated=true vendor/** linguist-vendored website/** linguist-documentation pkg/breakpad/vendor/** linguist-vendored -pkg/cimgui/vendor/** linguist-vendored pkg/glfw/wayland-headers/** linguist-vendored pkg/libintl/config.h linguist-generated=true pkg/libintl/libintl.h linguist-generated=true pkg/simdutf/vendor/** linguist-vendored src/font/nerd_font_attributes.zig linguist-generated=true src/font/nerd_font_codepoint_tables.py linguist-generated=true +src/font/res/** linguist-vendored src/terminal/res/** linguist-vendored diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml new file mode 100644 index 000000000..7c4256e0e --- /dev/null +++ b/.github/workflows/flatpak.yml @@ -0,0 +1,50 @@ +on: + workflow_dispatch: + inputs: + source-run-id: + description: run id of the workflow that generated the artifact + required: true + type: string + source-artifact-id: + description: source tarball built during build-dist + required: true + type: string + +name: Flatpak + +jobs: + build: + if: github.repository == 'ghostty-org/ghostty' + name: "Flatpak" + container: + image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-47 + options: --privileged + strategy: + fail-fast: false + matrix: + variant: + - arch: x86_64 + runner: namespace-profile-ghostty-md + - arch: aarch64 + runner: namespace-profile-ghostty-md-arm64 + runs-on: ${{ matrix.variant.runner }} + steps: + - name: Download Source Tarball Artifacts + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + run-id: ${{ inputs.source-run-id }} + artifact-ids: ${{ inputs.source-artifact-id }} + github-token: ${{ github.token }} + + - name: Extract tarball + run: | + mkdir dist + tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz + + - uses: flatpak/flatpak-github-actions/flatpak-builder@92ae9851ad316786193b1fd3f40c4b51eb5cb101 # v6.6 + with: + bundle: com.mitchellh.ghostty + manifest-path: dist/flatpak/com.mitchellh.ghostty.yml + cache-key: flatpak-builder-${{ github.sha }} + arch: ${{ matrix.variant.arch }} + verbose: true diff --git a/.github/workflows/milestone.yml b/.github/workflows/milestone.yml index 74f2dd7ce..49bba4e6b 100644 --- a/.github/workflows/milestone.yml +++ b/.github/workflows/milestone.yml @@ -15,7 +15,7 @@ jobs: name: Milestone Update steps: - name: Set Milestone for PR - uses: hustcer/milestone-action@dcd6c3742acc1846929c054251c64cccd555a00d # v3.0 + uses: hustcer/milestone-action@ebed8d5daafd855a600d7e665c1b130f06d24130 # v3.1 if: github.event.pull_request.merged == true with: action: bind-pr # `bind-pr` is the default action @@ -24,7 +24,7 @@ jobs: # Bind milestone to closed issue that has a merged PR fix - name: Set Milestone for Issue - uses: hustcer/milestone-action@dcd6c3742acc1846929c054251c64cccd555a00d # v3.0 + uses: hustcer/milestone-action@ebed8d5daafd855a600d7e665c1b130f06d24130 # v3.1 if: github.event.issue.state == 'closed' with: action: bind-issue diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index f928ed5a5..90ce82989 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -39,7 +39,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 with: @@ -47,10 +47,10 @@ jobs: /nix /zig - name: Setup Nix - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" diff --git a/.github/workflows/publish-tag.yml b/.github/workflows/publish-tag.yml index c433e7484..acb1ab1f1 100644 --- a/.github/workflows/publish-tag.yml +++ b/.github/workflows/publish-tag.yml @@ -64,7 +64,7 @@ jobs: mkdir blob mv appcast.xml blob/appcast.xml - name: Upload Appcast to R2 - uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1 + uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4 with: r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }} r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }} diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 82970a065..1342c4db6 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -56,7 +56,7 @@ jobs: fi - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # Important so that build number generation works fetch-depth: 0 @@ -80,7 +80,7 @@ jobs: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 @@ -89,11 +89,11 @@ jobs: /nix /zig - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -113,7 +113,7 @@ jobs: nix develop -c minisign -S -m "ghostty-source.tar.gz" -s minisign.key < minisign.password - name: Upload artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: source-tarball path: |- @@ -132,18 +132,18 @@ jobs: GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }} steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: DeterminateSystems/nix-installer-action@main with: determinate: true - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select - run: sudo xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcode-select -s /Applications/Xcode_26.2.app - name: Xcode Version run: xcodebuild -version @@ -269,7 +269,7 @@ jobs: zip -9 -r --symlinks ../../../ghostty-macos-universal-dsym.zip Ghostty.app.dSYM/ - name: Upload artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: macos path: |- @@ -286,7 +286,7 @@ jobs: curl -sL https://sentry.io/get-cli/ | bash - name: Download macOS Artifacts - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: macos @@ -306,10 +306,10 @@ jobs: GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Download macOS Artifacts - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: macos @@ -340,7 +340,7 @@ jobs: mv appcast_new.xml appcast.xml - name: Upload artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: sparkle path: |- @@ -357,17 +357,17 @@ jobs: GHOSTTY_VERSION: ${{ needs.setup.outputs.version }} steps: - name: Download macOS Artifacts - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: macos - name: Download Sparkle Artifacts - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: sparkle - name: Download Source Tarball Artifacts - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: source-tarball diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index df73198d1..82645c102 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -29,14 +29,14 @@ jobs: commit: ${{ steps.extract_build_info.outputs.commit }} commit_long: ${{ steps.extract_build_info.outputs.commit_long }} steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # Important so that build number generation works fetch-depth: 0 - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -66,7 +66,7 @@ jobs: needs: [setup, build-macos] if: needs.setup.outputs.should_skip != 'true' steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Tip Tag run: | git config user.name "github-actions[bot]" @@ -81,7 +81,7 @@ jobs: env: GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install sentry-cli run: | @@ -104,7 +104,7 @@ jobs: env: GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install sentry-cli run: | @@ -127,7 +127,7 @@ jobs: env: GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install sentry-cli run: | @@ -159,17 +159,17 @@ jobs: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 with: path: | /nix /zig - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -217,7 +217,7 @@ jobs: GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # Important so that build number generation works fetch-depth: 0 @@ -226,13 +226,13 @@ jobs: - uses: DeterminateSystems/nix-installer-action@main with: determinate: true - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select - run: sudo xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcode-select -s /Applications/Xcode_26.2.app - name: Xcode Version run: xcodebuild -version @@ -451,7 +451,7 @@ jobs: GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # Important so that build number generation works fetch-depth: 0 @@ -460,13 +460,13 @@ jobs: - uses: DeterminateSystems/nix-installer-action@main with: determinate: true - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select - run: sudo xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcode-select -s /Applications/Xcode_26.2.app - name: Xcode Version run: xcodebuild -version @@ -635,7 +635,7 @@ jobs: GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # Important so that build number generation works fetch-depth: 0 @@ -644,13 +644,13 @@ jobs: - uses: DeterminateSystems/nix-installer-action@main with: determinate: true - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select - run: sudo xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcode-select -s /Applications/Xcode_26.2.app - name: Xcode Version run: xcodebuild -version diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index 641bbcca6..6fc7e0fb4 100644 --- a/.github/workflows/snap.yml +++ b/.github/workflows/snap.yml @@ -26,7 +26,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Download Source Tarball Artifacts - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: run-id: ${{ inputs.source-run-id }} artifact-ids: ${{ inputs.source-artifact-id }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 20f674bab..abd5901e5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: - build-linux-libghostty - build-nix - build-macos - - build-macos-matrix + - build-macos-freetype - build-snap - build-windows - test @@ -44,7 +44,7 @@ jobs: - test-debian-13 - valgrind - zig-fmt - - flatpak + steps: - id: status name: Determine status @@ -74,7 +74,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 @@ -84,10 +84,10 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -117,7 +117,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 @@ -127,10 +127,10 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -150,7 +150,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 @@ -160,10 +160,10 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -184,7 +184,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 @@ -194,10 +194,10 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -217,6 +217,7 @@ jobs: x86_64-macos, aarch64-linux, x86_64-linux, + x86_64-linux-musl, x86_64-windows, wasm32-freestanding, ] @@ -227,7 +228,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 @@ -237,10 +238,10 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -263,7 +264,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 @@ -273,10 +274,10 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -292,7 +293,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 @@ -302,10 +303,10 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -325,7 +326,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 @@ -335,10 +336,10 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -371,7 +372,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 @@ -381,10 +382,10 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -397,7 +398,7 @@ jobs: - name: Upload artifact id: upload-artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: source-tarball path: |- @@ -409,7 +410,7 @@ jobs: needs: [build-dist, build-snap] steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Trigger Snap workflow run: | @@ -421,24 +422,42 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + trigger-flatpak: + if: github.event_name != 'pull_request' + runs-on: namespace-profile-ghostty-xsm + needs: [build-dist, build-flatpak] + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Trigger Flatpak workflow + run: | + gh workflow run \ + flatpak.yml \ + --ref ${{ github.ref_name || 'main' }} \ + --field source-run-id=${{ github.run_id }} \ + --field source-artifact-id=${{ needs.build-dist.outputs.artifact-id }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + build-macos: runs-on: namespace-profile-ghostty-macos-tahoe needs: test steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # TODO(tahoe): https://github.com/NixOS/nix/issues/13342 - uses: DeterminateSystems/nix-installer-action@main with: determinate: true - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Xcode Select - run: sudo xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcode-select -s /Applications/Xcode_26.2.app - name: Xcode Version run: xcodebuild -version @@ -464,24 +483,24 @@ jobs: cd macos xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO" - build-macos-matrix: + build-macos-freetype: runs-on: namespace-profile-ghostty-macos-tahoe needs: test steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # TODO(tahoe): https://github.com/NixOS/nix/issues/13342 - uses: DeterminateSystems/nix-installer-action@main with: determinate: true - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Xcode Select - run: sudo xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcode-select -s /Applications/Xcode_26.2.app - name: Xcode Version run: xcodebuild -version @@ -493,18 +512,10 @@ jobs: - name: Test All run: | nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=freetype - nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=coretext - nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=coretext_freetype - nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=coretext_harfbuzz - nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=coretext_noshape - name: Build All run: | nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=freetype - nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=coretext - nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=coretext_freetype - nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=coretext_harfbuzz - nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=coretext_noshape build-windows: runs-on: windows-2022 @@ -514,7 +525,7 @@ jobs: needs: test steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # This could be from a script if we wanted to but inlining here for now # in one place. @@ -585,7 +596,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Get required Zig version id: zig @@ -600,10 +611,10 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -632,7 +643,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 @@ -642,10 +653,10 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -680,7 +691,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 @@ -690,10 +701,10 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -715,7 +726,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 @@ -725,10 +736,10 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -742,19 +753,19 @@ jobs: needs: test steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # TODO(tahoe): https://github.com/NixOS/nix/issues/13342 - uses: DeterminateSystems/nix-installer-action@main with: determinate: true - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Xcode Select - run: sudo xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcode-select -s /Applications/Xcode_26.2.app - name: Xcode Version run: xcodebuild -version @@ -779,7 +790,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 @@ -789,10 +800,10 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -809,17 +820,17 @@ jobs: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 with: path: | /nix /zig - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -833,21 +844,23 @@ jobs: if: github.repository == 'ghostty-org/ghostty' runs-on: namespace-profile-ghostty-xsm timeout-minutes: 60 + permissions: + contents: read env: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 with: path: | /nix /zig - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -855,6 +868,8 @@ jobs: useDaemon: false # sometimes fails on short jobs - name: pinact check run: nix develop -c pinact run --check + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} prettier: if: github.repository == 'ghostty-org/ghostty' @@ -864,17 +879,17 @@ jobs: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 with: path: | /nix /zig - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -891,17 +906,17 @@ jobs: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 with: path: | /nix /zig - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -918,17 +933,17 @@ jobs: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 with: path: | /nix /zig - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -945,17 +960,17 @@ jobs: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 with: path: | /nix /zig - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -967,8 +982,6 @@ jobs: --check-sourced \ --color=always \ --severity=warning \ - --shell=bash \ - --external-sources \ $(find . \( -name "*.sh" -o -name "*.bash" \) -type f ! -path "./zig-out/*" ! -path "./macos/build/*" ! -path "./.git/*" | sort) translations: @@ -979,17 +992,17 @@ jobs: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 with: path: | /nix /zig - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -1006,17 +1019,17 @@ jobs: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 with: path: | /nix /zig - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -1040,7 +1053,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 @@ -1050,10 +1063,10 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -1072,10 +1085,10 @@ jobs: uses: namespacelabs/nscloud-setup@d1c625762f7c926a54bd39252efff0705fd11c64 # v0.0.10 - name: Configure Namespace powered Buildx - uses: namespacelabs/nscloud-setup-buildx-action@91c2e6537780e3b092cb8476406be99a8f91bd5e # v0.0.20 + uses: namespacelabs/nscloud-setup-buildx-action@a7e525416136ee2842da3c800e7067b72a27200e # v0.0.21 - name: Download Source Tarball Artifacts - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: source-tarball @@ -1092,32 +1105,6 @@ jobs: build-args: | DISTRO_VERSION=13 - flatpak: - if: github.repository == 'ghostty-org/ghostty' - name: "Flatpak" - container: - image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-47 - options: --privileged - strategy: - fail-fast: false - matrix: - variant: - - arch: x86_64 - runner: namespace-profile-ghostty-md - - arch: aarch64 - runner: namespace-profile-ghostty-md-arm64 - runs-on: ${{ matrix.variant.runner }} - needs: test - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: flatpak/flatpak-github-actions/flatpak-builder@92ae9851ad316786193b1fd3f40c4b51eb5cb101 # v6.6 - with: - bundle: com.mitchellh.ghostty - manifest-path: flatpak/com.mitchellh.ghostty.yml - 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 @@ -1128,7 +1115,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21 @@ -1138,10 +1125,10 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -1167,7 +1154,7 @@ jobs: # timeout-minutes: 10 # steps: # - name: Checkout Ghostty - # uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + # uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # # - name: Start SSH # run: | diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index ca65c2a21..9395f19e1 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -17,7 +17,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 @@ -29,24 +29,41 @@ jobs: /zig - name: Setup Nix - uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4 + uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - - name: Run zig fetch - id: zig_fetch + - name: Download colorschemes + id: download env: GH_TOKEN: ${{ github.token }} run: | # Get the latest release from iTerm2-Color-Schemes RELEASE_INFO=$(gh api repos/mbadolato/iTerm2-Color-Schemes/releases/latest) TAG_NAME=$(echo "$RELEASE_INFO" | jq -r '.tag_name') - nix develop -c zig fetch --save="iterm2_themes" "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/${TAG_NAME}/ghostty-themes.tgz" + FILENAME="ghostty-themes-${TAG_NAME}.tgz" + mkdir -p upload + curl -L -o "upload/${FILENAME}" "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/${TAG_NAME}/ghostty-themes.tgz" echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT + echo "filename=$FILENAME" >> $GITHUB_OUTPUT + + - name: Upload to R2 + uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4 + with: + r2-account-id: ${{ secrets.CF_R2_DEPS_ACCOUNT_ID }} + r2-access-key-id: ${{ secrets.CF_R2_DEPS_AWS_KEY }} + r2-secret-access-key: ${{ secrets.CF_R2_DEPS_SECRET_KEY }} + r2-bucket: ghostty-deps + source-dir: upload + destination-dir: ./ + + - name: Run zig fetch + run: | + nix develop -c zig fetch --save="iterm2_themes" "https://deps.files.ghostty.org/${{ steps.download.outputs.filename }}" - name: Update zig cache hash run: | @@ -62,7 +79,7 @@ jobs: run: nix build .#ghostty - name: Create pull request - uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: title: Update iTerm2 colorschemes base: main @@ -75,5 +92,5 @@ jobs: build.zig.zon.json flatpak/zig-packages.json body: | - Upstream release: https://github.com/mbadolato/iTerm2-Color-Schemes/releases/tag/${{ steps.zig_fetch.outputs.tag_name }} + Upstream release: https://github.com/mbadolato/iTerm2-Color-Schemes/releases/tag/${{ steps.download.outputs.tag_name }} labels: dependencies diff --git a/.gitignore b/.gitignore index e451b171a..e521f8851 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ zig-cache/ .zig-cache/ zig-out/ /result* +/.nixos-test-history example/*.wasm test/ghostty test/cases/**/*.actual.png diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 000000000..919cc175d --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,8 @@ +# ShellCheck +# https://github.com/koalaman/shellcheck/wiki/Directive#shellcheckrc-file + +# Allow opening any 'source'd file, even if not specified as input +external-sources=true + +# Assume bash by default +shell=bash diff --git a/AGENTS.md b/AGENTS.md index a3e752816..dc2b47a70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,4 +30,5 @@ A file for [guiding coding agents](https://agents.md/). - Do not use `xcodebuild` - Use `zig build` to build the macOS app and any shared Zig code +- Use `zig build run` to build and run the macOS app - Run Xcode tests using `zig build test` diff --git a/AI_POLICY.md b/AI_POLICY.md new file mode 100644 index 000000000..1ed0006d4 --- /dev/null +++ b/AI_POLICY.md @@ -0,0 +1,69 @@ +# AI Usage Policy + +The Ghostty project has strict rules for AI usage: + +- **All AI usage in any form must be disclosed.** You must state + the tool you used (e.g. Claude Code, Cursor, Amp) along with + the extent that the work was AI-assisted. + +- **Pull requests created in any way by AI can only be for accepted issues.** + Drive-by pull requests that do not reference an accepted issue will be + closed. If AI isn't disclosed but a maintainer suspects its use, the + PR will be closed. If you want to share code for a non-accepted issue, + open a discussion or attach it to an existing discussion. + +- **Pull requests created by AI must have been fully verified with + human use.** AI must not create hypothetically correct code that + hasn't been tested. Importantly, you must not allow AI to write + code for platforms or environments you don't have access to manually + test on. + +- **Issues and discussions can use AI assistance but must have a full + human-in-the-loop.** This means that any content generated with AI + must have been reviewed _and edited_ by a human before submission. + AI is very good at being overly verbose and including noise that + distracts from the main point. Humans must do their research and + trim this down. + +- **No AI-generated media is allowed (art, images, videos, audio, etc.).** + Text and code are the only acceptable AI-generated content, per the + other rules in this policy. + +- **Bad AI drivers will be banned and ridiculed in public.** You've + been warned. We love to help junior developers learn and grow, but + if you're interested in that then don't use AI, and we'll help you. + I'm sorry that bad AI drivers have ruined this for you. + +These rules apply only to outside contributions to Ghostty. Maintainers +are exempt from these rules and may use AI tools at their discretion; +they've proven themselves trustworthy to apply good judgment. + +## There are Humans Here + +Please remember that Ghostty is maintained by humans. + +Every discussion, issue, and pull request is read and reviewed by +humans (and sometimes machines, too). It is a boundary point at which +people interact with each other and the work done. It is rude and +disrespectful to approach this boundary with low-effort, unqualified +work, since it puts the burden of validation on the maintainer. + +In a perfect world, AI would produce high-quality, accurate work +every time. But today, that reality depends on the driver of the AI. +And today, most drivers of AI are just not good enough. So, until either +the people get better, the AI gets better, or both, we have to have +strict rules to protect maintainers. + +## AI is Welcome Here + +Ghostty is written with plenty of AI assistance, and many maintainers embrace +AI tools as a productive tool in their workflow. As a project, we welcome +AI as a tool! + +**Our reason for the strict AI policy is not due to an anti-AI stance**, but +instead due to the number of highly unqualified people using AI. It's the +people, not the tools, that are the problem. + +I include this section to be transparent about the project's usage about +AI for people who may disagree with it, and to address the misconception +that this policy is anti-AI in nature. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b4285f42f..41376765b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,61 +13,10 @@ it, please check out our ["Developing Ghostty"](HACKING.md) document as well. > 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 +## AI Usage -> [!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). -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. - -The submitter must have also tested the pull request on all impacted -platforms, and it's **highly discouraged** to code for an unfamiliar platform -with AI assistance alone: if you only have a macOS machine, do **not** ask AI -to write the equivalent GTK code, and vice versa — someone else with more -expertise will eventually get to it and do it for you. - -Even though using AI to generate responses on a PR is allowed when properly -disclosed, **we do not encourage you to do so**. Often, the positive impact -of genuine, responsive human interaction more than makes up for any language -barrier. ❤️ - -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. - -An example of a **problematic** disclosure (not having tested all platforms): - -> I used Amp to code both macOS and GTK UIs, but I have not yet tested -> the GTK UI as I don't have a Linux setup. - -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)! - -When using AI assistance, we expect a fairly high level of accountability -and responsibility from contributors, and expect them to understand the code -that is produced and be able to answer critical questions about it. It -isn't a maintainers job to review a PR so broken that it requires -significant rework to be acceptable, and we **reserve the right to close -these PRs without hesitation**. - -Please be respectful to maintainers and disclose AI assistance. +The Ghostty project has strict rules for AI usage. Please see +the [AI Usage Policy](AI_POLICY.md). **This is very important.** ## Quick Guide @@ -202,3 +151,266 @@ pull request will be accepted with a high degree of certainty. > **Pull requests are NOT a place to discuss feature design.** Please do > not open a WIP pull request to discuss a feature. Instead, use a discussion > and link to your branch. + +# Developer Guide + +> [!NOTE] +> +> **The remainder of this file is dedicated to developers actively +> working on Ghostty.** If you're a user reporting an issue, you can +> ignore the rest of this document. + +## Including and Updating Translations + +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 +key event and ends with text encoding being sent to the pty (it +does not include _rendering_ the text, which is part of the +font or rendering stack). + +If you modify any part of the input stack, you must manually verify +all the following input cases work properly. We unfortunately do +not automate this in any way, but if we can do that one day that'd +save a LOT of grief and time. + +Note: this list may not be exhaustive, I'm still working on it. + +### Linux IME + +IME (Input Method Editors) are a common source of bugs in the input stack, +especially on Linux since there are multiple different IME systems +interacting with different windowing systems and application frameworks +all written by different organizations. + +The following matrix should be tested to ensure that all IME input works +properly: + +1. Wayland, X11 +2. ibus, fcitx, none +3. Dead key input (e.g. Spanish), CJK (e.g. Japanese), Emoji, Unicode Hex +4. ibus versions: 1.5.29, 1.5.30, 1.5.31 (each exhibit slightly different behaviors) + +> [!NOTE] +> +> This is a **work in progress**. I'm still working on this list and it +> is not complete. As I find more test cases, I will add them here. + +#### Dead Key Input + +Set your keyboard layout to "Spanish" (or another layout that uses dead keys). + +1. Launch Ghostty +2. Press `'` +3. Press `a` +4. Verify that `á` is displayed + +Note that the dead key may or may not show a preedit state visually. +For ibus and fcitx it does but for the "none" case it does not. Importantly, +the text should be correct when it is sent to the pty. + +We should also test canceling dead key input: + +1. Launch Ghostty +2. Press `'` +3. Press escape +4. Press `a` +5. Verify that `a` is displayed (no diacritic) + +#### CJK Input + +Configure fcitx or ibus with a keyboard layout like Japanese or Mozc. The +exact layout doesn't matter. + +1. Launch Ghostty +2. Press `Ctrl+Shift` to switch to "Hiragana" +3. On a US physical layout, type: `konn`, you should see `こん` in preedit. +4. Press `Enter` +5. Verify that `こん` is displayed in the terminal. + +We should also test switching input methods while preedit is active, which +should commit the text: + +1. Launch Ghostty +2. Press `Ctrl+Shift` to switch to "Hiragana" +3. On a US physical layout, type: `konn`, you should see `こん` in preedit. +4. Press `Ctrl+Shift` to switch to another layout (any) +5. Verify that `こん` is displayed in the terminal as committed text. + +## Nix Virtual Machines + +Several Nix virtual machine definitions are provided by the project for testing +and developing Ghostty against multiple different Linux desktop environments. + +Running these requires a working Nix installation, either Nix on your +favorite Linux distribution, NixOS, or macOS with nix-darwin installed. Further +requirements for macOS are detailed below. + +VMs should only be run on your local desktop and then powered off when not in +use, which will discard any changes to the VM. + +The VM definitions provide minimal software "out of the box" but additional +software can be installed by using standard Nix mechanisms like `nix run nixpkgs#`. + +### Linux + +1. Check out the Ghostty source and change to the directory. +2. Run `nix run .#`. `` can be any of the VMs defined in the + `nix/vm` directory (without the `.nix` suffix) excluding any file prefixed + with `common` or `create`. +3. The VM will build and then launch. Depending on the speed of your system, this + can take a while, but eventually you should get a new VM window. +4. The Ghostty source directory should be mounted to `/tmp/shared` in the VM. Depending + on what UID and GID of the user that you launched the VM as, `/tmp/shared` _may_ be + writable by the VM user, so be careful! + +### macOS + +1. To run the VMs on macOS you will need to enable the Linux builder in your `nix-darwin` + config. This _should_ be as simple as adding `nix.linux-builder.enable=true` to your + configuration and then rebuilding. See [this](https://nixcademy.com/posts/macos-linux-builder/) + blog post for more information about the Linux builder and how to tune the performance. +2. Once the Linux builder has been enabled, you should be able to follow the Linux instructions + above to launch a VM. + +### Custom VMs + +To easily create a custom VM without modifying the Ghostty source, create a new +directory, then create a file called `flake.nix` with the following text in the +new directory. + +``` +{ + inputs = { + nixpkgs.url = "nixpkgs/nixpkgs-unstable"; + ghostty.url = "github:ghostty-org/ghostty"; + }; + outputs = { + nixpkgs, + ghostty, + ... + }: { + nixosConfigurations.custom-vm = ghostty.create-gnome-vm { + nixpkgs = nixpkgs; + system = "x86_64-linux"; + overlay = ghostty.overlays.releasefast; + # module = ./configuration.nix # also works + module = {pkgs, ...}: { + environment.systemPackages = [ + pkgs.btop + ]; + }; + }; + }; +} +``` + +The custom VM can then be run with a command like this: + +``` +nix run .#nixosConfigurations.custom-vm.config.system.build.vm +``` + +A file named `ghostty.qcow2` will be created that is used to persist any changes +made in the VM. To "reset" the VM to default delete the file and it will be +recreated the next time you run the VM. + +### Contributing new VM definitions + +#### VM Acceptance Criteria + +We welcome the contribution of new VM definitions, as long as they meet the following criteria: + +1. They should be different enough from existing VM definitions that they represent a distinct + user (and developer) experience. +2. There's a significant Ghostty user population that uses a similar environment. +3. The VMs can be built using only packages from the current stable NixOS release. + +#### VM Definition Criteria + +1. VMs should be as minimal as possible so that they build and launch quickly. + Additional software can be added at runtime with a command like `nix run nixpkgs#`. +2. VMs should not expose any services to the network, or run any remote access + software like SSH daemons, VNC or RDP. +3. VMs should auto-login using the "ghostty" user. + +## Nix VM Integration Tests + +Several Nix VM tests are provided by the project for testing Ghostty in a "live" +environment rather than just unit tests. + +Running these requires a working Nix installation, either Nix on your +favorite Linux distribution, NixOS, or macOS with nix-darwin installed. Further +requirements for macOS are detailed below. + +### Linux + +1. Check out the Ghostty source and change to the directory. +2. Run `nix run .#check...driver`. `` should be + `x86_64-linux` or `aarch64-linux` (even on macOS, this launches a Linux + VM, not a macOS one). `` should be one of the tests defined in + `nix/tests.nix`. The test will build and then launch. Depending on the speed + of your system, this can take a while. Eventually though the test should + complete. Hopefully successfully, but if not error messages should be printed + out that can be used to diagnose the issue. +3. To run _all_ of the tests, run `nix flake check`. + +### macOS + +1. To run the VMs on macOS you will need to enable the Linux builder in your `nix-darwin` + config. This _should_ be as simple as adding `nix.linux-builder.enable=true` to your + configuration and then rebuilding. See [this](https://nixcademy.com/posts/macos-linux-builder/) + blog post for more information about the Linux builder and how to tune the performance. +2. Once the Linux builder has been enabled, you should be able to follow the Linux instructions + above to launch a test. + +### Interactively Running Test VMs + +To run a test interactively, run `nix run +.#check...driverInteractive`. This will load a Python console +that can be used to manage the test VMs. In this console run `start_all()` to +start the VM(s). The VMs should boot up and a window should appear showing the +VM's console. + +For more information about the Nix test console, see [the NixOS manual](https://nixos.org/manual/nixos/stable/index.html#sec-call-nixos-test-outside-nixos) + +### SSH Access to Test VMs + +Some test VMs are configured to allow outside SSH access for debugging. To +access the VM, use a command like the following: + +``` +ssh -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null -p 2222 root@192.168.122.1 +ssh -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null -p 2222 ghostty@192.168.122.1 +``` + +The SSH options are important because the SSH host keys will be regenerated +every time the test is started. Without them, your personal SSH known hosts file +will become difficult to manage. The port that is needed to access the VM may +change depending on the test. + +None of the users in the VM have passwords so do not expose these VMs to the Internet. diff --git a/HACKING.md b/HACKING.md index 0a4bbef20..0abb3a2d8 100644 --- a/HACKING.md +++ b/HACKING.md @@ -93,6 +93,36 @@ produced. > may ask you to fix it and close the issue. It isn't a maintainers job to > review a PR so broken that it requires significant rework to be acceptable. +## Logging + +Ghostty can write logs to a number of destinations. On all platforms, logging to +`stderr` is available. Depending on the platform and how Ghostty was launched, +logs sent to `stderr` may be stored by the system and made available for later +retrieval. + +On Linux if Ghostty is launched by the default `systemd` user service, you can use +`journald` to see Ghostty's logs: `journalctl --user --unit app-com.mitchellh.ghostty.service`. + +On macOS logging to the macOS unified log is available and enabled by default. +Use the system `log` CLI to view Ghostty's logs: `sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'`. + +Ghostty's logging can be configured in two ways. The first is by what +optimization level Ghostty is compiled with. If Ghostty is compiled with `Debug` +optimizations debug logs will be output to `stderr`. If Ghostty is compiled with +any other optimization the debug logs will not be output to `stderr`. + +Ghostty also checks the `GHOSTTY_LOG` environment variable. It can be used +to control which destinations receive logs. Ghostty currently defines two +destinations: + +- `stderr` - logging to `stderr`. +- `macos` - logging to macOS's unified log (has no effect on non-macOS platforms). + +Combine values with a comma to enable multiple destinations. Prefix a +destination with `no-` to disable it. Enabling and disabling destinations +can be done at the same time. Setting `GHOSTTY_LOG` to `true` will enable all +destinations. Setting `GHOSTTY_LOG` to `false` will disable all destinations. + ## Linting ### Prettier @@ -134,6 +164,28 @@ alejandra . Make sure your Alejandra version matches the version of Alejandra in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix). +### ShellCheck + +Bash scripts are checked with [ShellCheck](https://www.shellcheck.net/) in CI. + +Nix users can use the following command to run ShellCheck over all of our scripts: + +``` +nix develop -c shellcheck \ + --check-sourced \ + --severity=warning \ + $(find . \( -name "*.sh" -o -name "*.bash" \) -type f ! -path "./zig-out/*" ! -path "./macos/build/*" ! -path "./.git/*" | sort) +``` + +Non-Nix users can [install ShellCheck](https://github.com/koalaman/shellcheck#user-content-installing) and then run: + +``` +shellcheck \ + --check-sourced \ + --severity=warning \ + $(find . \( -name "*.sh" -o -name "*.bash" \) -type f ! -path "./zig-out/*" ! -path "./macos/build/*" ! -path "./.git/*" | sort) +``` + ### Updating the Zig Cache Fixed-Output Derivation Hash The Nix package depends on a [fixed-output diff --git a/build.zig b/build.zig index 5fd611b6c..fa68b91b4 100644 --- a/build.zig +++ b/build.zig @@ -2,6 +2,7 @@ const std = @import("std"); const assert = std.debug.assert; const builtin = @import("builtin"); const buildpkg = @import("src/build/main.zig"); + const appVersion = @import("build.zig.zon").version; const minimumZigVersion = @import("build.zig.zon").minimum_zig_version; diff --git a/build.zig.zon b/build.zig.zon index 20cf44141..d5c06259a 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -15,14 +15,14 @@ }, .vaxis = .{ // rockorager/libvaxis - .url = "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz", + .url = "https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz", .hash = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS", .lazy = true, }, .z2d = .{ // vancluever/z2d - .url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz", - .hash = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T", + .url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz", + .hash = "z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ", .lazy = true, }, .zig_objc = .{ @@ -39,7 +39,7 @@ }, .uucode = .{ // jacobsandlund/uucode - .url = "https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz", + .url = "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz", .hash = "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E", }, .zig_wayland = .{ @@ -50,20 +50,20 @@ }, .zf = .{ // natecraddock/zf - .url = "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz", + .url = "https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz", .hash = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh", .lazy = true, }, .gobject = .{ - // https://github.com/jcollie/ghostty-gobject based on zig_gobject + // https://github.com/ghostty-org/zig-gobject based on zig_gobject // Temporary until we generate them at build time automatically. - .url = "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst", - .hash = "gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV", + .url = "https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst", + .hash = "gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-", .lazy = true, }, // C libs - .cimgui = .{ .path = "./pkg/cimgui", .lazy = true }, + .dcimgui = .{ .path = "./pkg/dcimgui", .lazy = true }, .fontconfig = .{ .path = "./pkg/fontconfig", .lazy = true }, .freetype = .{ .path = "./pkg/freetype", .lazy = true }, .gtk4_layer_shell = .{ .path = "./pkg/gtk4-layer-shell", .lazy = true }, @@ -116,8 +116,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz", - .hash = "N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-", + .url = "https://deps.files.ghostty.org/ghostty-themes-release-20260112-150707-28c8f5b.tgz", + .hash = "N-V-__8AAIdIAwDt5PxH-cwCxEcTfw4jBV8sR6fZ_XLh-cR7", .lazy = true, }, }, diff --git a/build.zig.zon.bak b/build.zig.zon.bak new file mode 100644 index 000000000..191ae7fa9 --- /dev/null +++ b/build.zig.zon.bak @@ -0,0 +1,124 @@ +.{ + .name = .ghostty, + .version = "1.3.0-dev", + .paths = .{""}, + .fingerprint = 0x64407a2a0b4147e5, + .minimum_zig_version = "0.15.2", + .dependencies = .{ + // Zig libs + + .libxev = .{ + // mitchellh/libxev + .url = "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz", + .hash = "libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs", + .lazy = true, + }, + .vaxis = .{ + // rockorager/libvaxis + .url = "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz", + .hash = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS", + .lazy = true, + }, + .z2d = .{ + // vancluever/z2d + .url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz", + .hash = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T", + .lazy = true, + }, + .zig_objc = .{ + // mitchellh/zig-objc + .url = "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz", + .hash = "zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK", + .lazy = true, + }, + .zig_js = .{ + // mitchellh/zig-js + .url = "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz", + .hash = "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi", + .lazy = true, + }, + .uucode = .{ + // jacobsandlund/uucode + .url = "https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz", + .hash = "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E", + }, + .zig_wayland = .{ + // codeberg ifreund/zig-wayland + .url = "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz", + .hash = "wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe", + .lazy = true, + }, + .zf = .{ + // natecraddock/zf + .url = "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz", + .hash = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh", + .lazy = true, + }, + .gobject = .{ + // https://github.com/ghostty-org/zig-gobject based on zig_gobject + // Temporary until we generate them at build time automatically. + .url = "https://github.com/ghostty-org/zig-gobject/releases/download/0.7.0-2025-11-08-23-1/ghostty-gobject-0.7.0-2025-11-08-23-1.tar.zst", + .hash = "gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-", + .lazy = true, + }, + + // C libs + .cimgui = .{ .path = "./pkg/cimgui", .lazy = true }, + .fontconfig = .{ .path = "./pkg/fontconfig", .lazy = true }, + .freetype = .{ .path = "./pkg/freetype", .lazy = true }, + .gtk4_layer_shell = .{ .path = "./pkg/gtk4-layer-shell", .lazy = true }, + .harfbuzz = .{ .path = "./pkg/harfbuzz", .lazy = true }, + .highway = .{ .path = "./pkg/highway", .lazy = true }, + .libintl = .{ .path = "./pkg/libintl", .lazy = true }, + .libpng = .{ .path = "./pkg/libpng", .lazy = true }, + .macos = .{ .path = "./pkg/macos", .lazy = true }, + .oniguruma = .{ .path = "./pkg/oniguruma", .lazy = true }, + .opengl = .{ .path = "./pkg/opengl", .lazy = true }, + .sentry = .{ .path = "./pkg/sentry", .lazy = true }, + .simdutf = .{ .path = "./pkg/simdutf", .lazy = true }, + .utfcpp = .{ .path = "./pkg/utfcpp", .lazy = true }, + .wuffs = .{ .path = "./pkg/wuffs", .lazy = true }, + .zlib = .{ .path = "./pkg/zlib", .lazy = true }, + + // Shader translation + .glslang = .{ .path = "./pkg/glslang", .lazy = true }, + .spirv_cross = .{ .path = "./pkg/spirv-cross", .lazy = true }, + + // Wayland + .wayland = .{ + .url = "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz", + .hash = "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t", + .lazy = true, + }, + .wayland_protocols = .{ + .url = "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz", + .hash = "N-V-__8AAKw-DAAaV8bOAAGqA0-oD7o-HNIlPFYKRXSPT03S", + .lazy = true, + }, + .plasma_wayland_protocols = .{ + .url = "https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz", + .hash = "N-V-__8AAKYZBAB-CFHBKs3u4JkeiT4BMvyHu3Y5aaWF3Bbs", + .lazy = true, + }, + + // Fonts + .jetbrains_mono = .{ + .url = "https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz", + .hash = "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x", + .lazy = true, + }, + .nerd_fonts_symbols_only = .{ + .url = "https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz", + .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO26s", + .lazy = true, + }, + + // Other + .apple_sdk = .{ .path = "./pkg/apple-sdk" }, + .iterm2_themes = .{ + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz", + .hash = "N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-", + .lazy = true, + }, + }, +} diff --git a/build.zig.zon.json b/build.zig.zon.json index cb827e238..b12216bd9 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -1,4 +1,9 @@ { + "N-V-__8AANT61wB--nJ95Gj_ctmzAtcjloZ__hRqNw5lC1Kr": { + "name": "bindings", + "url": "https://deps.files.ghostty.org/DearBindings_v0.17_ImGui_v1.92.5-docking.tar.gz", + "hash": "sha256-i/7FAOAJJvZ5hT7iPWfMOS08MYFzPKRwRzhlHT9wuqM=" + }, "N-V-__8AALw2uwF_03u4JRkZwRLc3Y9hakkYV7NKRR9-RIZJ": { "name": "breakpad", "url": "https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz", @@ -24,10 +29,10 @@ "url": "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz", "hash": "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U=" }, - "gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV": { + "gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-": { "name": "gobject", - "url": "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst", - "hash": "sha256-SXiqGm81aUn6yq1wFXgNTAULdKOHS/Rzkp5OgNkkcXo=" + "url": "https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst", + "hash": "sha256-2b1DBvAIHY5LhItq3+q9L6tJgi7itnnrSAHc7fXWDEg=" }, "N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": { "name": "gtk4_layer_shell", @@ -44,15 +49,15 @@ "url": "https://deps.files.ghostty.org/highway-66486a10623fa0d72fe91260f96c892e41aceb06.tar.gz", "hash": "sha256-h9T4iT704I8iSXNgj/6/lCaKgTgLp5wS6IQZaMgKohI=" }, - "N-V-__8AAH0GaQC8a52s6vfIxg88OZgFgEW6DFxfSK4lX_l3": { + "N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI": { "name": "imgui", - "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", - "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" + "url": "https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz", + "hash": "sha256-yBbCDox18+Fa6Gc1DnmSVQLRpqhZOLsac7iSfl8x+cs=" }, - "N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-": { + "N-V-__8AAIdIAwDt5PxH-cwCxEcTfw4jBV8sR6fZ_XLh-cR7": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz", - "hash": "sha256-5QePBQlSsz9W2r4zTS3QD+cDAeyObhR51E2AkJ3ZIUk=" + "url": "https://deps.files.ghostty.org/ghostty-themes-release-20260112-150707-28c8f5b.tgz", + "hash": "sha256-NIqF12KqXhIrP+LyBtg6WtkHxNUdWOyziAdq8S45RrU=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "name": "jetbrains_mono", @@ -116,12 +121,12 @@ }, "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E": { "name": "uucode", - "url": "https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz", + "url": "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz", "hash": "sha256-SzpYGhgG4B6Luf8eT35sKLobCxjmwEuo1Twk0jeu9g4=" }, "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS": { "name": "vaxis", - "url": "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz", + "url": "https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz", "hash": "sha256-LnIzK8icW1Qexua9SHaeHz+3V8QAbz0a+UC1T5sIjvY=" }, "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t": { @@ -139,14 +144,14 @@ "url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz", "hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM=" }, - "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T": { + "z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ": { "name": "z2d", - "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz", - "hash": "sha256-+QqCRoXwrFA1/l+oWvYVyAVebGQitAFQNhi9U3EVrxA=" + "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz", + "hash": "sha256-afIdou/V7gk3/lXE0J5Ir8T7L5GgHvFnyMJ1rgRnl/c=" }, "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh": { "name": "zf", - "url": "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz", + "url": "https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz", "hash": "sha256-OwFdkorwTp4mJyvBXrTbtNmp1GnrbSkKDdrmc7d8RWg=" }, "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi": { diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 0ec137c70..430619e74 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -82,6 +82,14 @@ fetcher.${proto}; in linkFarm name [ + { + name = "N-V-__8AANT61wB--nJ95Gj_ctmzAtcjloZ__hRqNw5lC1Kr"; + path = fetchZigArtifact { + name = "bindings"; + url = "https://deps.files.ghostty.org/DearBindings_v0.17_ImGui_v1.92.5-docking.tar.gz"; + hash = "sha256-i/7FAOAJJvZ5hT7iPWfMOS08MYFzPKRwRzhlHT9wuqM="; + }; + } { name = "N-V-__8AALw2uwF_03u4JRkZwRLc3Y9hakkYV7NKRR9-RIZJ"; path = fetchZigArtifact { @@ -123,11 +131,11 @@ in }; } { - name = "gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV"; + name = "gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-"; path = fetchZigArtifact { name = "gobject"; - url = "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst"; - hash = "sha256-SXiqGm81aUn6yq1wFXgNTAULdKOHS/Rzkp5OgNkkcXo="; + url = "https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst"; + hash = "sha256-2b1DBvAIHY5LhItq3+q9L6tJgi7itnnrSAHc7fXWDEg="; }; } { @@ -155,19 +163,19 @@ in }; } { - name = "N-V-__8AAH0GaQC8a52s6vfIxg88OZgFgEW6DFxfSK4lX_l3"; + name = "N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI"; path = fetchZigArtifact { name = "imgui"; - url = "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz"; - hash = "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="; + url = "https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz"; + hash = "sha256-yBbCDox18+Fa6Gc1DnmSVQLRpqhZOLsac7iSfl8x+cs="; }; } { - name = "N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-"; + name = "N-V-__8AAIdIAwDt5PxH-cwCxEcTfw4jBV8sR6fZ_XLh-cR7"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz"; - hash = "sha256-5QePBQlSsz9W2r4zTS3QD+cDAeyObhR51E2AkJ3ZIUk="; + url = "https://deps.files.ghostty.org/ghostty-themes-release-20260112-150707-28c8f5b.tgz"; + hash = "sha256-NIqF12KqXhIrP+LyBtg6WtkHxNUdWOyziAdq8S45RrU="; }; } { @@ -270,7 +278,7 @@ in name = "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E"; path = fetchZigArtifact { name = "uucode"; - url = "https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz"; + url = "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz"; hash = "sha256-SzpYGhgG4B6Luf8eT35sKLobCxjmwEuo1Twk0jeu9g4="; }; } @@ -278,7 +286,7 @@ in name = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS"; path = fetchZigArtifact { name = "vaxis"; - url = "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz"; + url = "https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz"; hash = "sha256-LnIzK8icW1Qexua9SHaeHz+3V8QAbz0a+UC1T5sIjvY="; }; } @@ -307,18 +315,18 @@ in }; } { - name = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T"; + name = "z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ"; path = fetchZigArtifact { name = "z2d"; - url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz"; - hash = "sha256-+QqCRoXwrFA1/l+oWvYVyAVebGQitAFQNhi9U3EVrxA="; + url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz"; + hash = "sha256-afIdou/V7gk3/lXE0J5Ir8T7L5GgHvFnyMJ1rgRnl/c="; }; } { name = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh"; path = fetchZigArtifact { name = "zf"; - url = "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz"; + url = "https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz"; hash = "sha256-OwFdkorwTp4mJyvBXrTbtNmp1GnrbSkKDdrmc7d8RWg="; }; } diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 6b19df24e..72597a650 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -1,16 +1,17 @@ git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732 +https://deps.files.ghostty.org/DearBindings_v0.17_ImGui_v1.92.5-docking.tar.gz https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz https://deps.files.ghostty.org/gettext-0.24.tar.gz +https://deps.files.ghostty.org/ghostty-themes-release-20260112-150707-28c8f5b.tgz https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz -https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst +https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz https://deps.files.ghostty.org/highway-66486a10623fa0d72fe91260f96c892e41aceb06.tar.gz -https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz https://deps.files.ghostty.org/libxml2-2.11.5.tar.gz @@ -20,16 +21,16 @@ https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz +https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz +https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz +https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz -https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz -https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz -https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz -https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz -https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz +https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz +https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz diff --git a/dist/linux/ghostty_nautilus.py b/dist/linux/ghostty_nautilus.py index 42c397642..01202031c 100644 --- a/dist/linux/ghostty_nautilus.py +++ b/dist/linux/ghostty_nautilus.py @@ -17,81 +17,45 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -from os.path import isdir -from gi import require_version -from gi.repository import Nautilus, GObject, Gio, GLib +from gi.repository import Nautilus, GObject, Gio -class OpenInGhosttyAction(GObject.GObject, Nautilus.MenuProvider): - def __init__(self): - super().__init__() - session = Gio.bus_get_sync(Gio.BusType.SESSION, None) - self._systemd = None - # Check if the this system runs under systemd, per sd_booted(3) - if isdir('/run/systemd/system/'): - self._systemd = Gio.DBusProxy.new_sync(session, - Gio.DBusProxyFlags.NONE, - None, - "org.freedesktop.systemd1", - "/org/freedesktop/systemd1", - "org.freedesktop.systemd1.Manager", None) - - def _open_terminal(self, path): +def open_in_ghostty_activated(_menu, paths): + for path in paths: cmd = ['ghostty', f'--working-directory={path}', '--gtk-single-instance=false'] - child = Gio.Subprocess.new(cmd, Gio.SubprocessFlags.NONE) - if self._systemd: - # Move new terminal into a dedicated systemd scope to make systemd - # track the terminal separately; in particular this makes systemd - # keep a separate CPU and memory account for the terminal which in turn - # ensures that oomd doesn't take nautilus down if a process in - # ghostty consumes a lot of memory. - pid = int(child.get_identifier()) - props = [("PIDs", GLib.Variant('au', [pid])), - ('CollectMode', GLib.Variant('s', 'inactive-or-failed'))] - name = 'app-nautilus-com.mitchellh.ghostty-{}.scope'.format(pid) - args = GLib.Variant('(ssa(sv)a(sa(sv)))', (name, 'fail', props, [])) - self._systemd.call_sync('StartTransientUnit', args, - Gio.DBusCallFlags.NO_AUTO_START, 500, None) + Gio.Subprocess.new(cmd, Gio.SubprocessFlags.NONE) - def _menu_item_activated(self, _menu, paths): - for path in paths: - self._open_terminal(path) - def _make_item(self, name, paths): +def get_paths_to_open(files): + paths = [] + for file in files: + location = file.get_location() if file.is_directory() else file.get_parent_location() + path = location.get_path() + if path and path not in paths: + paths.append(path) + if 10 < len(paths): + # Let's not open anything if the user selected a lot of directories, + # to avoid accidentally spamming their desktop with dozends of + # new windows or tabs. Ten is a totally arbitrary limit :) + return [] + else: + return paths + + +def get_items_for_files(name, files): + paths = get_paths_to_open(files) + if paths: item = Nautilus.MenuItem(name=name, label='Open in Ghostty', icon='com.mitchellh.ghostty') - item.connect('activate', self._menu_item_activated, paths) - return item + item.connect('activate', open_in_ghostty_activated, paths) + return [item] + else: + return [] - def _paths_to_open(self, files): - paths = [] - for file in files: - location = file.get_location() if file.is_directory() else file.get_parent_location() - path = location.get_path() - if path and path not in paths: - paths.append(path) - if 10 < len(paths): - # Let's not open anything if the user selected a lot of directories, - # to avoid accidentally spamming their desktop with dozends of - # new windows or tabs. Ten is a totally arbitrary limit :) - return [] - else: - return paths - def get_file_items(self, *args): - # Nautilus 3.0 API passes args (window, files), 4.0 API just passes files - files = args[0] if len(args) == 1 else args[1] - paths = self._paths_to_open(files) - if paths: - return [self._make_item(name='GhosttyNautilus::open_in_ghostty', paths=paths)] - else: - return [] +class GhosttyMenuProvider(GObject.GObject, Nautilus.MenuProvider): + def get_file_items(self, files): + return get_items_for_files('GhosttyNautilus::open_in_ghostty', files) - def get_background_items(self, *args): - # Nautilus 3.0 API passes args (window, file), 4.0 API just passes file - file = args[0] if len(args) == 1 else args[1] - paths = self._paths_to_open([file]) - if paths: - return [self._make_item(name='GhosttyNautilus::open_folder_in_ghostty', paths=paths)] - else: - return [] + def get_background_items(self, file): + return get_items_for_files('GhosttyNautilus::open_folder_in_ghostty', [file]) diff --git a/flake.lock b/flake.lock index 90b97ed4a..be298785c 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1747046372, - "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", + "lastModified": 1761588595, + "narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=", "owner": "edolstra", "repo": "flake-compat", - "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", + "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5", "type": "github" }, "original": { @@ -34,36 +34,44 @@ "type": "github" } }, + "home-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1768068402, + "narHash": "sha256-bAXnnJZKJiF7Xr6eNW6+PhBf1lg2P1aFUO9+xgWkXfA=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "8bc5473b6bc2b6e1529a9c4040411e1199c43b4c", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 315532800, - "narHash": "sha256-sV6pJNzFkiPc6j9Bi9JuHBnWdVhtKB/mHgVmMPvDFlk=", - "rev": "82c2e0d6dde50b17ae366d2aa36f224dc19af469", + "lastModified": 1768032153, + "narHash": "sha256-zvxtwlM8ZlulmZKyYCQAPpkm5dngSEnnHjmjV7Teloc=", + "rev": "3146c6aa9995e7351a398e17470e15305e6e18ff", "type": "tarball", - "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre877938.82c2e0d6dde5/nixexprs.tar.xz" + "url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre925418.3146c6aa9995/nixexprs.tar.xz" }, "original": { "type": "tarball", "url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz" } }, - "nixpkgs_2": { - "locked": { - "lastModified": 1758360447, - "narHash": "sha256-XDY3A83bclygHDtesRoaRTafUd80Q30D/Daf9KSG6bs=", - "rev": "8eaee110344796db060382e15d3af0a9fc396e0e", - "type": "tarball", - "url": "https://releases.nixos.org/nixos/unstable/nixos-25.11pre864002.8eaee1103447/nixexprs.tar.xz" - }, - "original": { - "type": "tarball", - "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz" - } - }, "root": { "inputs": { "flake-compat": "flake-compat", "flake-utils": "flake-utils", + "home-manager": "home-manager", "nixpkgs": "nixpkgs", "zig": "zig", "zon2nix": "zon2nix" @@ -97,11 +105,11 @@ ] }, "locked": { - "lastModified": 1760401936, - "narHash": "sha256-/zj5GYO5PKhBWGzbHbqT+ehY8EghuABdQ2WGfCwZpCQ=", + "lastModified": 1763295135, + "narHash": "sha256-sGv/NHCmEnJivguGwB5w8LRmVqr1P72OjS+NzcJsssE=", "owner": "mitchellh", "repo": "zig-overlay", - "rev": "365085b6652259753b598d43b723858184980bbe", + "rev": "64f8b42cfc615b2cf99144adf2b7728c7847c72a", "type": "github" }, "original": { @@ -112,20 +120,22 @@ }, "zon2nix": { "inputs": { - "nixpkgs": "nixpkgs_2" + "nixpkgs": [ + "nixpkgs" + ] }, "locked": { - "lastModified": 1758405547, - "narHash": "sha256-WgaDgvIZMPvlZcZrpPMjkaalTBnGF2lTG+62znXctWM=", + "lastModified": 1768231828, + "narHash": "sha256-wL/8Iij4T2OLkhHcc4NieOjf7YeJffaUYbCiCqKv/+0=", "owner": "jcollie", "repo": "zon2nix", - "rev": "bf983aa90ff169372b9fa8c02e57ea75e0b42245", + "rev": "c28e93f3ba133d4c1b1d65224e2eebede61fd071", "type": "github" }, "original": { "owner": "jcollie", "repo": "zon2nix", - "rev": "bf983aa90ff169372b9fa8c02e57ea75e0b42245", + "rev": "c28e93f3ba133d4c1b1d65224e2eebede61fd071", "type": "github" } } diff --git a/flake.nix b/flake.nix index 3dcfef185..c96004a09 100644 --- a/flake.nix +++ b/flake.nix @@ -6,7 +6,9 @@ # glibc versions used by our dependencies from Nix are compatible with the # system glibc that the user is building for. # - # We are currently on unstable to get Zig 0.15 for our package.nix + # We are currently on nixpkgs-unstable to get Zig 0.15 for our package.nix and + # Gnome 49/Gtk 4.20. + # nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"; flake-utils.url = "github:numtide/flake-utils"; @@ -26,12 +28,16 @@ }; zon2nix = { - url = "github:jcollie/zon2nix?rev=bf983aa90ff169372b9fa8c02e57ea75e0b42245"; + url = "github:jcollie/zon2nix?rev=c28e93f3ba133d4c1b1d65224e2eebede61fd071"; inputs = { - # Don't override nixpkgs until Zig 0.15 is available in the Nix branch - # we are using for "normal" builds. - # - # nixpkgs.follows = "nixpkgs"; + nixpkgs.follows = "nixpkgs"; + }; + }; + + home-manager = { + url = "github:nix-community/home-manager"; + inputs = { + nixpkgs.follows = "nixpkgs"; }; }; }; @@ -41,92 +47,102 @@ nixpkgs, zig, zon2nix, + home-manager, ... - }: - builtins.foldl' nixpkgs.lib.recursiveUpdate {} ( - builtins.map ( - system: let - pkgs = nixpkgs.legacyPackages.${system}; - in { - devShell.${system} = pkgs.callPackage ./nix/devShell.nix { - zig = zig.packages.${system}."0.15.2"; - wraptest = pkgs.callPackage ./nix/pkgs/wraptest.nix {}; - zon2nix = zon2nix; + }: let + inherit (nixpkgs) lib legacyPackages; - python3 = pkgs.python3.override { - self = pkgs.python3; - packageOverrides = pyfinal: pyprev: { - blessed = pyfinal.callPackage ./nix/pkgs/blessed.nix {}; - ucs-detect = pyfinal.callPackage ./nix/pkgs/ucs-detect.nix {}; - }; - }; + # Our supported systems are the same supported systems as the Zig binaries. + platforms = lib.attrNames zig.packages; + + # It's not always possible to build Ghostty with Nix for each system, + # one such example being macOS due to missing Swift 6 and xcodebuild + # support in the Nix ecosystem. Therefore for things like package outputs + # we need to limit the attributes we expose. + buildablePlatforms = lib.filter (p: !(lib.systems.elaborate p).isDarwin) platforms; + + forAllPlatforms = f: lib.genAttrs platforms (s: f legacyPackages.${s}); + forBuildablePlatforms = f: lib.genAttrs buildablePlatforms (s: f legacyPackages.${s}); + in { + devShell = forAllPlatforms (pkgs: + pkgs.callPackage ./nix/devShell.nix { + zig = zig.packages.${pkgs.stdenv.hostPlatform.system}."0.15.2"; + wraptest = pkgs.callPackage ./nix/pkgs/wraptest.nix {}; + zon2nix = zon2nix; + + python3 = pkgs.python3.override { + self = pkgs.python3; + packageOverrides = pyfinal: pyprev: { + blessed = pyfinal.callPackage ./nix/pkgs/blessed.nix {}; + ucs-detect = pyfinal.callPackage ./nix/pkgs/ucs-detect.nix {}; }; - - packages.${system} = let - mkArgs = optimize: { - inherit optimize; - - revision = self.shortRev or self.dirtyShortRev or "dirty"; - }; - in rec { - deps = pkgs.callPackage ./build.zig.zon.nix {}; - ghostty-debug = pkgs.callPackage ./nix/package.nix (mkArgs "Debug"); - ghostty-releasesafe = pkgs.callPackage ./nix/package.nix (mkArgs "ReleaseSafe"); - ghostty-releasefast = pkgs.callPackage ./nix/package.nix (mkArgs "ReleaseFast"); - - ghostty = ghostty-releasefast; - default = ghostty; - }; - - formatter.${system} = pkgs.alejandra; - - apps.${system} = let - runVM = ( - module: let - vm = import ./nix/vm/create.nix { - inherit system module nixpkgs; - overlay = self.overlays.debug; - }; - program = pkgs.writeShellScript "run-ghostty-vm" '' - SHARED_DIR=$(pwd) - export SHARED_DIR - - ${pkgs.lib.getExe vm.config.system.build.vm} "$@" - ''; - in { - type = "app"; - program = "${program}"; - } - ); - in { - wayland-cinnamon = runVM ./nix/vm/wayland-cinnamon.nix; - wayland-gnome = runVM ./nix/vm/wayland-gnome.nix; - wayland-plasma6 = runVM ./nix/vm/wayland-plasma6.nix; - x11-cinnamon = runVM ./nix/vm/x11-cinnamon.nix; - x11-gnome = runVM ./nix/vm/x11-gnome.nix; - x11-plasma6 = runVM ./nix/vm/x11-plasma6.nix; - x11-xfce = runVM ./nix/vm/x11-xfce.nix; - }; - } - # Our supported systems are the same supported systems as the Zig binaries. - ) (builtins.attrNames zig.packages) - ) - // { - overlays = { - default = self.overlays.releasefast; - releasefast = final: prev: { - ghostty = self.packages.${prev.stdenv.hostPlatform.system}.ghostty-releasefast; }; - debug = final: prev: { - ghostty = self.packages.${prev.stdenv.hostPlatform.system}.ghostty-debug; + }); + + packages = + forAllPlatforms (pkgs: { + # Deps are needed for environmental setup on macOS + deps = pkgs.callPackage ./build.zig.zon.nix {}; + }) + // forBuildablePlatforms (pkgs: let + mkArgs = optimize: { + inherit optimize; + revision = self.shortRev or self.dirtyShortRev or "dirty"; }; + in rec { + ghostty-debug = pkgs.callPackage ./nix/package.nix (mkArgs "Debug"); + ghostty-releasesafe = pkgs.callPackage ./nix/package.nix (mkArgs "ReleaseSafe"); + ghostty-releasefast = pkgs.callPackage ./nix/package.nix (mkArgs "ReleaseFast"); + + ghostty = ghostty-releasefast; + default = ghostty; + }); + + formatter = forAllPlatforms (pkgs: pkgs.alejandra); + + apps = forBuildablePlatforms (pkgs: let + runVM = module: desc: let + vm = import ./nix/vm/create.nix { + inherit (pkgs.stdenv.hostPlatform) system; + inherit module nixpkgs; + overlay = self.overlays.debug; + }; + program = pkgs.writeShellScript "run-ghostty-vm" '' + SHARED_DIR=$(pwd) + export SHARED_DIR + + ${pkgs.lib.getExe vm.config.system.build.vm} "$@" + ''; + in { + type = "app"; + program = "${program}"; + meta.description = "start a vm from ${toString module}"; + }; + in { + wayland-cinnamon = runVM ./nix/vm/wayland-cinnamon.nix; + wayland-gnome = runVM ./nix/vm/wayland-gnome.nix; + wayland-plasma6 = runVM ./nix/vm/wayland-plasma6.nix; + x11-cinnamon = runVM ./nix/vm/x11-cinnamon.nix; + x11-plasma6 = runVM ./nix/vm/x11-plasma6.nix; + x11-xfce = runVM ./nix/vm/x11-xfce.nix; + }); + + checks = forAllPlatforms (pkgs: + import ./nix/tests.nix { + inherit home-manager nixpkgs self; + inherit (pkgs.stdenv.hostPlatform) system; + }); + + overlays = { + default = self.overlays.releasefast; + releasefast = final: prev: { + ghostty = self.packages.${prev.stdenv.hostPlatform.system}.ghostty-releasefast; + }; + debug = final: prev: { + ghostty = self.packages.${prev.stdenv.hostPlatform.system}.ghostty-debug; }; - create-vm = import ./nix/vm/create.nix; - create-cinnamon-vm = import ./nix/vm/create-cinnamon.nix; - create-gnome-vm = import ./nix/vm/create-gnome.nix; - create-plasma6-vm = import ./nix/vm/create-plasma6.nix; - create-xfce-vm = import ./nix/vm/create-xfce.nix; }; + }; nixConfig = { extra-substituters = ["https://ghostty.cachix.org"]; diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 9563f9622..3e2b1e26d 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -1,4 +1,10 @@ [ + { + "type": "archive", + "url": "https://deps.files.ghostty.org/DearBindings_v0.17_ImGui_v1.92.5-docking.tar.gz", + "dest": "vendor/p/N-V-__8AANT61wB--nJ95Gj_ctmzAtcjloZ__hRqNw5lC1Kr", + "sha256": "8bfec500e00926f679853ee23d67cc392d3c3181733ca4704738651d3f70baa3" + }, { "type": "archive", "url": "https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz", @@ -31,9 +37,9 @@ }, { "type": "archive", - "url": "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst", - "dest": "vendor/p/gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV", - "sha256": "4978aa1a6f356949facaad7015780d4c050b74a3874bf473929e4e80d924717a" + "url": "https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst", + "dest": "vendor/p/gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-", + "sha256": "d9bd4306f0081d8e4b848b6adfeabd2fab49822ee2b679eb4801dcedf5d60c48" }, { "type": "archive", @@ -55,15 +61,15 @@ }, { "type": "archive", - "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", - "dest": "vendor/p/N-V-__8AAH0GaQC8a52s6vfIxg88OZgFgEW6DFxfSK4lX_l3", - "sha256": "a05fd01e04cf11ab781e28387c621d2e420f1e6044c8e27a25e603ea99ef7860" + "url": "https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz", + "dest": "vendor/p/N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI", + "sha256": "c816c20e8c75f3e15ae867350e79925502d1a6a85938bb1a73b8927e5f31f9cb" }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz", - "dest": "vendor/p/N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-", - "sha256": "e5078f050952b33f56dabe334d2dd00fe70301ec8e6e1479d44d80909dd92149" + "url": "https://deps.files.ghostty.org/ghostty-themes-release-20260112-150707-28c8f5b.tgz", + "dest": "vendor/p/N-V-__8AAIdIAwDt5PxH-cwCxEcTfw4jBV8sR6fZ_XLh-cR7", + "sha256": "348a85d762aa5e122b3fe2f206d83a5ad907c4d51d58ecb388076af12e3946b5" }, { "type": "archive", @@ -139,13 +145,13 @@ }, { "type": "archive", - "url": "https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz", + "url": "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz", "dest": "vendor/p/uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E", "sha256": "4b3a581a1806e01e8bb9ff1e4f7e6c28ba1b0b18e6c04ba8d53c24d237aef60e" }, { "type": "archive", - "url": "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz", + "url": "https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz", "dest": "vendor/p/vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS", "sha256": "2e72332bc89c5b541ec6e6bd48769e1f3fb757c4006f3d1af940b54f9b088ef6" }, @@ -169,13 +175,13 @@ }, { "type": "archive", - "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz", - "dest": "vendor/p/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T", - "sha256": "f90a824685f0ac5035fe5fa85af615c8055e6c6422b401503618bd537115af10" + "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz", + "dest": "vendor/p/z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ", + "sha256": "69f21da2efd5ee0937fe55c4d09e48afc4fb2f91a01ef167c8c275ae046797f7" }, { "type": "archive", - "url": "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz", + "url": "https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz", "dest": "vendor/p/zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh", "sha256": "3b015d928af04e9e26272bc15eb4dbb4d9a9d469eb6d290a0ddae673b77c4568" }, diff --git a/include/ghostty.h b/include/ghostty.h index 6cafe8773..3d3973084 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -66,6 +66,14 @@ typedef enum { GHOSTTY_MOUSE_LEFT, GHOSTTY_MOUSE_RIGHT, GHOSTTY_MOUSE_MIDDLE, + GHOSTTY_MOUSE_FOUR, + GHOSTTY_MOUSE_FIVE, + GHOSTTY_MOUSE_SIX, + GHOSTTY_MOUSE_SEVEN, + GHOSTTY_MOUSE_EIGHT, + GHOSTTY_MOUSE_NINE, + GHOSTTY_MOUSE_TEN, + GHOSTTY_MOUSE_ELEVEN, } ghostty_input_mouse_button_e; typedef enum { @@ -102,6 +110,13 @@ typedef enum { GHOSTTY_MODS_SUPER_RIGHT = 1 << 9, } ghostty_input_mods_e; +typedef enum { + GHOSTTY_BINDING_FLAGS_CONSUMED = 1 << 0, + GHOSTTY_BINDING_FLAGS_ALL = 1 << 1, + GHOSTTY_BINDING_FLAGS_GLOBAL = 1 << 2, + GHOSTTY_BINDING_FLAGS_PERFORMABLE = 1 << 3, +} ghostty_binding_flags_e; + typedef enum { GHOSTTY_ACTION_RELEASE, GHOSTTY_ACTION_PRESS, @@ -317,12 +332,14 @@ typedef struct { typedef enum { GHOSTTY_TRIGGER_PHYSICAL, GHOSTTY_TRIGGER_UNICODE, + GHOSTTY_TRIGGER_CATCH_ALL, } ghostty_input_trigger_tag_e; typedef union { ghostty_input_key_e translated; ghostty_input_key_e physical; uint32_t unicode; + // catch_all has no payload } ghostty_input_trigger_key_u; typedef struct { @@ -414,6 +431,12 @@ typedef union { ghostty_platform_ios_s ios; } ghostty_platform_u; +typedef enum { + GHOSTTY_SURFACE_CONTEXT_WINDOW = 0, + GHOSTTY_SURFACE_CONTEXT_TAB = 1, + GHOSTTY_SURFACE_CONTEXT_SPLIT = 2, +} ghostty_surface_context_e; + typedef struct { ghostty_platform_e platform_tag; ghostty_platform_u platform; @@ -426,6 +449,7 @@ typedef struct { size_t env_var_count; const char* initial_input; bool wait_after_command; + ghostty_surface_context_e context; } ghostty_surface_config_s; typedef struct { @@ -452,6 +476,12 @@ typedef struct { size_t len; } ghostty_config_color_list_s; +// config.RepeatableCommand +typedef struct { + const ghostty_command_s* commands; + size_t len; +} ghostty_config_command_list_s; + // config.Palette typedef struct { ghostty_config_color_s colors[256]; @@ -512,6 +542,12 @@ typedef enum { GHOSTTY_GOTO_SPLIT_RIGHT, } ghostty_action_goto_split_e; +// apprt.action.GotoWindow +typedef enum { + GHOSTTY_GOTO_WINDOW_PREVIOUS, + GHOSTTY_GOTO_WINDOW_NEXT, +} ghostty_action_goto_window_e; + // apprt.action.ResizeSplit.Direction typedef enum { GHOSTTY_RESIZE_SPLIT_UP, @@ -573,6 +609,12 @@ typedef enum { GHOSTTY_QUIT_TIMER_STOP, } ghostty_action_quit_timer_e; +// apprt.action.Readonly +typedef enum { + GHOSTTY_READONLY_OFF, + GHOSTTY_READONLY_ON, +} ghostty_action_readonly_e; + // apprt.action.DesktopNotification.C typedef struct { const char* title; @@ -584,6 +626,12 @@ typedef struct { const char* title; } ghostty_action_set_title_s; +// apprt.action.PromptTitle +typedef enum { + GHOSTTY_PROMPT_TITLE_SURFACE, + GHOSTTY_PROMPT_TITLE_TAB, +} ghostty_action_prompt_title_e; + // apprt.action.Pwd.C typedef struct { const char* pwd; @@ -671,6 +719,27 @@ typedef struct { ghostty_input_trigger_s trigger; } ghostty_action_key_sequence_s; +// apprt.action.KeyTable.Tag +typedef enum { + GHOSTTY_KEY_TABLE_ACTIVATE, + GHOSTTY_KEY_TABLE_DEACTIVATE, + GHOSTTY_KEY_TABLE_DEACTIVATE_ALL, +} ghostty_action_key_table_tag_e; + +// apprt.action.KeyTable.CValue +typedef union { + struct { + const char *name; + size_t len; + } activate; +} ghostty_action_key_table_u; + +// apprt.action.KeyTable.C +typedef struct { + ghostty_action_key_table_tag_e tag; + ghostty_action_key_table_u value; +} ghostty_action_key_table_s; + // apprt.action.ColorKind typedef enum { GHOSTTY_ACTION_COLOR_KIND_FOREGROUND = -1, @@ -714,6 +783,7 @@ typedef struct { typedef enum { GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS, GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER, + GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT, } ghostty_action_close_tab_mode_e; // apprt.surface.Message.ChildExited @@ -784,9 +854,11 @@ typedef enum { GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL, GHOSTTY_ACTION_TOGGLE_COMMAND_PALETTE, GHOSTTY_ACTION_TOGGLE_VISIBILITY, + GHOSTTY_ACTION_TOGGLE_BACKGROUND_OPACITY, GHOSTTY_ACTION_MOVE_TAB, GHOSTTY_ACTION_GOTO_TAB, GHOSTTY_ACTION_GOTO_SPLIT, + GHOSTTY_ACTION_GOTO_WINDOW, GHOSTTY_ACTION_RESIZE_SPLIT, GHOSTTY_ACTION_EQUALIZE_SPLITS, GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM, @@ -813,6 +885,7 @@ typedef enum { GHOSTTY_ACTION_FLOAT_WINDOW, GHOSTTY_ACTION_SECURE_INPUT, GHOSTTY_ACTION_KEY_SEQUENCE, + GHOSTTY_ACTION_KEY_TABLE, GHOSTTY_ACTION_COLOR_CHANGE, GHOSTTY_ACTION_RELOAD_CONFIG, GHOSTTY_ACTION_CONFIG_CHANGE, @@ -830,6 +903,7 @@ typedef enum { GHOSTTY_ACTION_END_SEARCH, GHOSTTY_ACTION_SEARCH_TOTAL, GHOSTTY_ACTION_SEARCH_SELECTED, + GHOSTTY_ACTION_READONLY, } ghostty_action_tag_e; typedef union { @@ -838,6 +912,7 @@ typedef union { ghostty_action_move_tab_s move_tab; ghostty_action_goto_tab_e goto_tab; ghostty_action_goto_split_e goto_split; + ghostty_action_goto_window_e goto_window; ghostty_action_resize_split_s resize_split; ghostty_action_size_limit_s size_limit; ghostty_action_initial_size_s initial_size; @@ -846,6 +921,7 @@ typedef union { ghostty_action_inspector_e inspector; ghostty_action_desktop_notification_s desktop_notification; ghostty_action_set_title_s set_title; + ghostty_action_prompt_title_e prompt_title; ghostty_action_pwd_s pwd; ghostty_action_mouse_shape_e mouse_shape; ghostty_action_mouse_visibility_e mouse_visibility; @@ -855,6 +931,7 @@ typedef union { ghostty_action_float_window_e float_window; ghostty_action_secure_input_e secure_input; ghostty_action_key_sequence_s key_sequence; + ghostty_action_key_table_s key_table; ghostty_action_color_change_s color_change; ghostty_action_reload_config_s reload_config; ghostty_action_config_change_s config_change; @@ -866,6 +943,7 @@ typedef union { ghostty_action_start_search_s start_search; ghostty_action_search_total_s search_total; ghostty_action_search_selected_s search_selected; + ghostty_action_readonly_e readonly; } ghostty_action_u; typedef struct { @@ -946,6 +1024,7 @@ ghostty_config_t ghostty_config_new(); void ghostty_config_free(ghostty_config_t); ghostty_config_t ghostty_config_clone(ghostty_config_t); void ghostty_config_load_cli_args(ghostty_config_t); +void ghostty_config_load_file(ghostty_config_t, const char*); void ghostty_config_load_default_files(ghostty_config_t); void ghostty_config_load_recursive_files(ghostty_config_t); void ghostty_config_finalize(ghostty_config_t); @@ -979,7 +1058,7 @@ ghostty_surface_t ghostty_surface_new(ghostty_app_t, void ghostty_surface_free(ghostty_surface_t); void* ghostty_surface_userdata(ghostty_surface_t); ghostty_app_t ghostty_surface_app(ghostty_surface_t); -ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t); +ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t, ghostty_surface_context_e); void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t); bool ghostty_surface_needs_confirm_quit(ghostty_surface_t); bool ghostty_surface_process_exited(ghostty_surface_t); @@ -994,9 +1073,10 @@ void ghostty_surface_set_color_scheme(ghostty_surface_t, ghostty_color_scheme_e); ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t, ghostty_input_mods_e); -void ghostty_surface_commands(ghostty_surface_t, ghostty_command_s**, size_t*); bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s); -bool ghostty_surface_key_is_binding(ghostty_surface_t, ghostty_input_key_s); +bool ghostty_surface_key_is_binding(ghostty_surface_t, + ghostty_input_key_s, + ghostty_binding_flags_e*); void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t); void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t); bool ghostty_surface_mouse_captured(ghostty_surface_t); diff --git a/include/ghostty/vt/osc.h b/include/ghostty/vt/osc.h index 7e2c8f322..f53077ab3 100644 --- a/include/ghostty/vt/osc.h +++ b/include/ghostty/vt/osc.h @@ -63,24 +63,26 @@ typedef enum { GHOSTTY_OSC_COMMAND_INVALID = 0, GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE = 1, GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON = 2, - GHOSTTY_OSC_COMMAND_PROMPT_START = 3, - GHOSTTY_OSC_COMMAND_PROMPT_END = 4, - GHOSTTY_OSC_COMMAND_END_OF_INPUT = 5, - GHOSTTY_OSC_COMMAND_END_OF_COMMAND = 6, - GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 7, - GHOSTTY_OSC_COMMAND_REPORT_PWD = 8, - GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 9, - GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 10, - GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 11, - GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 12, - GHOSTTY_OSC_COMMAND_HYPERLINK_START = 13, - GHOSTTY_OSC_COMMAND_HYPERLINK_END = 14, - GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 15, - GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 16, - GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 17, - GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 18, - GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 19, - GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 20, + GHOSTTY_OSC_COMMAND_SEMANTIC_PROMPT = 3, + GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 4, + GHOSTTY_OSC_COMMAND_REPORT_PWD = 5, + GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 6, + GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 7, + GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 8, + GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 9, + GHOSTTY_OSC_COMMAND_HYPERLINK_START = 10, + GHOSTTY_OSC_COMMAND_HYPERLINK_END = 11, + GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 12, + GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 13, + GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 14, + GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 15, + GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 16, + GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 17, + GHOSTTY_OSC_COMMAND_CONEMU_RUN_PROCESS = 18, + GHOSTTY_OSC_COMMAND_CONEMU_OUTPUT_ENVIRONMENT_VARIABLE = 19, + GHOSTTY_OSC_COMMAND_CONEMU_XTERM_EMULATION = 20, + GHOSTTY_OSC_COMMAND_CONEMU_COMMENT = 21, + GHOSTTY_OSC_COMMAND_KITTY_TEXT_SIZING = 22, } GhosttyOscCommandType; /** diff --git a/macos/Ghostty-Info.plist b/macos/Ghostty-Info.plist index 2bf3b0bae..5960dc0e7 100644 --- a/macos/Ghostty-Info.plist +++ b/macos/Ghostty-Info.plist @@ -100,5 +100,20 @@ SUPublicEDKey wsNcGf5hirwtdXMVnYoxRIX/SqZQLMOsYlD3q3imeok= + UTExportedTypeDeclarations + + + UTTypeIdentifier + com.mitchellh.ghosttySurfaceId + UTTypeDescription + Ghostty Surface Identifier + UTTypeConformsTo + + public.data + + UTTypeTagSpecification + + + diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index ca420afaa..adcc107e1 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -28,6 +28,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 810ACCA52E9D3302004F8F92 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A5B30529299BEAAA0047F10C /* Project object */; + proxyType = 1; + remoteGlobalIDString = A5B30530299BEAAA0047F10C; + remoteInfo = Ghostty; + }; A54F45F72E1F047A0046BD5C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = A5B30529299BEAAA0047F10C /* Project object */; @@ -42,6 +49,7 @@ 3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyReleaseLocal.entitlements; sourceTree = ""; }; 55154BDF2B33911F001622DC /* ghostty */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ghostty; path = "../zig-out/share/ghostty"; sourceTree = ""; }; 552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = ""; }; + 810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GhosttyUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = ""; }; A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyDebug.entitlements; sourceTree = ""; }; A546F1132D7B68D7003B11A0 /* locale */ = {isa = PBXFileReference; lastKnownFileType = folder; name = locale; path = "../zig-out/share/locale"; sourceTree = ""; }; @@ -66,11 +74,13 @@ isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( App/macOS/AppDelegate.swift, + "App/macOS/AppDelegate+Ghostty.swift", App/macOS/main.swift, App/macOS/MainMenu.xib, Features/About/About.xib, Features/About/AboutController.swift, Features/About/AboutView.swift, + Features/About/CyclingIconView.swift, "Features/App Intents/CloseTerminalIntent.swift", "Features/App Intents/CommandPaletteIntent.swift", "Features/App Intents/Entities/CommandEntity.swift", @@ -95,6 +105,7 @@ Features/QuickTerminal/QuickTerminal.xib, Features/QuickTerminal/QuickTerminalController.swift, Features/QuickTerminal/QuickTerminalPosition.swift, + Features/QuickTerminal/QuickTerminalRestorableState.swift, Features/QuickTerminal/QuickTerminalScreen.swift, Features/QuickTerminal/QuickTerminalScreenStateCache.swift, Features/QuickTerminal/QuickTerminalSize.swift, @@ -115,7 +126,9 @@ Features/Terminal/ErrorView.swift, Features/Terminal/TerminalController.swift, Features/Terminal/TerminalRestorable.swift, + Features/Terminal/TerminalTabColor.swift, Features/Terminal/TerminalView.swift, + Features/Terminal/TerminalViewContainer.swift, "Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift", "Features/Terminal/Window Styles/Terminal.xib", "Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib", @@ -135,19 +148,19 @@ Features/Update/UpdateSimulator.swift, Features/Update/UpdateViewModel.swift, "Ghostty/FullscreenMode+Extension.swift", - Ghostty/Ghostty.Command.swift, Ghostty/Ghostty.Error.swift, Ghostty/Ghostty.Event.swift, Ghostty/Ghostty.Input.swift, Ghostty/Ghostty.Surface.swift, - Ghostty/InspectorView.swift, "Ghostty/NSEvent+Extension.swift", - Ghostty/SurfaceScrollView.swift, - Ghostty/SurfaceView_AppKit.swift, + "Ghostty/Surface View/InspectorView.swift", + "Ghostty/Surface View/SurfaceDragSource.swift", + "Ghostty/Surface View/SurfaceGrabHandle.swift", + "Ghostty/Surface View/SurfaceScrollView.swift", + "Ghostty/Surface View/SurfaceView_AppKit.swift", Helpers/AppInfo.swift, Helpers/CodableBridge.swift, Helpers/Cursor.swift, - Helpers/DraggableWindowView.swift, Helpers/ExpiringUndoManager.swift, "Helpers/Extensions/Double+Extension.swift", "Helpers/Extensions/EventModifiers+Extension.swift", @@ -155,13 +168,16 @@ "Helpers/Extensions/KeyboardShortcut+Extension.swift", "Helpers/Extensions/NSAppearance+Extension.swift", "Helpers/Extensions/NSApplication+Extension.swift", + "Helpers/Extensions/NSColor+Extension.swift", "Helpers/Extensions/NSImage+Extension.swift", + "Helpers/Extensions/NSMenu+Extension.swift", "Helpers/Extensions/NSMenuItem+Extension.swift", "Helpers/Extensions/NSPasteboard+Extension.swift", "Helpers/Extensions/NSScreen+Extension.swift", "Helpers/Extensions/NSView+Extension.swift", "Helpers/Extensions/NSWindow+Extension.swift", "Helpers/Extensions/NSWorkspace+Extension.swift", + "Helpers/Extensions/Transferable+Extension.swift", "Helpers/Extensions/UndoManager+Extension.swift", "Helpers/Extensions/View+Extension.swift", Helpers/Fullscreen.swift, @@ -183,18 +199,26 @@ isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( App/iOS/iOSApp.swift, - Ghostty/SurfaceView_UIKit.swift, + "Ghostty/Surface View/SurfaceView_UIKit.swift", ); target = A5B30530299BEAAA0047F10C /* Ghostty */; }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + 810ACCA02E9D3302004F8F92 /* GhosttyUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = GhosttyUITests; sourceTree = ""; }; 81F82BC72E82815D001EDFA7 /* Sources */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (81F82CB12E8281F9001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 81F82CB02E8281F5001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Sources; sourceTree = ""; }; A54F45F42E1F047A0046BD5C /* Tests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Tests; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ + 810ACC9C2E9D3301004F8F92 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; A54F45F02E1F047A0046BD5C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -251,6 +275,7 @@ 3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */, 81F82BC72E82815D001EDFA7 /* Sources */, A54F45F42E1F047A0046BD5C /* Tests */, + 810ACCA02E9D3302004F8F92 /* GhosttyUITests */, A5D495A3299BECBA00DD1313 /* Frameworks */, A5A1F8862A489D7400D1E8BC /* Resources */, A5B30532299BEAAA0047F10C /* Products */, @@ -263,6 +288,7 @@ A5B30531299BEAAA0047F10C /* Ghostty.app */, A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */, A54F45F32E1F047A0046BD5C /* GhosttyTests.xctest */, + 810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */, ); name = Products; sourceTree = ""; @@ -279,6 +305,29 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 810ACC9E2E9D3301004F8F92 /* GhosttyUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 810ACCA72E9D3302004F8F92 /* Build configuration list for PBXNativeTarget "GhosttyUITests" */; + buildPhases = ( + 810ACC9B2E9D3301004F8F92 /* Sources */, + 810ACC9C2E9D3301004F8F92 /* Frameworks */, + 810ACC9D2E9D3301004F8F92 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 810ACCA62E9D3302004F8F92 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 810ACCA02E9D3302004F8F92 /* GhosttyUITests */, + ); + name = GhosttyUITests; + packageProductDependencies = ( + ); + productName = GhosttyUITests; + productReference = 810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; A54F45F22E1F047A0046BD5C /* GhosttyTests */ = { isa = PBXNativeTarget; buildConfigurationList = A54F45FC2E1F047A0046BD5C /* Build configuration list for PBXNativeTarget "GhosttyTests" */; @@ -352,9 +401,13 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 2600; + LastSwiftUpdateCheck = 2610; LastUpgradeCheck = 1610; TargetAttributes = { + 810ACC9E2E9D3301004F8F92 = { + CreatedOnToolsVersion = 26.1; + TestTargetID = A5B30530299BEAAA0047F10C; + }; A54F45F22E1F047A0046BD5C = { CreatedOnToolsVersion = 26.0; TestTargetID = A5B30530299BEAAA0047F10C; @@ -387,11 +440,19 @@ A5B30530299BEAAA0047F10C /* Ghostty */, A5D4499C2B53AE7B000F5B83 /* Ghostty-iOS */, A54F45F22E1F047A0046BD5C /* GhosttyTests */, + 810ACC9E2E9D3301004F8F92 /* GhosttyUITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 810ACC9D2E9D3301004F8F92 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; A54F45F12E1F047A0046BD5C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -430,6 +491,13 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 810ACC9B2E9D3301004F8F92 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; A54F45EF2E1F047A0046BD5C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -454,6 +522,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 810ACCA62E9D3302004F8F92 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A5B30530299BEAAA0047F10C /* Ghostty */; + targetProxy = 810ACCA52E9D3302004F8F92 /* PBXContainerItemProxy */; + }; A54F45F82E1F047A0046BD5C /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = A5B30530299BEAAA0047F10C /* Ghostty */; @@ -571,6 +644,73 @@ }; name = ReleaseLocal; }; + 810ACCA82E9D3302004F8F92 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = Ghostty; + }; + name = Debug; + }; + 810ACCA92E9D3302004F8F92 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = Ghostty; + }; + name = Release; + }; + 810ACCAA2E9D3302004F8F92 /* ReleaseLocal */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = Ghostty; + }; + name = ReleaseLocal; + }; A54F45F92E1F047A0046BD5C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -987,6 +1127,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 810ACCA72E9D3302004F8F92 /* Build configuration list for PBXNativeTarget "GhosttyUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 810ACCA82E9D3302004F8F92 /* Debug */, + 810ACCA92E9D3302004F8F92 /* Release */, + 810ACCAA2E9D3302004F8F92 /* ReleaseLocal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseLocal; + }; A54F45FC2E1F047A0046BD5C /* Build configuration list for PBXNativeTarget "GhosttyTests" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme b/macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme index 0d8761c9e..2b4f815ea 100644 --- a/macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme +++ b/macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme @@ -40,6 +40,17 @@ ReferencedContainer = "container:Ghostty.xcodeproj"> + + + + 0.5 + } + + var luminance: Double { + var r: CGFloat = 0 + var g: CGFloat = 0 + var b: CGFloat = 0 + var a: CGFloat = 0 + + guard let rgb = self.usingColorSpace(.sRGB) else { return 0 } + rgb.getRed(&r, green: &g, blue: &b, alpha: &a) + return (0.299 * r) + (0.587 * g) + (0.114 * b) + } +} + +extension NSImage { + func colorAt(x: Int, y: Int) -> NSColor? { + guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else { + return nil + } + return NSBitmapImageRep(cgImage: cgImage).colorAt(x: x, y: y) + } +} diff --git a/macos/GhosttyUITests/GhosttyCustomConfigCase.swift b/macos/GhosttyUITests/GhosttyCustomConfigCase.swift new file mode 100644 index 000000000..41993247a --- /dev/null +++ b/macos/GhosttyUITests/GhosttyCustomConfigCase.swift @@ -0,0 +1,59 @@ +// +// GhosttyCustomConfigCase.swift +// Ghostty +// +// Created by luca on 16.10.2025. +// + +import XCTest + +class GhosttyCustomConfigCase: XCTestCase { + /// We only want run these UI tests + /// when testing manually with Xcode IDE + /// + /// So that we don't have to wait for each ci check + /// to run these tedious tests + override class var defaultTestSuite: XCTestSuite { + // https://lldb.llvm.org/cpp_reference/PlatformDarwin_8cpp_source.html#:~:text==%20%22-,IDE_DISABLED_OS_ACTIVITY_DT_MODE + + if ProcessInfo.processInfo.environment["IDE_DISABLED_OS_ACTIVITY_DT_MODE"] != nil { + return XCTestSuite(forTestCaseClass: Self.self) + } else { + return XCTestSuite(name: "Skipping \(className())") + } + } + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + var configFile: URL? + override func setUpWithError() throws { + continueAfterFailure = false + } + + override func tearDown() async throws { + if let configFile { + try FileManager.default.removeItem(at: configFile) + } + } + + func updateConfig(_ newConfig: String) throws { + if configFile == nil { + let temporaryConfig = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + .appendingPathExtension("ghostty") + configFile = temporaryConfig + } + try newConfig.write(to: configFile!, atomically: true, encoding: .utf8) + } + + func ghosttyApplication() throws -> XCUIApplication { + let app = XCUIApplication() + app.launchArguments.append(contentsOf: ["-ApplePersistenceIgnoreState", "YES"]) + guard let configFile else { + return app + } + app.launchEnvironment["GHOSTTY_CONFIG_PATH"] = configFile.path + return app + } +} diff --git a/macos/GhosttyUITests/GhosttyThemeTests.swift b/macos/GhosttyUITests/GhosttyThemeTests.swift new file mode 100644 index 000000000..f8f5286fb --- /dev/null +++ b/macos/GhosttyUITests/GhosttyThemeTests.swift @@ -0,0 +1,159 @@ +// +// GhosttyThemeTests.swift +// Ghostty +// +// Created by luca on 27.10.2025. +// + +import AppKit +import XCTest + +final class GhosttyThemeTests: GhosttyCustomConfigCase { + let windowTitle = "GhosttyThemeTests" + private func assertTitlebarAppearance( + _ appearance: XCUIDevice.Appearance, + for app: XCUIApplication, + title: String? = nil, + colorLocation: CGPoint? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) throws { + for i in 0 ..< app.windows.count { + let titleView = app.windows.element(boundBy: i).staticTexts.element(matching: NSPredicate(format: "value == '\(title ?? windowTitle)'")) + + let image = titleView.screenshot().image + guard let imageColor = image.colorAt(x: Int(colorLocation?.x ?? 1), y: Int(colorLocation?.y ?? 1)) else { + throw XCTSkip("failed to get pixel color", file: file, line: line) + } + + switch appearance { + case .dark: + XCTAssertLessThanOrEqual(imageColor.luminance, 0.5, "Expected dark appearance for this test", file: file, line: line) + default: + XCTAssertGreaterThanOrEqual(imageColor.luminance, 0.5, "Expected light appearance for this test", file: file, line: line) + } + } + } + + /// https://github.com/ghostty-org/ghostty/issues/8282 + @MainActor + func testIssue8282() async throws { + try updateConfig("title=\(windowTitle) \n theme=light:3024 Day,dark:3024 Night") + XCUIDevice.shared.appearance = .dark + + let app = try ghosttyApplication() + app.launch() + try assertTitlebarAppearance(.dark, for: app) + // create a split + app.groups["Terminal pane"].typeKey("d", modifierFlags: .command) + // reload config + app.typeKey(",", modifierFlags: [.command, .shift]) + try await Task.sleep(for: .seconds(0.5)) + // create a new window + app.typeKey("n", modifierFlags: [.command]) + try assertTitlebarAppearance(.dark, for: app) + } + + @MainActor + func testLightTransparentWindowThemeWithDarkTerminal() async throws { + try updateConfig("title=\(windowTitle) \n window-theme=light") + let app = try ghosttyApplication() + app.launch() + try await Task.sleep(for: .seconds(0.5)) + try assertTitlebarAppearance(.dark, for: app) + } + + @MainActor + func testLightNativeWindowThemeWithDarkTerminal() async throws { + try updateConfig("title=\(windowTitle) \n window-theme = light \n macos-titlebar-style = native") + let app = try ghosttyApplication() + app.launch() + try assertTitlebarAppearance(.light, for: app) + } + + @MainActor + func testReloadingLightTransparentWindowTheme() async throws { + try updateConfig("title=\(windowTitle) \n ") + let app = try ghosttyApplication() + app.launch() + // default dark theme + try assertTitlebarAppearance(.dark, for: app) + try updateConfig("title=\(windowTitle) \n theme=light:3024 Day,dark:3024 Night \n window-theme = light") + // reload config + app.typeKey(",", modifierFlags: [.command, .shift]) + try await Task.sleep(for: .seconds(0.5)) + try assertTitlebarAppearance(.light, for: app) + } + + @MainActor + func testSwitchingSystemTheme() async throws { + try updateConfig("title=\(windowTitle) \n theme=light:3024 Day,dark:3024 Night") + XCUIDevice.shared.appearance = .dark + let app = try ghosttyApplication() + app.launch() + try assertTitlebarAppearance(.dark, for: app) + XCUIDevice.shared.appearance = .light + try await Task.sleep(for: .seconds(0.5)) + try assertTitlebarAppearance(.light, for: app) + } + + @MainActor + func testReloadFromLightWindowThemeToDefaultTheme() async throws { + try updateConfig("title=\(windowTitle) \n theme=light:3024 Day,dark:3024 Night") + XCUIDevice.shared.appearance = .light + let app = try ghosttyApplication() + app.launch() + try assertTitlebarAppearance(.light, for: app) + try updateConfig("title=\(windowTitle) \n ") + // reload config + app.typeKey(",", modifierFlags: [.command, .shift]) + try await Task.sleep(for: .seconds(0.5)) + try assertTitlebarAppearance(.dark, for: app) + } + + @MainActor + func testReloadFromDefaultThemeToDarkWindowTheme() async throws { + try updateConfig("title=\(windowTitle) \n ") + XCUIDevice.shared.appearance = .light + let app = try ghosttyApplication() + app.launch() + try assertTitlebarAppearance(.dark, for: app) + try updateConfig("title=\(windowTitle) \n theme=light:3024 Day,dark:3024 Night \n window-theme=dark") + // reload config + app.typeKey(",", modifierFlags: [.command, .shift]) + try await Task.sleep(for: .seconds(0.5)) + try assertTitlebarAppearance(.dark, for: app) + } + + @MainActor + func testReloadingFromDarkThemeToSystemLightTheme() async throws { + try updateConfig("title=\(windowTitle) \n theme=light:3024 Day,dark:3024 Night \n window-theme=dark") + XCUIDevice.shared.appearance = .light + let app = try ghosttyApplication() + app.launch() + try assertTitlebarAppearance(.dark, for: app) + try updateConfig("title=\(windowTitle) \n theme=light:3024 Day,dark:3024 Night") + // reload config + app.typeKey(",", modifierFlags: [.command, .shift]) + try await Task.sleep(for: .seconds(0.5)) + try assertTitlebarAppearance(.light, for: app) + } + + @MainActor + func testQuickTerminalThemeChange() async throws { + try updateConfig("title=\(windowTitle) \n theme=light:3024 Day,dark:3024 Night \n confirm-close-surface=false") + XCUIDevice.shared.appearance = .light + let app = try ghosttyApplication() + app.launch() + // close default window + app.typeKey("w", modifierFlags: [.command]) + // open quick terminal + app.menuBarItems["View"].firstMatch.click() + app.menuItems["Quick Terminal"].firstMatch.click() + let title = "Debug builds of Ghostty are very slow and you may experience performance problems. Debug builds are only recommended during development." + try assertTitlebarAppearance(.light, for: app, title: title, colorLocation: CGPoint(x: 5, y: 5)) // to avoid dark edge + XCUIDevice.shared.appearance = .dark + try await Task.sleep(for: .seconds(0.5)) + try assertTitlebarAppearance(.dark, for: app, title: title, colorLocation: CGPoint(x: 5, y: 5)) + } +} diff --git a/macos/GhosttyUITests/GhosttyTitleUITests.swift b/macos/GhosttyUITests/GhosttyTitleUITests.swift new file mode 100644 index 000000000..01bc64023 --- /dev/null +++ b/macos/GhosttyUITests/GhosttyTitleUITests.swift @@ -0,0 +1,23 @@ +// +// GhosttyTitleUITests.swift +// GhosttyUITests +// +// Created by luca on 13.10.2025. +// + +import XCTest + +final class GhosttyTitleUITests: GhosttyCustomConfigCase { + override func setUp() async throws { + try await super.setUp() + try updateConfig(#"title = "GhosttyUITestsLaunchTests""#) + } + + @MainActor + func testTitle() throws { + let app = try ghosttyApplication() + app.launch() + + XCTAssertEqual(app.windows.firstMatch.title, "GhosttyUITestsLaunchTests", "Oops, `title=` doesn't work!") + } +} diff --git a/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift b/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift new file mode 100644 index 000000000..bf8b6124e --- /dev/null +++ b/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift @@ -0,0 +1,143 @@ +// +// GhosttyTitlebarTabsUITests.swift +// Ghostty +// +// Created by luca on 16.10.2025. +// + +import XCTest + +final class GhosttyTitlebarTabsUITests: GhosttyCustomConfigCase { + override func setUp() async throws { + try await super.setUp() + + try updateConfig( + """ + macos-titlebar-style = tabs + title = "GhosttyTitlebarTabsUITests" + """ + ) + } + + @MainActor + func testCustomTitlebar() throws { + let app = try ghosttyApplication() + app.launch() + // create a split + app.groups["Terminal pane"].typeKey("d", modifierFlags: .command) + app.typeKey("\n", modifierFlags: [.command, .shift]) + let resetZoomButton = app.groups.buttons["ResetZoom"] + let windowTitle = app.windows.firstMatch.title + let titleView = app.staticTexts.element(matching: NSPredicate(format: "value == '\(windowTitle)'")) + + XCTAssertEqual(titleView.frame.midY, resetZoomButton.frame.midY, accuracy: 1, "Window title should be vertically centered with reset zoom button: \(titleView.frame.midY) != \(resetZoomButton.frame.midY)") + } + + @MainActor + func testTabsGeometryInNormalWindow() throws { + let app = try ghosttyApplication() + app.launch() + app.groups["Terminal pane"].typeKey("t", modifierFlags: .command) + XCTAssertEqual(app.tabs.count, 2, "There should be 2 tabs") + checkTabsGeometry(app.windows.firstMatch) + } + + @MainActor + func testTabsGeometryInFullscreen() throws { + let app = try ghosttyApplication() + app.launch() + app.typeKey("f", modifierFlags: [.command, .control]) + // using app to type ⌘+t might not be able to create tabs + app.groups["Terminal pane"].typeKey("t", modifierFlags: .command) + XCTAssertEqual(app.tabs.count, 2, "There should be 2 tabs") + checkTabsGeometry(app.windows.firstMatch) + } + + @MainActor + func testTabsGeometryAfterMovingTabs() throws { + let app = try ghosttyApplication() + app.launch() + XCTAssertTrue(app.windows.firstMatch.waitForExistence(timeout: 1), "Main window should exist") + // create another 2 tabs + app.groups["Terminal pane"].typeKey("t", modifierFlags: .command) + app.groups["Terminal pane"].typeKey("t", modifierFlags: .command) + + // move to the left + app.menuItems["_zoomLeft:"].firstMatch.click() + + // create another window with 2 tabs + app.windows.firstMatch.groups["Terminal pane"].typeKey("n", modifierFlags: .command) + XCTAssertEqual(app.windows.count, 2, "There should be 2 windows") + + // move to the right + app.menuItems["_zoomRight:"].firstMatch.click() + + // now second window is the first/main one in the list + app.windows.firstMatch.groups["Terminal pane"].typeKey("t", modifierFlags: .command) + + app.windows.element(boundBy: 1).tabs.firstMatch.click() // focus first window + + // now the first window is the main one + let firstTabInFirstWindow = app.windows.firstMatch.tabs.firstMatch + let firstTabInSecondWindow = app.windows.element(boundBy: 1).tabs.firstMatch + + // drag a tab from one window to another + firstTabInFirstWindow.press(forDuration: 0.2, thenDragTo: firstTabInSecondWindow) + + // check tabs in the first + checkTabsGeometry(app.windows.firstMatch) + // focus another window + app.windows.element(boundBy: 1).tabs.firstMatch.click() + checkTabsGeometry(app.windows.firstMatch) + } + + @MainActor + func testTabsGeometryAfterMergingAllWindows() throws { + let app = try ghosttyApplication() + app.launch() + XCTAssertTrue(app.windows.firstMatch.waitForExistence(timeout: 1), "Main window should exist") + + // create another 2 windows + app.typeKey("n", modifierFlags: .command) + app.typeKey("n", modifierFlags: .command) + + // merge into one window, resulting 3 tabs + app.menuItems["mergeAllWindows:"].firstMatch.click() + + XCTAssertTrue(app.wait(for: \.tabs.count, toEqual: 3, timeout: 1), "There should be 3 tabs") + checkTabsGeometry(app.windows.firstMatch) + } + + func checkTabsGeometry(_ window: XCUIElement) { + let closeTabButtons = window.buttons.matching(identifier: "_closeButton") + + XCTAssertEqual(closeTabButtons.count, window.tabs.count, "Close tab buttons count should match tabs count") + + var previousTabHeight: CGFloat? + for idx in 0 ..< window.tabs.count { + let currentTab = window.tabs.element(boundBy: idx) + // focus + currentTab.click() + // switch to the tab + window.typeKey("\(idx + 1)", modifierFlags: .command) + // add a split + window.typeKey("d", modifierFlags: .command) + // zoom this split + // haven't found a way to locate our reset zoom button yet.. + window.typeKey("\n", modifierFlags: [.command, .shift]) + window.typeKey("\n", modifierFlags: [.command, .shift]) + + if let previousHeight = previousTabHeight { + XCTAssertEqual(currentTab.frame.height, previousHeight, accuracy: 1, "The tab's height should stay the same") + } + previousTabHeight = currentTab.frame.height + + let titleFrame = currentTab.frame + let shortcutLabelFrame = window.staticTexts.element(matching: NSPredicate(format: "value CONTAINS[c] '⌘\(idx + 1)'")).firstMatch.frame + let closeButtonFrame = closeTabButtons.element(boundBy: idx).frame + + XCTAssertEqual(titleFrame.midY, shortcutLabelFrame.midY, accuracy: 1, "Tab title should be vertically centered with its shortcut label: \(titleFrame.midY) != \(shortcutLabelFrame.midY)") + XCTAssertEqual(titleFrame.midY, closeButtonFrame.midY, accuracy: 1, "Tab title should be vertically centered with its close button: \(titleFrame.midY) != \(closeButtonFrame.midY)") + } + } +} diff --git a/macos/Sources/App/iOS/iOSApp.swift b/macos/Sources/App/iOS/iOSApp.swift index 4af94491c..a1aafcc7d 100644 --- a/macos/Sources/App/iOS/iOSApp.swift +++ b/macos/Sources/App/iOS/iOSApp.swift @@ -1,8 +1,16 @@ import SwiftUI +import GhosttyKit @main struct Ghostty_iOSApp: App { - @StateObject private var ghostty_app = Ghostty.App() + @StateObject private var ghostty_app: Ghostty.App + + init() { + if ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv) != GHOSTTY_SUCCESS { + preconditionFailure("Initialize ghostty backend failed") + } + _ghostty_app = StateObject(wrappedValue: Ghostty.App()) + } var body: some Scene { WindowGroup { diff --git a/macos/Sources/App/macOS/AppDelegate+Ghostty.swift b/macos/Sources/App/macOS/AppDelegate+Ghostty.swift new file mode 100644 index 000000000..4d798a1a5 --- /dev/null +++ b/macos/Sources/App/macOS/AppDelegate+Ghostty.swift @@ -0,0 +1,23 @@ +import AppKit + +// MARK: Ghostty Delegate + +/// This implements the Ghostty app delegate protocol which is used by the Ghostty +/// APIs for app-global information. +extension AppDelegate: Ghostty.Delegate { + func ghosttySurface(id: UUID) -> Ghostty.SurfaceView? { + for window in NSApp.windows { + guard let controller = window.windowController as? BaseTerminalController else { + continue + } + + for surface in controller.surfaceTree { + if surface.id == id { + return surface + } + } + } + + return nil + } +} diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 192135c15..381fb9db2 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -46,6 +46,8 @@ class AppDelegate: NSObject, @IBOutlet private var menuSelectAll: NSMenuItem? @IBOutlet private var menuFindParent: NSMenuItem? @IBOutlet private var menuFind: NSMenuItem? + @IBOutlet private var menuSelectionForFind: NSMenuItem? + @IBOutlet private var menuScrollToSelection: NSMenuItem? @IBOutlet private var menuFindNext: NSMenuItem? @IBOutlet private var menuFindPrevious: NSMenuItem? @IBOutlet private var menuHideFindBar: NSMenuItem? @@ -68,6 +70,8 @@ class AppDelegate: NSObject, @IBOutlet private var menuDecreaseFontSize: NSMenuItem? @IBOutlet private var menuResetFontSize: NSMenuItem? @IBOutlet private var menuChangeTitle: NSMenuItem? + @IBOutlet private var menuChangeTabTitle: NSMenuItem? + @IBOutlet private var menuReadonly: NSMenuItem? @IBOutlet private var menuQuickTerminal: NSMenuItem? @IBOutlet private var menuTerminalInspector: NSMenuItem? @IBOutlet private var menuCommandPalette: NSMenuItem? @@ -92,16 +96,40 @@ class AppDelegate: NSObject, private var derivedConfig: DerivedConfig = DerivedConfig() /// The ghostty global state. Only one per process. - let ghostty: Ghostty.App = Ghostty.App() + let ghostty: Ghostty.App /// The global undo manager for app-level state such as window restoration. lazy var undoManager = ExpiringUndoManager() + /// The current state of the quick terminal. + private var quickTerminalControllerState: QuickTerminalState = .uninitialized + /// Our quick terminal. This starts out uninitialized and only initializes if used. - private(set) lazy var quickController = QuickTerminalController( - ghostty, - position: derivedConfig.quickTerminalPosition - ) + var quickController: QuickTerminalController { + switch quickTerminalControllerState { + case .initialized(let controller): + return controller + + case .pendingRestore(let state): + let controller = QuickTerminalController( + ghostty, + position: derivedConfig.quickTerminalPosition, + baseConfig: state.baseConfig, + restorationState: state + ) + quickTerminalControllerState = .initialized(controller) + return controller + + case .uninitialized: + let controller = QuickTerminalController( + ghostty, + position: derivedConfig.quickTerminalPosition, + restorationState: nil + ) + quickTerminalControllerState = .initialized(controller) + return controller + } + } /// Manages updates let updateController = UpdateController() @@ -127,6 +155,11 @@ class AppDelegate: NSObject, @Published private(set) var appIcon: NSImage? = nil override init() { +#if DEBUG + ghostty = Ghostty.App(configPath: ProcessInfo.processInfo.environment["GHOSTTY_CONFIG_PATH"]) +#else + ghostty = Ghostty.App() +#endif super.init() ghostty.delegate = self @@ -541,8 +574,9 @@ class AppDelegate: NSObject, self.menuDecreaseFontSize?.setImageIfDesired(systemSymbolName: "textformat.size.smaller") self.menuCommandPalette?.setImageIfDesired(systemSymbolName: "filemenu.and.selection") self.menuQuickTerminal?.setImageIfDesired(systemSymbolName: "apple.terminal") - self.menuChangeTitle?.setImageIfDesired(systemSymbolName: "pencil.line") + self.menuChangeTabTitle?.setImageIfDesired(systemSymbolName: "pencil.line") self.menuTerminalInspector?.setImageIfDesired(systemSymbolName: "scope") + self.menuReadonly?.setImageIfDesired(systemSymbolName: "eye.fill") self.menuToggleFullScreen?.setImageIfDesired(systemSymbolName: "square.arrowtriangle.4.outward") self.menuToggleVisibility?.setImageIfDesired(systemSymbolName: "eye") self.menuZoomSplit?.setImageIfDesired(systemSymbolName: "arrow.up.left.and.arrow.down.right") @@ -588,6 +622,8 @@ class AppDelegate: NSObject, syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection) syncMenuShortcut(config, action: "select_all", menuItem: self.menuSelectAll) syncMenuShortcut(config, action: "start_search", menuItem: self.menuFind) + syncMenuShortcut(config, action: "search_selection", menuItem: self.menuSelectionForFind) + syncMenuShortcut(config, action: "scroll_to_selection", menuItem: self.menuScrollToSelection) syncMenuShortcut(config, action: "search:next", menuItem: self.menuFindNext) syncMenuShortcut(config, action: "search:previous", menuItem: self.menuFindPrevious) @@ -609,6 +645,7 @@ class AppDelegate: NSObject, syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize) syncMenuShortcut(config, action: "reset_font_size", menuItem: self.menuResetFontSize) syncMenuShortcut(config, action: "prompt_surface_title", menuItem: self.menuChangeTitle) + syncMenuShortcut(config, action: "prompt_tab_title", menuItem: self.menuChangeTabTitle) syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal) syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility) syncMenuShortcut(config, action: "toggle_window_float_on_top", menuItem: self.menuFloatOnTop) @@ -657,6 +694,18 @@ class AppDelegate: NSObject, } private func localEventKeyDown(_ event: NSEvent) -> NSEvent? { + // If the tab overview is visible and escape is pressed, close it. + // This can't POSSIBLY be right and is probably a FirstResponder problem + // that we should handle elsewhere in our program. But this works and it + // is guarded by the tab overview currently showing. + if event.keyCode == 0x35, // Escape key + let window = NSApp.keyWindow, + let tabGroup = window.tabGroup, + tabGroup.isOverviewVisible { + window.toggleTabOverview(nil) + return nil + } + // If we have a main window then we don't process any of the keys // because we let it capture and propagate. guard NSApp.mainWindow == nil else { return event } @@ -902,33 +951,8 @@ class AppDelegate: NSObject, var appIconName: String? = config.macosIcon.rawValue switch (config.macosIcon) { - case .official: - // Discard saved icon name - appIconName = nil - break - case .blueprint: - appIcon = NSImage(named: "BlueprintImage")! - - case .chalkboard: - appIcon = NSImage(named: "ChalkboardImage")! - - case .glass: - appIcon = NSImage(named: "GlassImage")! - - case .holographic: - appIcon = NSImage(named: "HolographicImage")! - - case .microchip: - appIcon = NSImage(named: "MicrochipImage")! - - case .paper: - appIcon = NSImage(named: "PaperImage")! - - case .retro: - appIcon = NSImage(named: "RetroImage")! - - case .xray: - appIcon = NSImage(named: "XrayImage")! + case let icon where icon.assetName != nil: + appIcon = NSImage(named: icon.assetName!)! case .custom: if let userIcon = NSImage(contentsOfFile: config.macosCustomIcon) { @@ -938,6 +962,7 @@ class AppDelegate: NSObject, appIcon = nil // Revert back to official icon if invalid location appIconName = nil // Discard saved icon name } + case .customStyle: // Discard saved icon name // if no valid colours were found @@ -953,10 +978,20 @@ class AppDelegate: NSObject, let colorStrings = ([ghostColor] + screenColors).compactMap(\.hexString) appIconName = (colorStrings + [config.macosIconFrame.rawValue]) .joined(separator: "_") + + default: + // Discard saved icon name + appIconName = nil } - // Only change the icon if it has actually changed - // from the current one - guard UserDefaults.standard.string(forKey: "CustomGhosttyIcon") != appIconName else { + + // Only change the icon if it has actually changed from the current one, + // or if the app build has changed (e.g. after an update that reset the icon) + let cachedIconName = UserDefaults.standard.string(forKey: "CustomGhosttyIcon") + let cachedIconBuild = UserDefaults.standard.string(forKey: "CustomGhosttyIconBuild") + let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String + let buildChanged = cachedIconBuild != currentBuild + + guard cachedIconName != appIconName || buildChanged else { #if DEBUG if appIcon == nil { await MainActor.run { @@ -973,14 +1008,16 @@ class AppDelegate: NSObject, let newIcon = appIcon let appPath = Bundle.main.bundlePath - NSWorkspace.shared.setIcon(newIcon, forFile: appPath, options: []) + guard NSWorkspace.shared.setIcon(newIcon, forFile: appPath, options: []) else { return } NSWorkspace.shared.noteFileSystemChanged(appPath) await MainActor.run { self.appIcon = newIcon NSApplication.shared.applicationIconImage = newIcon } + UserDefaults.standard.set(appIconName, forKey: "CustomGhosttyIcon") + UserDefaults.standard.set(currentBuild, forKey: "CustomGhosttyIconBuild") } //MARK: - Restorable State @@ -992,10 +1029,31 @@ class AppDelegate: NSObject, func application(_ app: NSApplication, willEncodeRestorableState coder: NSCoder) { Self.logger.debug("application will save window state") + + guard ghostty.config.windowSaveState != "never" else { return } + + // Encode our quick terminal state if we have it. + switch quickTerminalControllerState { + case .initialized(let controller) where controller.restorable: + let data = QuickTerminalRestorableState(from: controller) + data.encode(with: coder) + + case .pendingRestore(let state): + state.encode(with: coder) + + default: + break + } } func application(_ app: NSApplication, didDecodeRestorableState coder: NSCoder) { Self.logger.debug("application will restore window state") + + // Decode our quick terminal state. + if ghostty.config.windowSaveState != "never", + let state = QuickTerminalRestorableState(coder: coder) { + quickTerminalControllerState = .pendingRestore(state) + } } //MARK: - UNUserNotificationCenterDelegate @@ -1269,6 +1327,16 @@ extension AppDelegate: NSMenuItemValidation { } } +/// Represents the state of the quick terminal controller. +private enum QuickTerminalState { + /// Controller has not been initialized and has no pending restoration state. + case uninitialized + /// Restoration state is pending; controller will use this when first accessed. + case pendingRestore(QuickTerminalRestorableState) + /// Controller has been initialized. + case initialized(QuickTerminalController) +} + @globalActor fileprivate actor AppIconActor: GlobalActor { static let shared = AppIconActor() diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index 3e1084cd7..e28344098 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -16,6 +16,7 @@ + @@ -46,6 +47,7 @@ + @@ -56,6 +58,7 @@ + @@ -279,6 +282,19 @@ + + + + + + + + + + + + + @@ -315,12 +331,24 @@ - + + + + + + + + + + + + + diff --git a/macos/Sources/Features/About/AboutView.swift b/macos/Sources/Features/About/AboutView.swift index 6ed3285ed..967eb16b0 100644 --- a/macos/Sources/Features/About/AboutView.swift +++ b/macos/Sources/Features/About/AboutView.swift @@ -44,10 +44,7 @@ struct AboutView: View { var body: some View { VStack(alignment: .center) { - ghosttyIconImage() - .resizable() - .aspectRatio(contentMode: .fit) - .frame(height: 128) + CyclingIconView() VStack(alignment: .center, spacing: 32) { VStack(alignment: .center, spacing: 8) { diff --git a/macos/Sources/Features/About/CyclingIconView.swift b/macos/Sources/Features/About/CyclingIconView.swift new file mode 100644 index 000000000..4274278e0 --- /dev/null +++ b/macos/Sources/Features/About/CyclingIconView.swift @@ -0,0 +1,62 @@ +import SwiftUI +import GhosttyKit + +/// A view that cycles through Ghostty's official icon variants. +struct CyclingIconView: View { + @State private var currentIcon: Ghostty.MacOSIcon = .official + @State private var isHovering: Bool = false + + private let icons: [Ghostty.MacOSIcon] = [ + .official, + .blueprint, + .chalkboard, + .microchip, + .glass, + .holographic, + .paper, + .retro, + .xray, + ] + private let timerPublisher = Timer.publish(every: 3, on: .main, in: .common) + + var body: some View { + ZStack { + iconView(for: currentIcon) + .id(currentIcon) + } + .animation(.easeInOut(duration: 0.5), value: currentIcon) + .frame(height: 128) + .onReceive(timerPublisher.autoconnect()) { _ in + if !isHovering { + advanceToNextIcon() + } + } + .onHover { hovering in + isHovering = hovering + } + .onTapGesture { + advanceToNextIcon() + } + .help("macos-icon = \(currentIcon.rawValue)") + .accessibilityLabel("Ghostty Application Icon") + .accessibilityHint("Click to cycle through icon variants") + } + + @ViewBuilder + private func iconView(for icon: Ghostty.MacOSIcon) -> some View { + let iconImage: Image = switch icon.assetName { + case let assetName?: Image(assetName) + case nil: ghosttyIconImage() + } + + iconImage + .resizable() + .aspectRatio(contentMode: .fit) + } + + private func advanceToNextIcon() { + let currentIndex = icons.firstIndex(of: currentIcon) ?? 0 + let nextIndex = icons.indexWrapping(after: currentIndex) + currentIcon = icons[nextIndex] + } +} diff --git a/macos/Sources/Features/App Intents/Entities/CommandEntity.swift b/macos/Sources/Features/App Intents/Entities/CommandEntity.swift index f7abcc6de..3c7745e7c 100644 --- a/macos/Sources/Features/App Intents/Entities/CommandEntity.swift +++ b/macos/Sources/Features/App Intents/Entities/CommandEntity.swift @@ -1,4 +1,5 @@ import AppIntents +import Cocoa // MARK: AppEntity @@ -94,23 +95,23 @@ struct CommandQuery: EntityQuery { @MainActor func entities(for identifiers: [CommandEntity.ID]) async throws -> [CommandEntity] { + guard let appDelegate = NSApp.delegate as? AppDelegate else { return [] } + let commands = appDelegate.ghostty.config.commandPaletteEntries + // Extract unique terminal IDs to avoid fetching duplicates let terminalIds = Set(identifiers.map(\.terminalId)) let terminals = try await TerminalEntity.defaultQuery.entities(for: Array(terminalIds)) - // Build a cache of terminals and their available commands - // This avoids repeated command fetching for the same terminal - typealias Tuple = (terminal: TerminalEntity, commands: [Ghostty.Command]) - let commandMap: [TerminalEntity.ID: Tuple] = + // Build a lookup from terminal ID to terminal entity + let terminalMap: [TerminalEntity.ID: TerminalEntity] = terminals.reduce(into: [:]) { result, terminal in - guard let commands = try? terminal.surfaceModel?.commands() else { return } - result[terminal.id] = (terminal: terminal, commands: commands) + result[terminal.id] = terminal } - + // Map each identifier to its corresponding CommandEntity. If a command doesn't // exist it maps to nil and is removed via compactMap. return identifiers.compactMap { id in - guard let (terminal, commands) = commandMap[id.terminalId], + guard let terminal = terminalMap[id.terminalId], let command = commands.first(where: { $0.actionKey == id.actionKey }) else { return nil } @@ -121,8 +122,8 @@ struct CommandQuery: EntityQuery { @MainActor func suggestedEntities() async throws -> [CommandEntity] { - guard let terminal = commandPaletteIntent?.terminal, - let surface = terminal.surfaceModel else { return [] } - return try surface.commands().map { CommandEntity($0, for: terminal) } + guard let appDelegate = NSApp.delegate as? AppDelegate, + let terminal = commandPaletteIntent?.terminal else { return [] } + return appDelegate.ghostty.config.commandPaletteEntries.map { CommandEntity($0, for: terminal) } } } diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 79c3ca756..235881dde 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -1,30 +1,50 @@ import SwiftUI struct CommandOption: Identifiable, Hashable { + /// Unique identifier for this option. let id = UUID() + /// The primary text displayed for this command. let title: String + /// Secondary text displayed below the title. + let subtitle: String? + /// Tooltip text shown on hover. let description: String? + /// Keyboard shortcut symbols to display. let symbols: [String]? + /// SF Symbol name for the leading icon. let leadingIcon: String? + /// Color for the leading indicator circle. + let leadingColor: Color? + /// Badge text displayed as a pill. let badge: String? + /// Whether to visually emphasize this option. let emphasis: Bool + /// Sort key for stable ordering when titles are equal. + let sortKey: AnySortKey? + /// The action to perform when this option is selected. let action: () -> Void init( title: String, + subtitle: String? = nil, description: String? = nil, symbols: [String]? = nil, leadingIcon: String? = nil, + leadingColor: Color? = nil, badge: String? = nil, emphasis: Bool = false, + sortKey: AnySortKey? = nil, action: @escaping () -> Void ) { self.title = title + self.subtitle = subtitle self.description = description self.symbols = symbols self.leadingIcon = leadingIcon + self.leadingColor = leadingColor self.badge = badge self.emphasis = emphasis + self.sortKey = sortKey self.action = action } @@ -47,12 +67,24 @@ struct CommandPaletteView: View { @FocusState private var isTextFieldFocused: Bool // The options that we should show, taking into account any filtering from - // the query. + // the query. Options with matching leadingColor are ranked higher. var filteredOptions: [CommandOption] { if query.isEmpty { return options } else { - return options.filter { $0.title.localizedCaseInsensitiveContains(query) } + // Filter by title/subtitle match OR color match + let filtered = options.filter { + $0.title.localizedCaseInsensitiveContains(query) || + ($0.subtitle?.localizedCaseInsensitiveContains(query) ?? false) || + colorMatchScore(for: $0.leadingColor, query: query) > 0 + } + + // Sort by color match score (higher scores first), then maintain original order + return filtered.sorted { a, b in + let scoreA = colorMatchScore(for: a.leadingColor, query: query) + let scoreB = colorMatchScore(for: b.leadingColor, query: query) + return scoreA > scoreB + } } } @@ -168,6 +200,32 @@ struct CommandPaletteView: View { isTextFieldFocused = isPresented } } + + /// Returns a score (0.0 to 1.0) indicating how well a color matches a search query color name. + /// Returns 0 if no color name in the query matches, or if the color is nil. + private func colorMatchScore(for color: Color?, query: String) -> Double { + guard let color = color else { return 0 } + + let queryLower = query.lowercased() + let nsColor = NSColor(color) + + var bestScore: Double = 0 + for name in NSColor.colorNames { + guard queryLower.contains(name), + let systemColor = NSColor(named: name) else { continue } + + let distance = nsColor.distance(to: systemColor) + // Max distance in weighted RGB space is ~3.0, so normalize and invert + // Use a threshold to determine "close enough" matches + let maxDistance: Double = 1.5 + if distance < maxDistance { + let score = 1.0 - (distance / maxDistance) + bestScore = max(bestScore, score) + } + } + + return bestScore + } } /// The text field for building the query for the command palette. @@ -283,14 +341,28 @@ fileprivate struct CommandRow: View { var body: some View { Button(action: action) { HStack(spacing: 8) { + if let color = option.leadingColor { + Circle() + .fill(color) + .frame(width: 8, height: 8) + } + if let icon = option.leadingIcon { Image(systemName: icon) .foregroundStyle(option.emphasis ? Color.accentColor : .secondary) .font(.system(size: 14, weight: .medium)) } - Text(option.title) - .fontWeight(option.emphasis ? .medium : .regular) + VStack(alignment: .leading, spacing: 2) { + Text(option.title) + .fontWeight(option.emphasis ? .medium : .regular) + + if let subtitle = option.subtitle { + Text(subtitle) + .font(.caption) + .foregroundStyle(.secondary) + } + } Spacer() diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index 96ff3d0c1..e0237f257 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -18,63 +18,6 @@ struct TerminalCommandPaletteView: View { /// The callback when an action is submitted. var onAction: ((String) -> Void) - // The commands available to the command palette. - private var commandOptions: [CommandOption] { - var options: [CommandOption] = [] - - // Add update command if an update is installable. This must always be the first so - // it is at the top. - if let updateViewModel, updateViewModel.state.isInstallable { - // We override the update available one only because we want to properly - // convey it'll go all the way through. - let title: String - if case .updateAvailable = updateViewModel.state { - title = "Update Ghostty and Restart" - } else { - title = updateViewModel.text - } - - options.append(CommandOption( - title: title, - description: updateViewModel.description, - leadingIcon: updateViewModel.iconName ?? "shippingbox.fill", - badge: updateViewModel.badge, - emphasis: true - ) { - (NSApp.delegate as? AppDelegate)?.updateController.installUpdate() - }) - } - - // Add cancel/skip update command if the update is installable - if let updateViewModel, updateViewModel.state.isInstallable { - options.append(CommandOption( - title: "Cancel or Skip Update", - description: "Dismiss the current update process" - ) { - updateViewModel.state.cancel() - }) - } - - // Add terminal commands - guard let surface = surfaceView.surfaceModel else { return options } - do { - let terminalCommands = try surface.commands().map { c in - return CommandOption( - title: c.title, - description: c.description, - symbols: ghosttyConfig.keyboardShortcut(for: c.action)?.keyList - ) { - onAction(c.action) - } - } - options.append(contentsOf: terminalCommands) - } catch { - return options - } - - return options - } - var body: some View { ZStack { if isPresented { @@ -96,13 +39,8 @@ struct TerminalCommandPaletteView: View { } .frame(width: geometry.size.width, height: geometry.size.height, alignment: .top) } - .transition( - .move(edge: .top) - .combined(with: .opacity) - ) } } - .animation(.spring(response: 0.4, dampingFraction: 0.8), value: isPresented) .onChange(of: isPresented) { newValue in // When the command palette disappears we need to send focus back to the // surface view we were overlaid on top of. There's probably a better way @@ -116,6 +54,116 @@ struct TerminalCommandPaletteView: View { } } } + + /// All commands available in the command palette, combining update and terminal options. + private var commandOptions: [CommandOption] { + var options: [CommandOption] = [] + // Updates always appear first + options.append(contentsOf: updateOptions) + + // Sort the rest. We replace ":" with a character that sorts before space + // so that "Foo:" sorts before "Foo Bar:". Use sortKey as a tie-breaker + // for stable ordering when titles are equal. + options.append(contentsOf: (jumpOptions + terminalOptions).sorted { a, b in + let aNormalized = a.title.replacingOccurrences(of: ":", with: "\t") + let bNormalized = b.title.replacingOccurrences(of: ":", with: "\t") + let comparison = aNormalized.localizedCaseInsensitiveCompare(bNormalized) + if comparison != .orderedSame { + return comparison == .orderedAscending + } + // Tie-breaker: use sortKey if both have one + if let aSortKey = a.sortKey, let bSortKey = b.sortKey { + return aSortKey < bSortKey + } + return false + }) + return options + } + + /// Commands for installing or canceling available updates. + private var updateOptions: [CommandOption] { + var options: [CommandOption] = [] + + guard let updateViewModel, updateViewModel.state.isInstallable else { + return options + } + + // We override the update available one only because we want to properly + // convey it'll go all the way through. + let title: String + if case .updateAvailable = updateViewModel.state { + title = "Update Ghostty and Restart" + } else { + title = updateViewModel.text + } + + options.append(CommandOption( + title: title, + description: updateViewModel.description, + leadingIcon: updateViewModel.iconName ?? "shippingbox.fill", + badge: updateViewModel.badge, + emphasis: true + ) { + (NSApp.delegate as? AppDelegate)?.updateController.installUpdate() + }) + + options.append(CommandOption( + title: "Cancel or Skip Update", + description: "Dismiss the current update process" + ) { + updateViewModel.state.cancel() + }) + + return options + } + + /// Custom commands from the command-palette-entry configuration. + private var terminalOptions: [CommandOption] { + guard let appDelegate = NSApp.delegate as? AppDelegate else { return [] } + return appDelegate.ghostty.config.commandPaletteEntries.map { c in + CommandOption( + title: c.title, + description: c.description + ) { + onAction(c.action) + } + } + } + + /// Commands for jumping to other terminal surfaces. + private var jumpOptions: [CommandOption] { + TerminalController.all.flatMap { controller -> [CommandOption] in + guard let window = controller.window else { return [] } + + let color = (window as? TerminalWindow)?.tabColor + let displayColor = color != TerminalTabColor.none ? color : nil + + return controller.surfaceTree.map { surface in + let title = surface.title.isEmpty ? window.title : surface.title + let displayTitle = title.isEmpty ? "Untitled" : title + let pwd = surface.pwd?.abbreviatedPath + let subtitle: String? = if let pwd, !displayTitle.contains(pwd) { + pwd + } else { + nil + } + + return CommandOption( + title: "Focus: \(displayTitle)", + subtitle: subtitle, + leadingIcon: "rectangle.on.rectangle", + leadingColor: displayColor?.displayColor.map { Color($0) }, + sortKey: AnySortKey(ObjectIdentifier(surface)) + ) { + NotificationCenter.default.post( + name: Ghostty.Notification.ghosttyPresentTerminal, + object: surface + ) + } + } + } + } + } /// This is done to ensure that the given view is in the responder chain. diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 4c2052f23..07c0c4c19 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -22,7 +22,7 @@ class QuickTerminalController: BaseTerminalController { private var previousActiveSpace: CGSSpace? = nil /// Cache for per-screen window state. - private let screenStateCache = QuickTerminalScreenStateCache() + let screenStateCache: QuickTerminalScreenStateCache /// Non-nil if we have hidden dock state. private var hiddenDock: HiddenDock? = nil @@ -32,15 +32,27 @@ class QuickTerminalController: BaseTerminalController { /// Tracks if we're currently handling a manual resize to prevent recursion private var isHandlingResize: Bool = false - + + /// This is set to false by init if the window managed by this controller should not be restorable. + /// For example, terminals executing custom scripts are not restorable. + let restorable: Bool + private var restorationState: QuickTerminalRestorableState? + init(_ ghostty: Ghostty.App, position: QuickTerminalPosition = .top, baseConfig base: Ghostty.SurfaceConfiguration? = nil, - surfaceTree tree: SplitTree? = nil + restorationState: QuickTerminalRestorableState? = nil, ) { self.position = position self.derivedConfig = DerivedConfig(ghostty.config) - + // The window we manage is not restorable if we've specified a command + // to execute. We do this because the restored window is meaningless at the + // time of writing this: it'd just restore to a shell in the same directory + // as the script. We may want to revisit this behavior when we have scrollback + // restoration. + restorable = (base?.command ?? "") == "" + self.restorationState = restorationState + self.screenStateCache = QuickTerminalScreenStateCache(stateByDisplay: restorationState?.screenStateEntries ?? [:]) // Important detail here: we initialize with an empty surface tree so // that we don't start a terminal process. This gets started when the // first terminal is shown in `animateIn`. @@ -104,8 +116,8 @@ class QuickTerminalController: BaseTerminalController { // window close so we can animate out. window.delegate = self - // The quick window is not restorable (yet!). "Yet" because in theory we can - // make this restorable, but it isn't currently implemented. + // The quick window is restored by `screenStateCache`. + // We disable this for better control window.isRestorable = false // Setup our configured appearance that we support. @@ -125,11 +137,11 @@ class QuickTerminalController: BaseTerminalController { } // Setup our content - window.contentView = NSHostingView(rootView: TerminalView( + window.contentView = TerminalViewContainer( ghostty: self.ghostty, viewModel: self, delegate: self - )) + ) // Clear out our frame at this point, the fixup from above is complete. if let qtWindow = window as? QuickTerminalWindow { @@ -342,16 +354,33 @@ class QuickTerminalController: BaseTerminalController { // animate out. if surfaceTree.isEmpty, let ghostty_app = ghostty.app { - var config = Ghostty.SurfaceConfiguration() - config.environmentVariables["GHOSTTY_QUICK_TERMINAL"] = "1" - - let view = Ghostty.SurfaceView(ghostty_app, baseConfig: config) - surfaceTree = SplitTree(view: view) - focusedSurface = view + if let tree = restorationState?.surfaceTree, !tree.isEmpty { + surfaceTree = tree + let view = tree.first(where: { $0.id.uuidString == restorationState?.focusedSurface }) ?? tree.first! + focusedSurface = view + // Add a short delay to check if the correct surface is focused. + // Each SurfaceWrapper defaults its FocusedValue to itself; without this delay, + // the tree often focuses the first surface instead of the intended one. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + if !view.focused { + self.focusedSurface = view + self.makeWindowKey(window) + } + } + } else { + var config = Ghostty.SurfaceConfiguration() + config.environmentVariables["GHOSTTY_QUICK_TERMINAL"] = "1" + + let view = Ghostty.SurfaceView(ghostty_app, baseConfig: config) + surfaceTree = SplitTree(view: view) + focusedSurface = view + } } // Animate the window in animateWindowIn(window: window, from: position) + // Clear the restoration state after first use + restorationState = nil } func animateOut() { @@ -370,6 +399,22 @@ class QuickTerminalController: BaseTerminalController { animateWindowOut(window: window, to: position) } + func saveScreenState(exitFullscreen: Bool) { + // If we are in fullscreen, then we exit fullscreen. We do this immediately so + // we have th correct window.frame for the save state below. + if exitFullscreen, let fullscreenStyle, fullscreenStyle.isFullscreen { + fullscreenStyle.exit() + } + guard let window else { return } + // Save the current window frame before animating out. This preserves + // the user's preferred window size and position for when the quick + // terminal is reactivated with a new surface. Without this, SwiftUI + // would reset the window to its minimum content size. + if window.frame.width > 0 && window.frame.height > 0, let screen = window.screen { + screenStateCache.save(frame: window.frame, for: screen) + } + } + private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) { guard let screen = derivedConfig.quickTerminalScreen.screen else { return } @@ -494,19 +539,7 @@ class QuickTerminalController: BaseTerminalController { } private func animateWindowOut(window: NSWindow, to position: QuickTerminalPosition) { - // If we are in fullscreen, then we exit fullscreen. We do this immediately so - // we have th correct window.frame for the save state below. - if let fullscreenStyle, fullscreenStyle.isFullscreen { - fullscreenStyle.exit() - } - - // Save the current window frame before animating out. This preserves - // the user's preferred window size and position for when the quick - // terminal is reactivated with a new surface. Without this, SwiftUI - // would reset the window to its minimum content size. - if window.frame.width > 0 && window.frame.height > 0, let screen = window.screen { - screenStateCache.save(frame: window.frame, for: screen) - } + saveScreenState(exitFullscreen: true) // If we hid the dock then we unhide it. hiddenDock = nil @@ -563,9 +596,10 @@ class QuickTerminalController: BaseTerminalController { }) } - private func syncAppearance() { + override func syncAppearance() { guard let window else { return } + defer { updateColorSchemeForSurfaceTree() } // Change the collection behavior of the window depending on the configuration. window.collectionBehavior = derivedConfig.quickTerminalSpaceBehavior.collectionBehavior @@ -574,7 +608,8 @@ class QuickTerminalController: BaseTerminalController { guard window.isVisible else { return } // If we have window transparency then set it transparent. Otherwise set it opaque. - if (self.derivedConfig.backgroundOpacity < 1) { + // Also check if the user has overridden transparency to be fully opaque. + if !isBackgroundOpaque && (self.derivedConfig.backgroundOpacity < 1 || derivedConfig.backgroundBlur.isGlassStyle) { window.isOpaque = false // This is weird, but we don't use ".clear" because this creates a look that @@ -582,7 +617,9 @@ class QuickTerminalController: BaseTerminalController { // Terminal.app more easily. window.backgroundColor = .white.withAlphaComponent(0.001) - ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque()) + if !derivedConfig.backgroundBlur.isGlassStyle { + ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque()) + } } else { window.isOpaque = true window.backgroundColor = .windowBackgroundColor @@ -687,6 +724,7 @@ class QuickTerminalController: BaseTerminalController { let quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior let quickTerminalSize: QuickTerminalSize let backgroundOpacity: Double + let backgroundBlur: Ghostty.Config.BackgroundBlur init() { self.quickTerminalScreen = .main @@ -695,6 +733,7 @@ class QuickTerminalController: BaseTerminalController { self.quickTerminalSpaceBehavior = .move self.quickTerminalSize = QuickTerminalSize() self.backgroundOpacity = 1.0 + self.backgroundBlur = .disabled } init(_ config: Ghostty.Config) { @@ -704,6 +743,7 @@ class QuickTerminalController: BaseTerminalController { self.quickTerminalSpaceBehavior = config.quickTerminalSpaceBehavior self.quickTerminalSize = config.quickTerminalSize self.backgroundOpacity = config.backgroundOpacity + self.backgroundBlur = config.backgroundBlur } } diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalRestorableState.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalRestorableState.swift new file mode 100644 index 000000000..1fd8642d8 --- /dev/null +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalRestorableState.swift @@ -0,0 +1,26 @@ +import Cocoa + +struct QuickTerminalRestorableState: TerminalRestorable { + static var version: Int { 1 } + + let focusedSurface: String? + let surfaceTree: SplitTree + let screenStateEntries: QuickTerminalScreenStateCache.Entries + + init(from controller: QuickTerminalController) { + controller.saveScreenState(exitFullscreen: true) + self.focusedSurface = controller.focusedSurface?.id.uuidString + self.surfaceTree = controller.surfaceTree + self.screenStateEntries = controller.screenStateCache.stateByDisplay + } + + init(copy other: QuickTerminalRestorableState) { + self = other + } + + var baseConfig: Ghostty.SurfaceConfiguration? { + var config = Ghostty.SurfaceConfiguration() + config.environmentVariables["GHOSTTY_QUICK_TERMINAL"] = "1" + return config + } +} diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift index 7dc53816c..a1c17abb9 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift @@ -7,6 +7,8 @@ import Cocoa /// to restore to its previous size and position when reopened. It uses stable display UUIDs /// to survive NSScreen garbage collection and automatically prunes stale entries. class QuickTerminalScreenStateCache { + typealias Entries = [UUID: DisplayEntry] + /// The maximum number of saved screen states we retain. This is to avoid some kind of /// pathological memory growth in case we get our screen state serializing wrong. I don't /// know anyone with more than 10 screens, so let's just arbitrarily go with that. @@ -16,9 +18,10 @@ class QuickTerminalScreenStateCache { private static let screenStaleTTL: TimeInterval = 14 * 24 * 60 * 60 /// Keyed by display UUID to survive NSScreen garbage collection. - private var stateByDisplay: [UUID: DisplayEntry] = [:] - - init() { + private(set) var stateByDisplay: Entries = [:] + + init(stateByDisplay: Entries = [:]) { + self.stateByDisplay = stateByDisplay NotificationCenter.default.addObserver( self, selector: #selector(onScreensChanged(_:)), @@ -96,7 +99,7 @@ class QuickTerminalScreenStateCache { } } - private struct DisplayEntry { + struct DisplayEntry: Codable { var frame: NSRect var screenSize: CGSize var scale: CGFloat diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index 23b597591..2fb83e64c 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -121,10 +121,10 @@ extension SplitTree { /// Insert a new view at the given view point by creating a split in the given direction. /// This will always reset the zoomed state of the tree. - func insert(view: ViewType, at: ViewType, direction: NewDirection) throws -> Self { + func inserting(view: ViewType, at: ViewType, direction: NewDirection) throws -> Self { guard let root else { throw SplitError.viewNotFound } return .init( - root: try root.insert(view: view, at: at, direction: direction), + root: try root.inserting(view: view, at: at, direction: direction), zoomed: nil) } /// Find a node containing a view with the specified ID. @@ -137,7 +137,7 @@ extension SplitTree { /// Remove a node from the tree. If the node being removed is part of a split, /// the sibling node takes the place of the parent split. - func remove(_ target: Node) -> Self { + func removing(_ target: Node) -> Self { guard let root else { return self } // If we're removing the root itself, return an empty tree @@ -155,7 +155,7 @@ extension SplitTree { } /// Replace a node in the tree with a new node. - func replace(node: Node, with newNode: Node) throws -> Self { + func replacing(node: Node, with newNode: Node) throws -> Self { guard let root else { throw SplitError.viewNotFound } // Get the path to the node we want to replace @@ -164,7 +164,7 @@ extension SplitTree { } // Replace the node - let newRoot = try root.replaceNode(at: path, with: newNode) + let newRoot = try root.replacingNode(at: path, with: newNode) // Update zoomed if it was the replaced node let newZoomed = (zoomed == node) ? newNode : zoomed @@ -232,7 +232,7 @@ extension SplitTree { /// Equalize all splits in the tree so that each split's ratio is based on the /// relative weight (number of leaves) of its children. - func equalize() -> Self { + func equalized() -> Self { guard let root else { return self } let newRoot = root.equalize() return .init(root: newRoot, zoomed: zoomed) @@ -255,7 +255,7 @@ extension SplitTree { /// - bounds: The bounds used to construct the spatial tree representation /// - Returns: A new SplitTree with the adjusted split ratios /// - Throws: SplitError.viewNotFound if the node is not found in the tree or no suitable parent split exists - func resize(node: Node, by pixels: UInt16, in direction: Spatial.Direction, with bounds: CGRect) throws -> Self { + func resizing(node: Node, by pixels: UInt16, in direction: Spatial.Direction, with bounds: CGRect) throws -> Self { guard let root else { throw SplitError.viewNotFound } // Find the path to the target node @@ -327,7 +327,7 @@ extension SplitTree { ) // Replace the split node with the new one - let newRoot = try root.replaceNode(at: splitPath, with: .split(newSplit)) + let newRoot = try root.replacingNode(at: splitPath, with: .split(newSplit)) return .init(root: newRoot, zoomed: nil) } @@ -508,7 +508,7 @@ extension SplitTree.Node { /// /// - Note: If the existing view (`at`) is not found in the tree, this method does nothing. We should /// maybe throw instead but at the moment we just do nothing. - func insert(view: ViewType, at: ViewType, direction: NewDirection) throws -> Self { + func inserting(view: ViewType, at: ViewType, direction: NewDirection) throws -> Self { // Get the path to our insertion point. If it doesn't exist we do // nothing. guard let path = path(to: .leaf(view: at)) else { @@ -544,11 +544,11 @@ extension SplitTree.Node { )) // Replace the node at the path with the new split - return try replaceNode(at: path, with: newSplit) + return try replacingNode(at: path, with: newSplit) } /// Helper function to replace a node at the given path from the root - func replaceNode(at path: Path, with newNode: Self) throws -> Self { + func replacingNode(at path: Path, with newNode: Self) throws -> Self { // If path is empty, replace the root if path.isEmpty { return newNode @@ -635,7 +635,7 @@ extension SplitTree.Node { /// Resize a split node to the specified ratio. /// For leaf nodes, this returns the node unchanged. /// For split nodes, this creates a new split with the updated ratio. - func resize(to ratio: Double) -> Self { + func resizing(to ratio: Double) -> Self { switch self { case .leaf: // Leaf nodes don't have a ratio to resize diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index 103413c70..2a42dc599 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -1,15 +1,40 @@ import SwiftUI +/// A single operation within the split tree. +/// +/// Rather than binding the split tree (which is immutable), any mutable operations are +/// exposed via this enum to the embedder to handle. +enum TerminalSplitOperation { + case resize(Resize) + case drop(Drop) + + struct Resize { + let node: SplitTree.Node + let ratio: Double + } + + struct Drop { + /// The surface being dragged. + let payload: Ghostty.SurfaceView + + /// The surface it was dragged onto + let destination: Ghostty.SurfaceView + + /// The zone it was dropped to determine how to split the destination. + let zone: TerminalSplitDropZone + } +} + struct TerminalSplitTreeView: View { let tree: SplitTree - let onResize: (SplitTree.Node, Double) -> Void + let action: (TerminalSplitOperation) -> Void var body: some View { if let node = tree.zoomed ?? tree.root { TerminalSplitSubtreeView( node: node, isRoot: node == tree.root, - onResize: onResize) + action: action) // This is necessary because we can't rely on SwiftUI's implicit // structural identity to detect changes to this view. Due to // the tree structure of splits it could result in bad behaviors. @@ -19,21 +44,17 @@ struct TerminalSplitTreeView: View { } } -struct TerminalSplitSubtreeView: View { +fileprivate struct TerminalSplitSubtreeView: View { @EnvironmentObject var ghostty: Ghostty.App let node: SplitTree.Node var isRoot: Bool = false - let onResize: (SplitTree.Node, Double) -> Void + let action: (TerminalSplitOperation) -> Void var body: some View { switch (node) { case .leaf(let leafView): - Ghostty.InspectableSurface( - surfaceView: leafView, - isSplit: !isRoot) - .accessibilityElement(children: .contain) - .accessibilityLabel("Terminal pane") + TerminalSplitLeaf(surfaceView: leafView, isSplit: !isRoot, action: action) case .split(let split): let splitViewDirection: SplitViewDirection = switch (split.direction) { @@ -46,15 +67,15 @@ struct TerminalSplitSubtreeView: View { .init(get: { CGFloat(split.ratio) }, set: { - onResize(node, $0) + action(.resize(.init(node: node, ratio: $0))) }), dividerColor: ghostty.config.splitDividerColor, resizeIncrements: .init(width: 1, height: 1), left: { - TerminalSplitSubtreeView(node: split.left, onResize: onResize) + TerminalSplitSubtreeView(node: split.left, action: action) }, right: { - TerminalSplitSubtreeView(node: split.right, onResize: onResize) + TerminalSplitSubtreeView(node: split.right, action: action) }, onEqualize: { guard let surface = node.leftmostLeaf().surface else { return } @@ -64,3 +85,173 @@ struct TerminalSplitSubtreeView: View { } } } + +fileprivate struct TerminalSplitLeaf: View { + let surfaceView: Ghostty.SurfaceView + let isSplit: Bool + let action: (TerminalSplitOperation) -> Void + + @State private var dropState: DropState = .idle + @State private var isSelfDragging: Bool = false + + var body: some View { + GeometryReader { geometry in + Ghostty.InspectableSurface( + surfaceView: surfaceView, + isSplit: isSplit) + .background { + // If we're dragging ourself, we hide the entire drop zone. This makes + // it so that a released drop animates back to its source properly + // so it is a proper invalid drop zone. + if !isSelfDragging { + Color.clear + .onDrop(of: [.ghosttySurfaceId], delegate: SplitDropDelegate( + dropState: $dropState, + viewSize: geometry.size, + destinationSurface: surfaceView, + action: action + )) + } + } + .overlay { + if !isSelfDragging, case .dropping(let zone) = dropState { + zone.overlay(in: geometry) + .allowsHitTesting(false) + } + } + .onPreferenceChange(Ghostty.DraggingSurfaceKey.self) { value in + isSelfDragging = value == surfaceView.id + if isSelfDragging { + dropState = .idle + } + } + .accessibilityElement(children: .contain) + .accessibilityLabel("Terminal pane") + } + } + + private enum DropState: Equatable { + case idle + case dropping(TerminalSplitDropZone) + } + + private struct SplitDropDelegate: DropDelegate { + @Binding var dropState: DropState + let viewSize: CGSize + let destinationSurface: Ghostty.SurfaceView + let action: (TerminalSplitOperation) -> Void + + func validateDrop(info: DropInfo) -> Bool { + info.hasItemsConforming(to: [.ghosttySurfaceId]) + } + + func dropEntered(info: DropInfo) { + dropState = .dropping(.calculate(at: info.location, in: viewSize)) + } + + func dropUpdated(info: DropInfo) -> DropProposal? { + // For some reason dropUpdated is sent after performDrop is called + // and we don't want to reset our drop zone to show it so we have + // to guard on the state here. + guard case .dropping = dropState else { return DropProposal(operation: .forbidden) } + dropState = .dropping(.calculate(at: info.location, in: viewSize)) + return DropProposal(operation: .move) + } + + func dropExited(info: DropInfo) { + dropState = .idle + } + + func performDrop(info: DropInfo) -> Bool { + let zone = TerminalSplitDropZone.calculate(at: info.location, in: viewSize) + dropState = .idle + + // Load the dropped surface asynchronously using Transferable + let providers = info.itemProviders(for: [.ghosttySurfaceId]) + guard let provider = providers.first else { return false } + + // Capture action before the async closure + _ = provider.loadTransferable(type: Ghostty.SurfaceView.self) { [weak destinationSurface] result in + switch result { + case .success(let sourceSurface): + DispatchQueue.main.async { + // Don't allow dropping on self + guard let destinationSurface else { return } + guard sourceSurface !== destinationSurface else { return } + action(.drop(.init(payload: sourceSurface, destination: destinationSurface, zone: zone))) + } + + case .failure: + break + } + } + + return true + } + } +} + +enum TerminalSplitDropZone: String, Equatable { + case top + case bottom + case left + case right + + /// Determines which drop zone the cursor is in based on proximity to edges. + /// + /// Divides the view into four triangular regions by drawing diagonals from + /// corner to corner. The drop zone is determined by which edge the cursor + /// is closest to, creating natural triangular hit regions for each side. + static func calculate(at point: CGPoint, in size: CGSize) -> TerminalSplitDropZone { + let relX = point.x / size.width + let relY = point.y / size.height + + let distToLeft = relX + let distToRight = 1 - relX + let distToTop = relY + let distToBottom = 1 - relY + + let minDist = min(distToLeft, distToRight, distToTop, distToBottom) + + if minDist == distToLeft { return .left } + if minDist == distToRight { return .right } + if minDist == distToTop { return .top } + return .bottom + } + + @ViewBuilder + func overlay(in geometry: GeometryProxy) -> some View { + let overlayColor = Color.accentColor.opacity(0.3) + + switch self { + case .top: + VStack(spacing: 0) { + Rectangle() + .fill(overlayColor) + .frame(height: geometry.size.height / 2) + Spacer() + } + case .bottom: + VStack(spacing: 0) { + Spacer() + Rectangle() + .fill(overlayColor) + .frame(height: geometry.size.height / 2) + } + case .left: + HStack(spacing: 0) { + Rectangle() + .fill(overlayColor) + .frame(width: geometry.size.width / 2) + Spacer() + } + case .right: + HStack(spacing: 0) { + Spacer() + Rectangle() + .fill(overlayColor) + .frame(width: geometry.size.width / 2) + } + } + } +} diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 9104e61ff..b739e9ed1 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -72,12 +72,27 @@ class BaseTerminalController: NSWindowController, /// The previous frame information from the window private var savedFrame: SavedFrame? = nil + /// Cache previously applied appearance to avoid unnecessary updates + private var appliedColorScheme: ghostty_color_scheme_e? + /// The configuration derived from the Ghostty config so we don't need to rely on references. private var derivedConfig: DerivedConfig + /// Track whether background is forced opaque (true) or using config transparency (false) + var isBackgroundOpaque: Bool = false + /// The cancellables related to our focused surface. private var focusedSurfaceCancellables: Set = [] + /// An override title for the tab/window set by the user via prompt_tab_title. + /// When set, this takes precedence over the computed title from the terminal. + var titleOverride: String? = nil { + didSet { applyTitleToWindow() } + } + + /// The last computed title from the focused surface (without the override). + private var lastComputedTitle: String = "👻" + /// The time that undo/redo operations that contain running ptys are valid for. var undoExpiration: Duration { ghostty.config.undoTimeout @@ -180,6 +195,16 @@ class BaseTerminalController: NSWindowController, selector: #selector(ghosttyDidResizeSplit(_:)), name: Ghostty.Notification.didResizeSplit, object: nil) + center.addObserver( + self, + selector: #selector(ghosttyDidPresentTerminal(_:)), + name: Ghostty.Notification.ghosttyPresentTerminal, + object: nil) + center.addObserver( + self, + selector: #selector(ghosttySurfaceDragEndedNoTarget(_:)), + name: .ghosttySurfaceDragEndedNoTarget, + object: nil) // Listen for local events that we need to know of outside of // single surface handlers. @@ -215,7 +240,7 @@ class BaseTerminalController: NSWindowController, // Do the split let newTree: SplitTree do { - newTree = try surfaceTree.insert( + newTree = try surfaceTree.inserting( view: newView, at: oldView, direction: direction) @@ -322,6 +347,37 @@ class BaseTerminalController: NSWindowController, self.alert = alert } + /// Prompt the user to change the tab/window title. + func promptTabTitle() { + guard let window else { return } + + let alert = NSAlert() + alert.messageText = "Change Tab Title" + alert.informativeText = "Leave blank to restore the default." + alert.alertStyle = .informational + + let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 250, height: 24)) + textField.stringValue = titleOverride ?? window.title + alert.accessoryView = textField + + alert.addButton(withTitle: "OK") + alert.addButton(withTitle: "Cancel") + + alert.window.initialFirstResponder = textField + + alert.beginSheetModal(for: window) { [weak self] response in + guard let self else { return } + guard response == .alertFirstButtonReturn else { return } + + let newTitle = textField.stringValue + if newTitle.isEmpty { + self.titleOverride = nil + } else { + self.titleOverride = newTitle + } + } + } + /// Close a surface from a view. func closeSurface( _ view: Ghostty.SurfaceView, @@ -394,14 +450,14 @@ class BaseTerminalController: NSWindowController, } replaceSurfaceTree( - surfaceTree.remove(node), + surfaceTree.removing(node), moveFocusTo: nextFocus, moveFocusFrom: focusedSurface, undoAction: "Close Terminal" ) } - private func replaceSurfaceTree( + func replaceSurfaceTree( _ newTree: SplitTree, moveFocusTo newView: Ghostty.SurfaceView? = nil, moveFocusFrom oldView: Ghostty.SurfaceView? = nil, @@ -415,33 +471,33 @@ class BaseTerminalController: NSWindowController, Ghostty.moveFocus(to: newView, from: oldView) } } - + // Setup our undo - if let undoManager { - if let undoAction { - undoManager.setActionName(undoAction) + guard let undoManager else { return } + if let undoAction { + undoManager.setActionName(undoAction) + } + + undoManager.registerUndo( + withTarget: self, + expiresAfter: undoExpiration + ) { target in + target.surfaceTree = oldTree + if let oldView { + DispatchQueue.main.async { + Ghostty.moveFocus(to: oldView, from: target.focusedSurface) + } } + undoManager.registerUndo( - withTarget: self, - expiresAfter: undoExpiration + withTarget: target, + expiresAfter: target.undoExpiration ) { target in - target.surfaceTree = oldTree - if let oldView { - DispatchQueue.main.async { - Ghostty.moveFocus(to: oldView, from: target.focusedSurface) - } - } - - undoManager.registerUndo( - withTarget: target, - expiresAfter: target.undoExpiration - ) { target in - target.replaceSurfaceTree( - newTree, - moveFocusTo: newView, - moveFocusFrom: target.focusedSurface, - undoAction: undoAction) - } + target.replaceSurfaceTree( + newTree, + moveFocusTo: newView, + moveFocusFrom: target.focusedSurface, + undoAction: undoAction) } } } @@ -558,7 +614,7 @@ class BaseTerminalController: NSWindowController, guard surfaceTree.contains(target) else { return } // Equalize the splits - surfaceTree = surfaceTree.equalize() + surfaceTree = surfaceTree.equalized() } @objc private func ghosttyDidFocusSplit(_ notification: Notification) { @@ -578,9 +634,14 @@ class BaseTerminalController: NSWindowController, return } - // Remove the zoomed state for this surface tree. if surfaceTree.zoomed != nil { - surfaceTree = .init(root: surfaceTree.root, zoomed: nil) + if derivedConfig.splitPreserveZoom.contains(.navigation) { + surfaceTree = SplitTree( + root: surfaceTree.root, + zoomed: surfaceTree.root?.node(view: nextSurface)) + } else { + surfaceTree = SplitTree(root: surfaceTree.root, zoomed: nil) + } } // Move focus to the next surface @@ -643,12 +704,64 @@ class BaseTerminalController: NSWindowController, // Perform the resize using the new SplitTree resize method do { - surfaceTree = try surfaceTree.resize(node: targetNode, by: amount, in: spatialDirection, with: bounds) + surfaceTree = try surfaceTree.resizing(node: targetNode, by: amount, in: spatialDirection, with: bounds) } catch { Ghostty.logger.warning("failed to resize split: \(error)") } } + @objc private func ghosttyDidPresentTerminal(_ notification: Notification) { + guard let target = notification.object as? Ghostty.SurfaceView else { return } + guard surfaceTree.contains(target) else { return } + + // Bring the window to front and focus the surface. + window?.makeKeyAndOrderFront(nil) + + // We use a small delay to ensure this runs after any UI cleanup + // (e.g., command palette restoring focus to its original surface). + Ghostty.moveFocus(to: target) + Ghostty.moveFocus(to: target, delay: 0.1) + + // Show a brief highlight to help the user locate the presented terminal. + target.highlight() + } + + @objc private func ghosttySurfaceDragEndedNoTarget(_ notification: Notification) { + guard let target = notification.object as? Ghostty.SurfaceView else { return } + guard let targetNode = surfaceTree.root?.node(view: target) else { return } + + // If our tree isn't split, then we never create a new window, because + // it is already a single split. + guard surfaceTree.isSplit else { return } + + // If we are removing our focused surface then we move it. We need to + // keep track of our old one so undo sends focus back to the right place. + let oldFocusedSurface = focusedSurface + if focusedSurface == target { + focusedSurface = findNextFocusTargetAfterClosing(node: targetNode) + } + + // Remove the surface from our tree + let removedTree = surfaceTree.removing(targetNode) + + // Create a new tree with the dragged surface and open a new window + let newTree = SplitTree(view: target) + + // Treat our undo below as a full group. + undoManager?.beginUndoGrouping() + undoManager?.setActionName("Move Split") + defer { + undoManager?.endUndoGrouping() + } + + replaceSurfaceTree(removedTree, moveFocusFrom: oldFocusedSurface) + _ = TerminalController.newWindow( + ghostty, + tree: newTree, + position: notification.userInfo?[Notification.Name.ghosttySurfaceDragEndedNoTargetPointKey] as? NSPoint, + confirmUndo: false) + } + // MARK: Local Events private func localEventHandler(_ event: NSEvent) -> NSEvent? { @@ -715,10 +828,21 @@ class BaseTerminalController: NSWindowController, } private func titleDidChange(to: String) { + lastComputedTitle = to + applyTitleToWindow() + } + + private func applyTitleToWindow() { guard let window else { return } - // Set the main window title - window.title = to + if let titleOverride { + window.title = computeTitle( + title: titleOverride, + bell: focusedSurface?.bell ?? false) + return + } + + window.title = lastComputedTitle } func pwdDidChange(to: URL?) { @@ -742,14 +866,101 @@ class BaseTerminalController: NSWindowController, self.window?.contentResizeIncrements = to } - func splitDidResize(node: SplitTree.Node, to newRatio: Double) { - let resizedNode = node.resize(to: newRatio) + func performSplitAction(_ action: TerminalSplitOperation) { + switch action { + case .resize(let resize): + splitDidResize(node: resize.node, to: resize.ratio) + case .drop(let drop): + splitDidDrop(source: drop.payload, destination: drop.destination, zone: drop.zone) + } + } + + private func splitDidResize(node: SplitTree.Node, to newRatio: Double) { + let resizedNode = node.resizing(to: newRatio) do { - surfaceTree = try surfaceTree.replace(node: node, with: resizedNode) + surfaceTree = try surfaceTree.replacing(node: node, with: resizedNode) } catch { Ghostty.logger.warning("failed to replace node during split resize: \(error)") + } + } + + private func splitDidDrop( + source: Ghostty.SurfaceView, + destination: Ghostty.SurfaceView, + zone: TerminalSplitDropZone + ) { + // Map drop zone to split direction + let direction: SplitTree.NewDirection = switch zone { + case .top: .up + case .bottom: .down + case .left: .left + case .right: .right + } + + // Check if source is in our tree + if let sourceNode = surfaceTree.root?.node(view: source) { + // Source is in our tree - same window move + let treeWithoutSource = surfaceTree.removing(sourceNode) + let newTree: SplitTree + do { + newTree = try treeWithoutSource.inserting(view: source, at: destination, direction: direction) + } catch { + Ghostty.logger.warning("failed to insert surface during drop: \(error)") + return + } + + replaceSurfaceTree( + newTree, + moveFocusTo: source, + moveFocusFrom: focusedSurface, + undoAction: "Move Split") return } + + // Source is not in our tree - search other windows + var sourceController: BaseTerminalController? + var sourceNode: SplitTree.Node? + for window in NSApp.windows { + guard let controller = window.windowController as? BaseTerminalController else { continue } + guard controller !== self else { continue } + if let node = controller.surfaceTree.root?.node(view: source) { + sourceController = controller + sourceNode = node + break + } + } + + guard let sourceController, let sourceNode else { + Ghostty.logger.warning("source surface not found in any window during drop") + return + } + + // Remove from source controller's tree and add it to our tree. + // We do this first because if there is an error then we can + // abort. + let newTree: SplitTree + do { + newTree = try surfaceTree.inserting(view: source, at: destination, direction: direction) + } catch { + Ghostty.logger.warning("failed to insert surface during cross-window drop: \(error)") + return + } + + // Treat our undo below as a full group. + undoManager?.beginUndoGrouping() + undoManager?.setActionName("Move Split") + defer { + undoManager?.endUndoGrouping() + } + + // Remove the node from the source. + sourceController.removeSurfaceNode(sourceNode) + + // Add in the surface to our tree + replaceSurfaceTree( + newTree, + moveFocusTo: source, + moveFocusFrom: focusedSurface) } func performAction(_ action: String, on surfaceView: Ghostty.SurfaceView) { @@ -761,6 +972,35 @@ class BaseTerminalController: NSWindowController, } } + // MARK: Appearance + + /// Toggle the background opacity between transparent and opaque states. + /// Do nothing if the configured background-opacity is >= 1 (already opaque). + /// Subclasses should override this to add platform-specific checks and sync appearance. + func toggleBackgroundOpacity() { + // Do nothing if config is already fully opaque + guard ghostty.config.backgroundOpacity < 1 else { return } + + // Do nothing if in fullscreen (transparency doesn't apply in fullscreen) + guard let window, !window.styleMask.contains(.fullScreen) else { return } + + // Toggle between transparent and opaque + isBackgroundOpaque.toggle() + + // Update our appearance + syncAppearance() + } + + /// Override this to resync any appearance related properties. This will be called automatically + /// when certain window properties change that affect appearance. The list below should be updated + /// as we add new things: + /// + /// - ``toggleBackgroundOpacity`` + func syncAppearance() { + // Purposely a no-op. This lets subclasses override this and we can call + // it virtually from here. + } + // MARK: Fullscreen /// Toggle fullscreen for the given mode. @@ -821,6 +1061,9 @@ class BaseTerminalController: NSWindowController, } else { updateOverlayIsVisible = defaultUpdateOverlayVisibility() } + + // Always resync our appearance + syncAppearance() } // MARK: Clipboard Confirmation @@ -969,6 +1212,15 @@ class BaseTerminalController: NSWindowController, } func windowDidBecomeKey(_ notification: Notification) { + // If when we become key our first responder is the window itself, then we + // want to move focus to our focused terminal surface. This works around + // various weirdness with moving surfaces around. + if let window, window.firstResponder == window, let focusedSurface { + DispatchQueue.main.async { + Ghostty.moveFocus(to: focusedSurface) + } + } + // Becoming/losing key means we have to notify our surface(s) that we have focus // so things like cursors blink, pty events are sent, etc. self.syncFocusToSurfaceTree() @@ -1014,6 +1266,10 @@ class BaseTerminalController: NSWindowController, window.performClose(sender) } + @IBAction func changeTabTitle(_ sender: Any) { + promptTabTitle() + } + @IBAction func splitRight(_ sender: Any) { guard let surface = focusedSurface?.surface else { return } ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_RIGHT) @@ -1116,7 +1372,15 @@ class BaseTerminalController: NSWindowController, @IBAction func find(_ sender: Any) { focusedSurface?.find(sender) } - + + @IBAction func selectionForFind(_ sender: Any) { + focusedSurface?.selectionForFind(sender) + } + + @IBAction func scrollToSelection(_ sender: Any) { + focusedSurface?.scrollToSelection(sender) + } + @IBAction func findNext(_ sender: Any) { focusedSurface?.findNext(sender) } @@ -1138,17 +1402,20 @@ class BaseTerminalController: NSWindowController, let macosTitlebarProxyIcon: Ghostty.MacOSTitlebarProxyIcon let windowStepResize: Bool let focusFollowsMouse: Bool + let splitPreserveZoom: Ghostty.Config.SplitPreserveZoom init() { self.macosTitlebarProxyIcon = .visible self.windowStepResize = false self.focusFollowsMouse = false + self.splitPreserveZoom = .init() } init(_ config: Ghostty.Config) { self.macosTitlebarProxyIcon = config.macosTitlebarProxyIcon self.windowStepResize = config.windowStepResize self.focusFollowsMouse = config.focusFollowsMouse + self.splitPreserveZoom = config.splitPreserveZoom } } } @@ -1163,4 +1430,35 @@ extension BaseTerminalController: NSMenuItemValidation { return true } } + + // MARK: - Surface Color Scheme + + /// Update the surface tree's color scheme only when it actually changes. + /// + /// Calling ``ghostty_surface_set_color_scheme`` triggers + /// ``syncAppearance(_:)`` via notification, + /// so we avoid redundant calls. + func updateColorSchemeForSurfaceTree() { + /// Derive the target scheme from `window-theme` or system appearance. + /// We set the scheme on surfaces so they pick the correct theme + /// and let ``syncAppearance(_:)`` update the window accordingly. + /// + /// Using App's effectiveAppearance here to prevent incorrect updates. + let themeAppearance = NSApplication.shared.effectiveAppearance + let scheme: ghostty_color_scheme_e + if themeAppearance.isDark { + scheme = GHOSTTY_COLOR_SCHEME_DARK + } else { + scheme = GHOSTTY_COLOR_SCHEME_LIGHT + } + guard scheme != appliedColorScheme else { + return + } + for surfaceView in surfaceTree { + if let surface = surfaceView.surface { + ghostty_surface_set_color_scheme(surface, scheme) + } + } + appliedColorScheme = scheme + } } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 93a05b6b9..c7f9fe086 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -8,16 +8,16 @@ import GhosttyKit class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Controller { override var windowNibName: NSNib.Name? { let defaultValue = "Terminal" - + guard let appDelegate = NSApp.delegate as? AppDelegate else { return defaultValue } let config = appDelegate.ghostty.config - + // If we have no window decorations, there's no reason to do anything but // the default titlebar (because there will be no titlebar). if !config.windowDecorations { return defaultValue } - + let nib = switch config.macosTitlebarStyle { case "native": "Terminal" case "hidden": "TerminalHiddenTitlebar" @@ -34,32 +34,33 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr #endif default: defaultValue } - + return nib } - + /// This is set to true when we care about frame changes. This is a small optimization since /// this controller registers a listener for ALL frame change notifications and this lets us bail /// early if we don't care. private var tabListenForFrame: Bool = false - + /// This is the hash value of the last tabGroup.windows array. We use this to detect order /// changes in the list. private var tabWindowsHash: Int = 0 - + /// This is set to false by init if the window managed by this controller should not be restorable. /// For example, terminals executing custom scripts are not restorable. private var restorable: Bool = true - + /// The configuration derived from the Ghostty config so we don't need to rely on references. private(set) var derivedConfig: DerivedConfig - + + /// The notification cancellable for focused surface property changes. private var surfaceAppearanceCancellables: Set = [] - + /// This will be set to the initial frame of the window from the xib on load. private var initialFrame: NSRect? = nil - + init(_ ghostty: Ghostty.App, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, withSurfaceTree tree: SplitTree? = nil, @@ -71,12 +72,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // as the script. We may want to revisit this behavior when we have scrollback // restoration. self.restorable = (base?.command ?? "") == "" - + // Setup our initial derived config based on the current app config self.derivedConfig = DerivedConfig(ghostty.config) - + super.init(ghostty, baseConfig: base, surfaceTree: tree) - + // Setup our notifications for behaviors let center = NotificationCenter.default center.addObserver( @@ -104,6 +105,11 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr selector: #selector(onCloseOtherTabs), name: .ghosttyCloseOtherTabs, object: nil) + center.addObserver( + self, + selector: #selector(onCloseTabsOnTheRight), + name: .ghosttyCloseTabsOnTheRight, + object: nil) center.addObserver( self, selector: #selector(onResetWindowSize), @@ -128,46 +134,55 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr object: nil ) } - + required init?(coder: NSCoder) { fatalError("init(coder:) is not supported for this view") } - + deinit { // Remove all of our notificationcenter subscriptions let center = NotificationCenter.default center.removeObserver(self) } - + // MARK: Base Controller Overrides - + override func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { super.surfaceTreeDidChange(from: from, to: to) // Whenever our surface tree changes in any way (new split, close split, etc.) // we want to invalidate our state. invalidateRestorableState() - + // Update our zoom state if let window = window as? TerminalWindow { window.surfaceIsZoomed = to.zoomed != nil } - + // If our surface tree is now nil then we close our window. if (to.isEmpty) { self.window?.close() } } - - - override func fullscreenDidChange() { - super.fullscreenDidChange() - - // When our fullscreen state changes, we resync our appearance because some - // properties change when fullscreen or not. - guard let focusedSurface else { return } - - syncAppearance(focusedSurface.derivedConfig) + + override func replaceSurfaceTree( + _ newTree: SplitTree, + moveFocusTo newView: Ghostty.SurfaceView? = nil, + moveFocusFrom oldView: Ghostty.SurfaceView? = nil, + undoAction: String? = nil + ) { + // We have a special case if our tree is empty to close our tab immediately. + // This makes it so that undo is handled properly. + if newTree.isEmpty { + closeTabImmediately() + return + } + + super.replaceSurfaceTree( + newTree, + moveFocusTo: newView, + moveFocusFrom: oldView, + undoAction: undoAction) } // MARK: Terminal Creation @@ -190,7 +205,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr $0.window?.isMainWindow ?? false } ?? lastMain ?? all.last } - + // The last controller to be main. We use this when paired with "preferredParent" // to find the preferred window to attach new tabs, perform actions, etc. We // always prefer the main window but if there isn't any (because we're triggered @@ -280,6 +295,72 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr return c } + /// Create a new window with an existing split tree. + /// The window will be sized to match the tree's current view bounds if available. + /// - Parameters: + /// - ghostty: The Ghostty app instance. + /// - tree: The split tree to use for the new window. + /// - position: Optional screen position (top-left corner) for the new window. + /// If nil, the window will cascade from the last cascade point. + static func newWindow( + _ ghostty: Ghostty.App, + tree: SplitTree, + position: NSPoint? = nil, + confirmUndo: Bool = true, + ) -> TerminalController { + let c = TerminalController.init(ghostty, withSurfaceTree: tree) + + // Calculate the target frame based on the tree's view bounds + let treeSize: CGSize? = tree.root?.viewBounds() + + DispatchQueue.main.async { + if let window = c.window { + // If we have a tree size, resize the window's content to match + if let treeSize, treeSize.width > 0, treeSize.height > 0 { + window.setContentSize(treeSize) + window.constrainToScreen() + } + + if !window.styleMask.contains(.fullScreen) { + if let position { + window.setFrameTopLeftPoint(position) + window.constrainToScreen() + } else { + Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint) + } + } + } + + c.showWindow(self) + } + + // Setup our undo + if let undoManager = c.undoManager { + undoManager.setActionName("New Window") + undoManager.registerUndo( + withTarget: c, + expiresAfter: c.undoExpiration + ) { target in + undoManager.disableUndoRegistration { + if confirmUndo { + target.closeWindow(nil) + } else { + target.closeWindowImmediately() + } + } + + undoManager.registerUndo( + withTarget: ghostty, + expiresAfter: target.undoExpiration + ) { ghostty in + _ = TerminalController.newWindow(ghostty, tree: tree) + } + } + } + + return c + } + static func newTab( _ ghostty: Ghostty.App, from parent: NSWindow? = nil, @@ -402,7 +483,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr return controller } - + //MARK: - Methods @objc private func ghosttyConfigDidChange(_ notification: Notification) { @@ -425,15 +506,9 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr return } - - // This is a surface-level config update. If we have the surface, we - // update our appearance based on it. - guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return } - guard surfaceTree.contains(surfaceView) else { return } - - // We can't use surfaceView.derivedConfig because it may not be updated - // yet since it also responds to notifications. - syncAppearance(.init(config)) + /// Surface-level config will be updated in + /// ``Ghostty/Ghostty/SurfaceView/derivedConfig`` then + /// ``TerminalController/focusedSurfaceDidChange(to:)`` } /// Update the accessory view of each tab according to the keyboard @@ -489,6 +564,13 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr tabWindowsHash = v self.relabelTabs() } + + override func syncAppearance() { + // When our focus changes, we update our window appearance based on the + // currently focused surface. + guard let focusedSurface else { return } + syncAppearance(focusedSurface.derivedConfig) + } private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { // Let our window handle its own appearance @@ -518,13 +600,13 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr fromTopLeftOffsetX: CGFloat(x), offsetY: CGFloat(y), windowSize: frame.size) - + // Clamp the origin to ensure the window stays fully visible on screen var safeOrigin = origin let vf = screen.visibleFrame safeOrigin.x = min(max(safeOrigin.x, vf.minX), vf.maxX - frame.width) safeOrigin.y = min(max(safeOrigin.y, vf.minY), vf.maxY - frame.height) - + // Return our new origin var result = frame result.origin = safeOrigin @@ -552,14 +634,14 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr closeWindow(nil) } - private func closeTabImmediately(registerRedo: Bool = true) { + func closeTabImmediately(registerRedo: Bool = true) { guard let window = window else { return } guard let tabGroup = window.tabGroup, tabGroup.windows.count > 1 else { closeWindowImmediately() return } - + // Undo if let undoManager, let undoState { // Register undo action to restore the tab @@ -580,15 +662,15 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } } } - + window.close() } - + private func closeOtherTabsImmediately() { guard let window = window else { return } guard let tabGroup = window.tabGroup else { return } guard tabGroup.windows.count > 1 else { return } - + // Start an undo grouping if let undoManager { undoManager.beginUndoGrouping() @@ -596,7 +678,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr defer { undoManager?.endUndoGrouping() } - + // Iterate through all tabs except the current one. for window in tabGroup.windows where window != self.window { // We ignore any non-terminal tabs. They don't currently exist and we can't @@ -608,10 +690,10 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr controller.closeTabImmediately(registerRedo: false) } } - + if let undoManager { undoManager.setActionName("Close Other Tabs") - + // We need to register an undo that refocuses this window. Otherwise, the // undo operation above for each tab will steal focus. undoManager.registerUndo( @@ -621,7 +703,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr DispatchQueue.main.async { target.window?.makeKeyAndOrderFront(nil) } - + // Register redo action undoManager.registerUndo( withTarget: target, @@ -633,9 +715,49 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } } + private func closeTabsOnTheRightImmediately() { + guard let window = window else { return } + guard let tabGroup = window.tabGroup else { return } + guard let currentIndex = tabGroup.windows.firstIndex(of: window) else { return } + + let tabsToClose = tabGroup.windows.enumerated().filter { $0.offset > currentIndex } + guard !tabsToClose.isEmpty else { return } + + undoManager?.beginUndoGrouping() + defer { + undoManager?.endUndoGrouping() + } + + for (_, candidate) in tabsToClose { + if let controller = candidate.windowController as? TerminalController { + controller.closeTabImmediately(registerRedo: false) + } + } + + if let undoManager { + undoManager.setActionName("Close Tabs to the Right") + + undoManager.registerUndo( + withTarget: self, + expiresAfter: undoExpiration + ) { target in + DispatchQueue.main.async { + target.window?.makeKeyAndOrderFront(nil) + } + + undoManager.registerUndo( + withTarget: target, + expiresAfter: target.undoExpiration + ) { target in + target.closeTabsOnTheRightImmediately() + } + } + } + } + /// Closes the current window (including any other tabs) immediately and without /// confirmation. This will setup proper undo state so the action can be undone. - private func closeWindowImmediately() { + func closeWindowImmediately() { guard let window = window else { return } registerUndoForCloseWindow() @@ -707,7 +829,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr case (nil, nil): return true } } - + // Find the index of the key window in our sorted states. This is a bit verbose // but we only need this for this style of undo so we don't want to add it to // UndoState. @@ -733,12 +855,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr let controllers = undoStates.map { undoState in TerminalController(ghostty, with: undoState) } - + // The first controller becomes the parent window for all tabs. // If we don't have a first controller (shouldn't be possible?) // then we can't restore tabs. guard let firstController = controllers.first else { return } - + // Add all subsequent controllers as tabs to the first window for controller in controllers.dropFirst() { controller.showWindow(nil) @@ -747,7 +869,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr firstWindow.addTabbedWindow(newWindow, ordered: .above) } } - + // Make the appropriate window key. If we had a key window, restore it. // Otherwise, make the last window key. if let keyWindowIndex, keyWindowIndex < controllers.count { @@ -813,6 +935,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr let focusedSurface: UUID? let tabIndex: Int? weak var tabGroup: NSWindowTabGroup? + let tabColor: TerminalTabColor } convenience init(_ ghostty: Ghostty.App, @@ -824,6 +947,9 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr showWindow(nil) if let window { window.setFrame(undoState.frame, display: true) + if let terminalWindow = window as? TerminalWindow { + terminalWindow.tabColor = undoState.tabColor + } // If we have a tab group and index, restore the tab to its original position if let tabGroup = undoState.tabGroup, @@ -839,13 +965,20 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // Make it the key window window.makeKeyAndOrderFront(nil) } - + // Restore focus to the previously focused surface if let focusedUUID = undoState.focusedSurface, let focusTarget = surfaceTree.first(where: { $0.id == focusedUUID }) { DispatchQueue.main.async { Ghostty.moveFocus(to: focusTarget, from: nil) } + } else if let focusedSurface = surfaceTree.first { + // No prior focused surface or we can't find it, let's focus + // the first. + self.focusedSurface = focusedSurface + DispatchQueue.main.async { + Ghostty.moveFocus(to: focusedSurface, from: nil) + } } } } @@ -859,7 +992,8 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr surfaceTree: surfaceTree, focusedSurface: focusedSurface?.id, tabIndex: window.tabGroup?.windows.firstIndex(of: window), - tabGroup: window.tabGroup) + tabGroup: window.tabGroup, + tabColor: (window as? TerminalWindow)?.tabColor ?? .none) } //MARK: - NSWindowController @@ -895,35 +1029,39 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } // Initialize our content view to the SwiftUI root - window.contentView = NSHostingView(rootView: TerminalView( + window.contentView = TerminalViewContainer( ghostty: self.ghostty, viewModel: self, delegate: self, - )) - + ) + // If we have a default size, we want to apply it. if let defaultSize { switch (defaultSize) { case .frame: // Frames can be applied immediately defaultSize.apply(to: window) - + case .contentIntrinsicSize: // Content intrinsic size requires a short delay so that AppKit // can layout our SwiftUI views. - DispatchQueue.main.asyncAfter(deadline: .now() + .microseconds(10_000)) { [weak window] in - guard let window else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + .microseconds(10_000)) { [weak self, weak window] in + guard let self, let window else { return } defaultSize.apply(to: window) + if let screen = window.screen ?? NSScreen.main { + let frame = self.adjustForWindowPosition(frame: window.frame, on: screen) + window.setFrameOrigin(frame.origin) + } } } } - + // Store our initial frame so we can know our default later. This MUST // be after the defaultSize call above so that we don't re-apply our frame. // Note: we probably want to set this on the first frame change or something // so it respects cascade. initialFrame = window.frame - + // In various situations, macOS automatically tabs new windows. Ghostty handles // its own tabbing so we DONT want this behavior. This detects this scenario and undoes // it. @@ -1034,7 +1172,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr if let window { LastWindowPosition.shared.save(window) } - + // Remember our last main Self.lastMain = self } @@ -1081,27 +1219,27 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr @IBAction func closeOtherTabs(_ sender: Any?) { guard let window = window else { return } guard let tabGroup = window.tabGroup else { return } - + // If we only have one window then we have no other tabs to close guard tabGroup.windows.count > 1 else { return } - + // Check if we have to confirm close. guard tabGroup.windows.contains(where: { window in // Ignore ourself if window == self.window { return false } - + // Ignore non-terminals guard let controller = window.windowController as? TerminalController else { return false } - + // Check if any surfaces require confirmation return controller.surfaceTree.contains(where: { $0.needsConfirmQuit }) }) else { self.closeOtherTabsImmediately() return } - + confirmClose( messageText: "Close Other Tabs?", informativeText: "At least one other tab still has a running process. If you close the tab the process will be killed." @@ -1110,6 +1248,35 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } } + @IBAction func closeTabsOnTheRight(_ sender: Any?) { + guard let window = window else { return } + guard let tabGroup = window.tabGroup else { return } + guard let currentIndex = tabGroup.windows.firstIndex(of: window) else { return } + + let tabsToClose = tabGroup.windows.enumerated().filter { $0.offset > currentIndex } + guard !tabsToClose.isEmpty else { return } + + let needsConfirm = tabsToClose.contains { (_, candidate) in + guard let controller = candidate.windowController as? TerminalController else { + return false + } + + return controller.surfaceTree.contains(where: { $0.needsConfirmQuit }) + } + + if !needsConfirm { + self.closeTabsOnTheRightImmediately() + return + } + + confirmClose( + messageText: "Close Tabs on the Right?", + informativeText: "At least one tab to the right still has a running process. If you close the tab the process will be killed." + ) { + self.closeTabsOnTheRightImmediately() + } + } + @IBAction func returnToDefaultSize(_ sender: Any?) { guard let window, let defaultSize else { return } defaultSize.apply(to: window) @@ -1151,7 +1318,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } //MARK: - TerminalViewDelegate - + override func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) { super.focusedSurfaceDidChange(to: to) @@ -1215,7 +1382,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // Get our target window let targetWindow = tabbedWindows[finalIndex] - + // Moving tabs on macOS 26 RC causes very nasty visual glitches in the titlebar tabs. // I believe this is due to messed up constraints for our hacky tab bar. I'd like to // find a better workaround. For now, this improves things dramatically. @@ -1228,7 +1395,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr DispatchQueue.main.async { selectedWindow.makeKey() } - + return } } @@ -1311,6 +1478,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr closeOtherTabs(self) } + @objc private func onCloseTabsOnTheRight(notification: SwiftUI.Notification) { + guard let target = notification.object as? Ghostty.SurfaceView else { return } + guard surfaceTree.contains(target) else { return } + closeTabsOnTheRight(self) + } + @objc private func onCloseWindow(notification: SwiftUI.Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } guard surfaceTree.contains(target) else { return } @@ -1373,23 +1546,28 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr extension TerminalController { override func validateMenuItem(_ item: NSMenuItem) -> Bool { switch item.action { + case #selector(closeTabsOnTheRight): + guard let window, let tabGroup = window.tabGroup else { return false } + guard let currentIndex = tabGroup.windows.firstIndex(of: window) else { return false } + return tabGroup.windows.enumerated().contains { $0.offset > currentIndex } + case #selector(returnToDefaultSize): guard let window else { return false } - + // Native fullscreen windows can't revert to default size. if window.styleMask.contains(.fullScreen) { return false } - + // If we're fullscreen at all then we can't change size if fullscreenStyle?.isFullscreen ?? false { return false } - + // If our window is already the default size or we don't have a // default size, then disable. return defaultSize?.isChanged(for: window) ?? false - + default: return super.validateMenuItem(item) } @@ -1405,10 +1583,10 @@ extension TerminalController { enum DefaultSize { /// A frame, set with `window.setFrame` case frame(NSRect) - + /// A content size, set with `window.setContentSize` case contentIntrinsicSize - + func isChanged(for window: NSWindow) -> Bool { switch self { case .frame(let rect): @@ -1417,11 +1595,11 @@ extension TerminalController { guard let view = window.contentView else { return false } - + return view.frame.size != view.intrinsicContentSize } } - + func apply(to window: NSWindow) { switch self { case .frame(let rect): @@ -1430,13 +1608,13 @@ extension TerminalController { guard let size = window.contentView?.intrinsicContentSize else { return } - + window.setContentSize(size) window.constrainToScreen() } } } - + private var defaultSize: DefaultSize? { if derivedConfig.maximize, let screen = window?.screen ?? NSScreen.main { // Maximize takes priority, we take up the full screen we're on. diff --git a/macos/Sources/Features/Terminal/TerminalRestorable.swift b/macos/Sources/Features/Terminal/TerminalRestorable.swift index 71e54b612..fd0f4eab5 100644 --- a/macos/Sources/Features/Terminal/TerminalRestorable.swift +++ b/macos/Sources/Features/Terminal/TerminalRestorable.swift @@ -1,20 +1,22 @@ import Cocoa -/// The state stored for terminal window restoration. -class TerminalRestorableState: Codable { - static let selfKey = "state" - static let versionKey = "version" - static let version: Int = 5 +protocol TerminalRestorable: Codable { + static var selfKey: String { get } + static var versionKey: String { get } + static var version: Int { get } + init(copy other: Self) - let focusedSurface: String? - let surfaceTree: SplitTree - let effectiveFullscreenMode: FullscreenMode? + /// Returns a base configuration to use when restoring terminal surfaces. + /// Override this to provide custom environment variables or other configuration. + var baseConfig: Ghostty.SurfaceConfiguration? { get } +} - init(from controller: TerminalController) { - self.focusedSurface = controller.focusedSurface?.id.uuidString - self.surfaceTree = controller.surfaceTree - self.effectiveFullscreenMode = controller.fullscreenStyle?.fullscreenMode - } +extension TerminalRestorable { + static var selfKey: String { "state" } + static var versionKey: String { "version" } + + /// Default implementation returns nil (no custom base config). + var baseConfig: Ghostty.SurfaceConfiguration? { nil } init?(coder aDecoder: NSCoder) { // If the version doesn't match then we can't decode. In the future we can perform @@ -28,9 +30,7 @@ class TerminalRestorableState: Codable { return nil } - self.surfaceTree = v.value.surfaceTree - self.focusedSurface = v.value.focusedSurface - self.effectiveFullscreenMode = v.value.effectiveFullscreenMode + self.init(copy: v.value) } func encode(with coder: NSCoder) { @@ -39,6 +39,33 @@ class TerminalRestorableState: Codable { } } +/// The state stored for terminal window restoration. +class TerminalRestorableState: TerminalRestorable { + class var version: Int { 7 } + + let focusedSurface: String? + let surfaceTree: SplitTree + let effectiveFullscreenMode: FullscreenMode? + let tabColor: TerminalTabColor + let titleOverride: String? + + init(from controller: TerminalController) { + self.focusedSurface = controller.focusedSurface?.id.uuidString + self.surfaceTree = controller.surfaceTree + self.effectiveFullscreenMode = controller.fullscreenStyle?.fullscreenMode + self.tabColor = (controller.window as? TerminalWindow)?.tabColor ?? .none + self.titleOverride = controller.titleOverride + } + + required init(copy other: TerminalRestorableState) { + self.surfaceTree = other.surfaceTree + self.focusedSurface = other.focusedSurface + self.effectiveFullscreenMode = other.effectiveFullscreenMode + self.tabColor = other.tabColor + self.titleOverride = other.titleOverride + } +} + enum TerminalRestoreError: Error { case delegateInvalid case identifierUnknown @@ -94,6 +121,12 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { return } + // Restore our tab color + (window as? TerminalWindow)?.tabColor = state.tabColor + + // Restore the tab title override + c.titleOverride = state.titleOverride + // Setup our restored state on the controller // Find the focused surface in surfaceTree if let focusedStr = state.focusedSurface { @@ -158,3 +191,5 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { } } } + + diff --git a/macos/Sources/Features/Terminal/TerminalTabColor.swift b/macos/Sources/Features/Terminal/TerminalTabColor.swift new file mode 100644 index 000000000..08d89324c --- /dev/null +++ b/macos/Sources/Features/Terminal/TerminalTabColor.swift @@ -0,0 +1,185 @@ +import AppKit +import SwiftUI + +enum TerminalTabColor: Int, CaseIterable, Codable { + case none + case blue + case purple + case pink + case red + case orange + case yellow + case green + case teal + case graphite + + var localizedName: String { + switch self { + case .none: + return "None" + case .blue: + return "Blue" + case .purple: + return "Purple" + case .pink: + return "Pink" + case .red: + return "Red" + case .orange: + return "Orange" + case .yellow: + return "Yellow" + case .green: + return "Green" + case .teal: + return "Teal" + case .graphite: + return "Graphite" + } + } + + var displayColor: NSColor? { + switch self { + case .none: + return nil + case .blue: + return .systemBlue + case .purple: + return .systemPurple + case .pink: + return .systemPink + case .red: + return .systemRed + case .orange: + return .systemOrange + case .yellow: + return .systemYellow + case .green: + return .systemGreen + case .teal: + if #available(macOS 13.0, *) { + return .systemMint + } else { + return .systemTeal + } + case .graphite: + return .systemGray + } + } + + func swatchImage(selected: Bool) -> NSImage { + let size = NSSize(width: 18, height: 18) + return NSImage(size: size, flipped: false) { rect in + let circleRect = rect.insetBy(dx: 1, dy: 1) + let circlePath = NSBezierPath(ovalIn: circleRect) + + if let fillColor = self.displayColor { + fillColor.setFill() + circlePath.fill() + } else { + NSColor.clear.setFill() + circlePath.fill() + NSColor.quaternaryLabelColor.setStroke() + circlePath.lineWidth = 1 + circlePath.stroke() + } + + if self == .none { + let slash = NSBezierPath() + slash.move(to: NSPoint(x: circleRect.minX + 2, y: circleRect.minY + 2)) + slash.line(to: NSPoint(x: circleRect.maxX - 2, y: circleRect.maxY - 2)) + slash.lineWidth = 1.5 + NSColor.secondaryLabelColor.setStroke() + slash.stroke() + } + + if selected { + let highlight = NSBezierPath(ovalIn: rect.insetBy(dx: 0.5, dy: 0.5)) + highlight.lineWidth = 2 + NSColor.controlAccentColor.setStroke() + highlight.stroke() + } + + return true + } + } +} + +// MARK: - Menu View + +/// A SwiftUI view displaying a color palette for tab color selection. +/// Used as a custom view inside an NSMenuItem in the tab context menu. +struct TabColorMenuView: View { + @State private var currentSelection: TerminalTabColor + let onSelect: (TerminalTabColor) -> Void + + init(selectedColor: TerminalTabColor, onSelect: @escaping (TerminalTabColor) -> Void) { + self._currentSelection = State(initialValue: selectedColor) + self.onSelect = onSelect + } + + var body: some View { + VStack(alignment: .leading, spacing: 3) { + Text("Tab Color") + .padding(.bottom, 2) + + ForEach(Self.paletteRows, id: \.self) { row in + HStack(spacing: 2) { + ForEach(row, id: \.self) { color in + TabColorSwatch( + color: color, + isSelected: color == currentSelection + ) { + currentSelection = color + onSelect(color) + } + } + } + } + } + .padding(.leading, Self.leadingPadding) + .padding(.trailing, 12) + .padding(.top, 4) + .padding(.bottom, 4) + } + + static let paletteRows: [[TerminalTabColor]] = [ + [.none, .blue, .purple, .pink, .red], + [.orange, .yellow, .green, .teal, .graphite], + ] + + /// Leading padding to align with the menu's icon gutter. + /// macOS 26 introduced icons in menus, requiring additional padding. + private static var leadingPadding: CGFloat { + if #available(macOS 26.0, *) { + return 40 + } else { + return 12 + } + } +} + +/// A single color swatch button in the tab color palette. +private struct TabColorSwatch: View { + let color: TerminalTabColor + let isSelected: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + Group { + if color == .none { + Image(systemName: isSelected ? "circle.slash" : "circle") + .foregroundStyle(.secondary) + } else if let displayColor = color.displayColor { + Image(systemName: isSelected ? "checkmark.circle.fill" : "circle.fill") + .foregroundStyle(Color(nsColor: displayColor)) + } + } + .font(.system(size: 16)) + .frame(width: 20, height: 20) + } + .buttonStyle(.plain) + .help(color.localizedName) + } +} diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index fd53a617b..e117e0647 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -1,5 +1,6 @@ import SwiftUI import GhosttyKit +import os /// This delegate is notified of actions and property changes regarding the terminal view. This /// delegate is optional and can be used by a TerminalView caller to react to changes such as @@ -16,9 +17,9 @@ protocol TerminalViewDelegate: AnyObject { /// Perform an action. At the time of writing this is only triggered by the command palette. func performAction(_ action: String, on: Ghostty.SurfaceView) - - /// A split is resizing to a given value. - func splitDidResize(node: SplitTree.Node, to newRatio: Double) + + /// A split tree operation + func performSplitAction(_ action: TerminalSplitOperation) } /// The view model is a required implementation for TerminalView callers. This contains @@ -81,7 +82,7 @@ struct TerminalView: View { TerminalSplitTreeView( tree: viewModel.surfaceTree, - onResize: { delegate?.splitDidResize(node: $0, to: $1) }) + action: { delegate?.performSplitAction($0) }) .environmentObject(ghostty) .focused($focused) .onAppear { self.focused = true } diff --git a/macos/Sources/Features/Terminal/TerminalViewContainer.swift b/macos/Sources/Features/Terminal/TerminalViewContainer.swift new file mode 100644 index 000000000..c65dca1d2 --- /dev/null +++ b/macos/Sources/Features/Terminal/TerminalViewContainer.swift @@ -0,0 +1,160 @@ +import AppKit +import SwiftUI + +/// Use this container to achieve a glass effect at the window level. +/// Modifying `NSThemeFrame` can sometimes be unpredictable. +class TerminalViewContainer: NSView { + private let terminalView: NSView + + /// Glass effect view for liquid glass background when transparency is enabled + private var glassEffectView: NSView? + private var glassTopConstraint: NSLayoutConstraint? + private var derivedConfig: DerivedConfig + + init(ghostty: Ghostty.App, viewModel: ViewModel, delegate: (any TerminalViewDelegate)? = nil) { + self.derivedConfig = DerivedConfig(config: ghostty.config) + self.terminalView = NSHostingView(rootView: TerminalView( + ghostty: ghostty, + viewModel: viewModel, + delegate: delegate + )) + super.init(frame: .zero) + setup() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// To make ``TerminalController/DefaultSize/contentIntrinsicSize`` + /// work in ``TerminalController/windowDidLoad()``, + /// we override this to provide the correct size. + override var intrinsicContentSize: NSSize { + terminalView.intrinsicContentSize + } + + private func setup() { + addSubview(terminalView) + terminalView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + terminalView.topAnchor.constraint(equalTo: topAnchor), + terminalView.leadingAnchor.constraint(equalTo: leadingAnchor), + terminalView.bottomAnchor.constraint(equalTo: bottomAnchor), + terminalView.trailingAnchor.constraint(equalTo: trailingAnchor), + ]) + + NotificationCenter.default.addObserver( + self, + selector: #selector(ghosttyConfigDidChange(_:)), + name: .ghosttyConfigDidChange, + object: nil + ) + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + updateGlassEffectIfNeeded() + updateGlassEffectTopInsetIfNeeded() + } + + override func layout() { + super.layout() + updateGlassEffectTopInsetIfNeeded() + } + + @objc private func ghosttyConfigDidChange(_ notification: Notification) { + guard let config = notification.userInfo?[ + Notification.Name.GhosttyConfigChangeKey + ] as? Ghostty.Config else { return } + let newValue = DerivedConfig(config: config) + guard newValue != derivedConfig else { return } + derivedConfig = newValue + DispatchQueue.main.async(execute: updateGlassEffectIfNeeded) + } +} + +// MARK: Glass + +private extension TerminalViewContainer { +#if compiler(>=6.2) + @available(macOS 26.0, *) + func addGlassEffectViewIfNeeded() -> NSGlassEffectView? { + if let existed = glassEffectView as? NSGlassEffectView { + updateGlassEffectTopInsetIfNeeded() + return existed + } + guard let themeFrameView = window?.contentView?.superview else { + return nil + } + let effectView = NSGlassEffectView() + addSubview(effectView, positioned: .below, relativeTo: terminalView) + effectView.translatesAutoresizingMaskIntoConstraints = false + glassTopConstraint = effectView.topAnchor.constraint( + equalTo: topAnchor, + constant: -themeFrameView.safeAreaInsets.top + ) + if let glassTopConstraint { + NSLayoutConstraint.activate([ + glassTopConstraint, + effectView.leadingAnchor.constraint(equalTo: leadingAnchor), + effectView.bottomAnchor.constraint(equalTo: bottomAnchor), + effectView.trailingAnchor.constraint(equalTo: trailingAnchor), + ]) + } + glassEffectView = effectView + return effectView + } +#endif // compiler(>=6.2) + + func updateGlassEffectIfNeeded() { +#if compiler(>=6.2) + guard #available(macOS 26.0, *), derivedConfig.backgroundBlur.isGlassStyle else { + glassEffectView?.removeFromSuperview() + glassEffectView = nil + glassTopConstraint = nil + return + } + guard let effectView = addGlassEffectViewIfNeeded() else { + return + } + switch derivedConfig.backgroundBlur { + case .macosGlassRegular: + effectView.style = NSGlassEffectView.Style.regular + case .macosGlassClear: + effectView.style = NSGlassEffectView.Style.clear + default: + break + } + let backgroundColor = (window as? TerminalWindow)?.preferredBackgroundColor ?? NSColor(derivedConfig.backgroundColor) + effectView.tintColor = backgroundColor + .withAlphaComponent(derivedConfig.backgroundOpacity) + if let window, window.responds(to: Selector(("_cornerRadius"))), let cornerRadius = window.value(forKey: "_cornerRadius") as? CGFloat { + effectView.cornerRadius = cornerRadius + } +#endif // compiler(>=6.2) + } + + func updateGlassEffectTopInsetIfNeeded() { +#if compiler(>=6.2) + guard #available(macOS 26.0, *), derivedConfig.backgroundBlur.isGlassStyle else { + return + } + guard glassEffectView != nil else { return } + guard let themeFrameView = window?.contentView?.superview else { return } + glassTopConstraint?.constant = -themeFrameView.safeAreaInsets.top +#endif // compiler(>=6.2) + } + + struct DerivedConfig: Equatable { + var backgroundOpacity: Double = 0 + var backgroundBlur: Ghostty.Config.BackgroundBlur + var backgroundColor: Color = .clear + + init(config: Ghostty.Config) { + self.backgroundBlur = config.backgroundBlur + self.backgroundOpacity = config.backgroundOpacity + self.backgroundColor = config.backgroundColor + } + } +} diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index a829ec519..501ac0e67 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -7,10 +7,10 @@ import GhosttyKit class TerminalWindow: NSWindow { /// Posted when a terminal window awakes from nib. static let terminalDidAwake = Notification.Name("TerminalWindowDidAwake") - + /// Posted when a terminal window will close static let terminalWillCloseNotification = Notification.Name("TerminalWindowWillClose") - + /// This is the key in UserDefaults to use for the default `level` value. This is /// used by the manual float on top menu item feature. static let defaultLevelKey: String = "TerminalDefaultLevel" @@ -20,13 +20,23 @@ class TerminalWindow: NSWindow { /// Reset split zoom button in titlebar private let resetZoomAccessory = NSTitlebarAccessoryViewController() - + /// Update notification UI in titlebar private let updateAccessory = NSTitlebarAccessoryViewController() + /// Visual indicator that mirrors the selected tab color. + private lazy var tabColorIndicator: NSHostingView = { + let view = NSHostingView(rootView: TabColorIndicatorView(tabColor: tabColor)) + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + /// The configuration derived from the Ghostty config so we don't need to rely on references. private(set) var derivedConfig: DerivedConfig = .init() + /// Sets up our tab context menu + private var tabMenuObserver: NSObjectProtocol? = nil + /// Whether this window supports the update accessory. If this is false, then views within this /// window should determine how to show update notifications. var supportsUpdateAccessory: Bool { @@ -34,11 +44,24 @@ class TerminalWindow: NSWindow { true } + /// Glass effect view for liquid glass background when transparency is enabled + private var glassEffectView: NSView? + /// Gets the terminal controller from the window controller. var terminalController: TerminalController? { windowController as? TerminalController } - + + /// The color assigned to this window's tab. Setting this updates the tab color indicator + /// and marks the window's restorable state as dirty. + var tabColor: TerminalTabColor = .none { + didSet { + guard tabColor != oldValue else { return } + tabColorIndicator.rootView = TabColorIndicatorView(tabColor: tabColor) + invalidateRestorableState() + } + } + // MARK: NSWindow Overrides override var toolbar: NSToolbar? { @@ -53,7 +76,18 @@ class TerminalWindow: NSWindow { override func awakeFromNib() { // Notify that this terminal window has loaded NotificationCenter.default.post(name: Self.terminalDidAwake, object: self) - + + // This is fragile, but there doesn't seem to be an official API for customizing + // native tab bar menus. + tabMenuObserver = NotificationCenter.default.addObserver( + forName: Notification.Name(rawValue: "NSMenuWillOpenNotification"), + object: nil, + queue: .main + ) { [weak self] n in + guard let self, let menu = n.object as? NSMenu else { return } + self.configureTabContextMenuIfNeeded(menu) + } + // This is required so that window restoration properly creates our tabs // again. I'm not sure why this is required. If you don't do this, then // tabs restore as separate windows. @@ -61,14 +95,14 @@ class TerminalWindow: NSWindow { DispatchQueue.main.async { self.tabbingMode = .automatic } - + // All new windows are based on the app config at the time of creation. guard let appDelegate = NSApp.delegate as? AppDelegate else { return } let config = appDelegate.ghostty.config // Setup our initial config derivedConfig = .init(config) - + // If there is a hardcoded title in the configuration, we set that // immediately. Future `set_title` apprt actions will override this // if necessary but this ensures our window loads with the proper @@ -103,7 +137,7 @@ class TerminalWindow: NSWindow { })) addTitlebarAccessoryViewController(resetZoomAccessory) resetZoomAccessory.view.translatesAutoresizingMaskIntoConstraints = false - + // Create update notification accessory if supportsUpdateAccessory { updateAccessory.layoutAttribute = .right @@ -119,9 +153,16 @@ class TerminalWindow: NSWindow { // Setup the accessory view for tabs that shows our keyboard shortcuts, // zoomed state, etc. Note I tried to use SwiftUI here but ran into issues // where buttons were not clickable. - let stackView = NSStackView(views: [keyEquivalentLabel, resetZoomTabButton]) + tabColorIndicator.rootView = TabColorIndicatorView(tabColor: tabColor) + + let stackView = NSStackView() + stackView.orientation = .horizontal stackView.setHuggingPriority(.defaultHigh, for: .horizontal) - stackView.spacing = 3 + stackView.spacing = 4 + stackView.alignment = .centerY + stackView.addArrangedSubview(tabColorIndicator) + stackView.addArrangedSubview(keyEquivalentLabel) + stackView.addArrangedSubview(resetZoomTabButton) tab.accessoryView = stackView // Get our saved level @@ -132,7 +173,7 @@ class TerminalWindow: NSWindow { // still become key/main and receive events. override var canBecomeKey: Bool { return true } override var canBecomeMain: Bool { return true } - + override func close() { NotificationCenter.default.post(name: Self.terminalWillCloseNotification, object: self) super.close() @@ -153,7 +194,7 @@ class TerminalWindow: NSWindow { // Its possible we miss the accessory titlebar call so we check again // whenever the window becomes main. Both of these are idempotent. - if hasTabBar { + if tabBarView != nil { tabBarDidAppear() } else { tabBarDidDisappear() @@ -202,31 +243,6 @@ class TerminalWindow: NSWindow { /// added. static let tabBarIdentifier: NSUserInterfaceItemIdentifier = .init("_ghosttyTabBar") - func findTitlebarView() -> NSView? { - // Find our tab bar. If it doesn't exist we don't do anything. - // - // In normal window, `NSTabBar` typically appears as a subview of `NSTitlebarView` within `NSThemeFrame`. - // In fullscreen, the system creates a dedicated fullscreen window and the view hierarchy changes; - // in that case, the `titlebarView` is only accessible via a reference on `NSThemeFrame`. - // ref: https://github.com/mozilla-firefox/firefox/blob/054e2b072785984455b3b59acad9444ba1eeffb4/widget/cocoa/nsCocoaWindow.mm#L7205 - guard let themeFrameView = contentView?.rootView else { return nil } - let titlebarView = if themeFrameView.responds(to: Selector(("titlebarView"))) { - themeFrameView.value(forKey: "titlebarView") as? NSView - } else { - NSView?.none - } - return titlebarView - } - - func findTabBar() -> NSView? { - findTitlebarView()?.firstDescendant(withClassName: "NSTabBar") - } - - /// Returns true if there is a tab bar visible on this window. - var hasTabBar: Bool { - findTabBar() != nil - } - var hasMoreThanOneTabs: Bool { /// accessing ``tabGroup?.windows`` here /// will cause other edge cases, be careful @@ -264,7 +280,7 @@ class TerminalWindow: NSWindow { if let idx = titlebarAccessoryViewControllers.firstIndex(of: resetZoomAccessory) { removeTitlebarAccessoryViewController(at: idx) } - + // We don't need to do this with the update accessory. I don't know why but // everything works fine. } @@ -419,6 +435,7 @@ class TerminalWindow: NSWindow { // have no effect if the window is not visible. Ultimately, we'll have this called // at some point when a surface becomes focused. guard isVisible else { return } + defer { updateColorSchemeForSurfaceTree() } // Basic properties appearance = surfaceConfig.windowAppearance @@ -427,8 +444,12 @@ class TerminalWindow: NSWindow { // Window transparency only takes effect if our window is not native fullscreen. // In native fullscreen we disable transparency/opacity because the background // becomes gray and widgets show through. + // + // Also check if the user has overridden transparency to be fully opaque. + let forceOpaque = terminalController?.isBackgroundOpaque ?? false if !styleMask.contains(.fullScreen) && - surfaceConfig.backgroundOpacity < 1 + !forceOpaque && + (surfaceConfig.backgroundOpacity < 1 || surfaceConfig.backgroundBlur.isGlassStyle) { isOpaque = false @@ -437,7 +458,8 @@ class TerminalWindow: NSWindow { // Terminal.app more easily. backgroundColor = .white.withAlphaComponent(0.001) - if let appDelegate = NSApp.delegate as? AppDelegate { + // We don't need to set blur when using glass + if !surfaceConfig.backgroundBlur.isGlassStyle, let appDelegate = NSApp.delegate as? AppDelegate { ghostty_set_window_background_blur( appDelegate.ghostty.app, Unmanaged.passUnretained(self).toOpaque()) @@ -481,9 +503,13 @@ class TerminalWindow: NSWindow { return derivedConfig.backgroundColor.withAlphaComponent(alpha) } + func updateColorSchemeForSurfaceTree() { + terminalController?.updateColorSchemeForSurfaceTree() + } + private func setInitialWindowPosition(x: Int16?, y: Int16?) { // If we don't have an X/Y then we try to use the previously saved window pos. - guard let x, let y else { + guard x != nil, y != nil else { if (!LastWindowPosition.shared.restore(self)) { center() } @@ -502,7 +528,7 @@ class TerminalWindow: NSWindow { center() return } - + let frame = terminalController.adjustForWindowPosition(frame: frame, on: screen) setFrameOrigin(frame.origin) } @@ -512,20 +538,32 @@ class TerminalWindow: NSWindow { standardWindowButton(.miniaturizeButton)?.isHidden = true standardWindowButton(.zoomButton)?.isHidden = true } + + deinit { + if let observer = tabMenuObserver { + NotificationCenter.default.removeObserver(observer) + } + } // MARK: Config struct DerivedConfig { let title: String? + let backgroundBlur: Ghostty.Config.BackgroundBlur let backgroundColor: NSColor let backgroundOpacity: Double let macosWindowButtons: Ghostty.MacOSWindowButtons + let macosTitlebarStyle: String + let windowCornerRadius: CGFloat init() { self.title = nil self.backgroundColor = NSColor.windowBackgroundColor self.backgroundOpacity = 1 self.macosWindowButtons = .visible + self.backgroundBlur = .disabled + self.macosTitlebarStyle = "transparent" + self.windowCornerRadius = 16 } init(_ config: Ghostty.Config) { @@ -533,6 +571,18 @@ class TerminalWindow: NSWindow { self.backgroundColor = NSColor(config.backgroundColor) self.backgroundOpacity = config.backgroundOpacity self.macosWindowButtons = config.macosWindowButtons + self.backgroundBlur = config.backgroundBlur + self.macosTitlebarStyle = config.macosTitlebarStyle + + // Set corner radius based on macos-titlebar-style + // Native, transparent, and hidden styles use 16pt radius + // Tabs style uses 20pt radius + switch config.macosTitlebarStyle { + case "tabs": + self.windowCornerRadius = 20 + default: + self.windowCornerRadius = 16 + } } } } @@ -579,12 +629,12 @@ extension TerminalWindow { } } } - + /// A pill-shaped button that displays update status and provides access to update actions. struct UpdateAccessoryView: View { @ObservedObject var viewModel: ViewModel @ObservedObject var model: UpdateViewModel - + var body: some View { // We use the same top/trailing padding so that it hugs the same. UpdatePill(model: model) @@ -594,3 +644,120 @@ extension TerminalWindow { } } + +/// A small circle indicator displayed in the tab accessory view that shows +/// the user-assigned tab color. When no color is set, the view is hidden. +private struct TabColorIndicatorView: View { + /// The tab color to display. + let tabColor: TerminalTabColor + + var body: some View { + if let color = tabColor.displayColor { + Circle() + .fill(Color(color)) + .frame(width: 6, height: 6) + } else { + Circle() + .fill(Color.clear) + .frame(width: 6, height: 6) + .hidden() + } + } +} + +// MARK: - Tab Context Menu + +extension TerminalWindow { + private static let closeTabsOnRightMenuItemIdentifier = NSUserInterfaceItemIdentifier("com.mitchellh.ghostty.closeTabsOnTheRightMenuItem") + private static let changeTitleMenuItemIdentifier = NSUserInterfaceItemIdentifier("com.mitchellh.ghostty.changeTitleMenuItem") + private static let tabColorSeparatorIdentifier = NSUserInterfaceItemIdentifier("com.mitchellh.ghostty.tabColorSeparator") + + private static let tabColorPaletteIdentifier = NSUserInterfaceItemIdentifier("com.mitchellh.ghostty.tabColorPalette") + + func configureTabContextMenuIfNeeded(_ menu: NSMenu) { + guard isTabContextMenu(menu) else { return } + + // Get the target from an existing menu item. The native tab context menu items + // target the specific window/controller that was right-clicked, not the focused one. + // We need to use that same target so validation and action use the correct tab. + let targetController = menu.items + .first { $0.action == NSSelectorFromString("performClose:") } + .flatMap { $0.target as? NSWindow } + .flatMap { $0.windowController as? TerminalController } + + // Close tabs to the right + let item = NSMenuItem(title: "Close Tabs to the Right", action: #selector(TerminalController.closeTabsOnTheRight(_:)), keyEquivalent: "") + item.identifier = Self.closeTabsOnRightMenuItemIdentifier + item.target = targetController + item.setImageIfDesired(systemSymbolName: "xmark") + if menu.insertItem(item, after: NSSelectorFromString("performCloseOtherTabs:")) == nil, + menu.insertItem(item, after: NSSelectorFromString("performClose:")) == nil { + menu.addItem(item) + } + + // Other close items should have the xmark to match Safari on macOS 26 + for menuItem in menu.items { + if menuItem.action == NSSelectorFromString("performClose:") || + menuItem.action == NSSelectorFromString("performCloseOtherTabs:") { + menuItem.setImageIfDesired(systemSymbolName: "xmark") + } + } + + appendTabModifierSection(to: menu, target: targetController) + } + + private func isTabContextMenu(_ menu: NSMenu) -> Bool { + guard NSApp.keyWindow === self else { return false } + + // These selectors must all exist for it to be a tab context menu. + let requiredSelectors: Set = [ + "performClose:", + "performCloseOtherTabs:", + "moveTabToNewWindow:", + "toggleTabOverview:" + ] + + let selectorNames = Set(menu.items.compactMap { $0.action }.map { NSStringFromSelector($0) }) + return requiredSelectors.isSubset(of: selectorNames) + } + + private func appendTabModifierSection(to menu: NSMenu, target: TerminalController?) { + menu.removeItems(withIdentifiers: [ + Self.tabColorSeparatorIdentifier, + Self.changeTitleMenuItemIdentifier, + Self.tabColorPaletteIdentifier + ]) + + let separator = NSMenuItem.separator() + separator.identifier = Self.tabColorSeparatorIdentifier + menu.addItem(separator) + + // Change Title... + let changeTitleItem = NSMenuItem(title: "Change Title...", action: #selector(BaseTerminalController.changeTabTitle(_:)), keyEquivalent: "") + changeTitleItem.identifier = Self.changeTitleMenuItemIdentifier + changeTitleItem.target = target + changeTitleItem.setImageIfDesired(systemSymbolName: "pencil.line") + menu.addItem(changeTitleItem) + + let paletteItem = NSMenuItem() + paletteItem.identifier = Self.tabColorPaletteIdentifier + paletteItem.view = makeTabColorPaletteView( + selectedColor: (target?.window as? TerminalWindow)?.tabColor ?? .none + ) { [weak target] color in + (target?.window as? TerminalWindow)?.tabColor = color + } + menu.addItem(paletteItem) + } +} + +private func makeTabColorPaletteView( + selectedColor: TerminalTabColor, + selectionHandler: @escaping (TerminalTabColor) -> Void +) -> NSView { + let hostingView = NSHostingView(rootView: TabColorMenuView( + selectedColor: selectedColor, + onSelect: selectionHandler + )) + hostingView.frame.size = hostingView.intrinsicContentSize + return hostingView +} diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index 7ce138c2a..918191522 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -67,6 +67,38 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool viewModel.isMainWindow = false } + + /// On our Tahoe titlebar tabs, we need to fix up right click events because they don't work + /// naturally due to whatever mess we made. + override func sendEvent(_ event: NSEvent) { + guard viewModel.hasTabBar else { + super.sendEvent(event) + return + } + + let isRightClick = + event.type == .rightMouseDown || + (event.type == .otherMouseDown && event.buttonNumber == 2) || + (event.type == .leftMouseDown && event.modifierFlags.contains(.control)) + guard isRightClick else { + super.sendEvent(event) + return + } + + guard let tabBarView else { + super.sendEvent(event) + return + } + + let locationInTabBar = tabBarView.convert(event.locationInWindow, from: nil) + guard tabBarView.bounds.contains(locationInTabBar) else { + super.sendEvent(event) + return + } + + tabBarView.rightMouseDown(with: event) + } + // This is called by macOS for native tabbing in order to add the tab bar. We hook into // this, detect the tab bar being added, and override its behavior. override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) { @@ -144,8 +176,8 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool guard tabBarObserver == nil else { return } guard - let titlebarView = findTitlebarView(), - let tabBar = findTabBar() + let titlebarView, + let tabBarView = self.tabBarView else { return } // View model updates must happen on their own ticks. @@ -154,13 +186,13 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool } // Find our clip view - guard let clipView = tabBar.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return } + guard let clipView = tabBarView.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return } guard let accessoryView = clipView.subviews[safe: 0] else { return } guard let toolbarView = titlebarView.firstDescendant(withClassName: "NSToolbarView") else { return } // Make sure tabBar's height won't be stretched guard let newTabButton = titlebarView.firstDescendant(withClassName: "NSTabBarNewTabButton") else { return } - tabBar.frame.size.height = newTabButton.frame.width + tabBarView.frame.size.height = newTabButton.frame.width // The container is the view that we'll constrain our tab bar within. let container = toolbarView @@ -196,10 +228,10 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // other events occur, the tab bar can resize and clear our constraints. When this // happens, we need to remove our custom constraints and re-apply them once the // tab bar has proper dimensions again to avoid constraint conflicts. - tabBar.postsFrameChangedNotifications = true + tabBarView.postsFrameChangedNotifications = true tabBarObserver = NotificationCenter.default.addObserver( forName: NSView.frameDidChangeNotification, - object: tabBar, + object: tabBarView, queue: .main ) { [weak self] _ in guard let self else { return } @@ -290,7 +322,8 @@ extension TitlebarTabsTahoeTerminalWindow { } else { // 1x1.gif strikes again! For real: if we render a zero-sized // view here then the toolbar just disappears our view. I don't - // know. This appears fixed in 26.1 Beta but keep it safe for 26.0. + // know. On macOS 26.1+ the view no longer disappears, but the + // toolbar still logs an ambiguous content size warning. Color.clear.frame(width: 1, height: 1) } } diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index c0aad46b3..39db13c6d 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -5,7 +5,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { /// Titlebar tabs can't support the update accessory because of the way we layout /// the native tabs back into the menu bar. override var supportsUpdateAccessory: Bool { false } - + /// This is used to determine if certain elements should be drawn light or dark and should /// be updated whenever the window background color or surrounding elements changes. fileprivate var isLightTheme: Bool = false @@ -395,7 +395,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // Hide the window drag handle. windowDragHandle?.isHidden = true - // Reenable the main toolbar title + // Re-enable the main toolbar title if let toolbar = toolbar as? TerminalToolbar { toolbar.titleIsHidden = false } diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index 08d56c83d..a72436d7f 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -7,16 +7,16 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { /// This is necessary because various macOS operations (tab switching, tab bar /// visibility changes) can reset the titlebar appearance. private var lastSurfaceConfig: Ghostty.SurfaceView.DerivedConfig? - + /// KVO observation for tab group window changes. private var tabGroupWindowsObservation: NSKeyValueObservation? private var tabBarVisibleObservation: NSKeyValueObservation? - + deinit { tabGroupWindowsObservation?.invalidate() tabBarVisibleObservation?.invalidate() } - + // MARK: NSWindow override func awakeFromNib() { @@ -29,7 +29,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { override func becomeMain() { super.becomeMain() - + guard let lastSurfaceConfig else { return } syncAppearance(lastSurfaceConfig) @@ -42,7 +42,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { } } } - + override func update() { super.update() @@ -67,7 +67,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { // Save our config in case we need to reapply lastSurfaceConfig = surfaceConfig - // Everytime we change appearance, set KVO up again in case any of our + // Every time we change appearance, set KVO up again in case any of our // references changed (e.g. tabGroup is new). setupKVO() @@ -88,9 +88,18 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { // color of the titlebar in native fullscreen view. if let titlebarView = titlebarContainer?.firstDescendant(withClassName: "NSTitlebarView") { titlebarView.wantsLayer = true - titlebarView.layer?.backgroundColor = preferredBackgroundColor?.cgColor + + // For glass background styles, use a transparent titlebar to let the glass effect show through + // Only apply this for transparent and tabs titlebar styles + let isGlassStyle = derivedConfig.backgroundBlur.isGlassStyle + let isTransparentTitlebar = derivedConfig.macosTitlebarStyle == "transparent" || + derivedConfig.macosTitlebarStyle == "tabs" + + titlebarView.layer?.backgroundColor = (isGlassStyle && isTransparentTitlebar) + ? NSColor.clear.cgColor + : preferredBackgroundColor?.cgColor } - + // In all cases, we have to hide the background view since this has multiple subviews // that force a background color. titlebarBackgroundView?.isHidden = true @@ -99,14 +108,14 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { @available(macOS 13.0, *) private func syncAppearanceVentura(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { guard let titlebarContainer else { return } - + // Setup the titlebar background color to match ours titlebarContainer.wantsLayer = true titlebarContainer.layer?.backgroundColor = preferredBackgroundColor?.cgColor - + // See the docs for the function that sets this to true on why effectViewIsHidden = false - + // Necessary to not draw the border around the title titlebarAppearsTransparent = true } @@ -132,7 +141,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { // Remove existing observation if any tabGroupWindowsObservation?.invalidate() tabGroupWindowsObservation = nil - + // Check if tabGroup is available guard let tabGroup else { return } @@ -161,7 +170,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { // Remove existing observation if any tabBarVisibleObservation?.invalidate() tabBarVisibleObservation = nil - + // Set up KVO observation for isTabBarVisible tabBarVisibleObservation = tabGroup?.observe( \.isTabBarVisible, @@ -172,18 +181,18 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { self.syncAppearance(lastSurfaceConfig) } } - + // MARK: macOS 13 to 15 - + // We only need to set this once, but need to do it after the window has been created in order // to determine if the theme is using a very dark background, in which case we don't want to // remove the effect view if the default tab bar is being used since the effect created in // `updateTabsForVeryDarkBackgrounds` creates a confusing visual design. private var effectViewIsHidden = false - + private func hideEffectView() { guard !effectViewIsHidden else { return } - + // By hiding the visual effect view, we allow the window's (or titlebar's in this case) // background color to show through. If we were to set `titlebarAppearsTransparent` to true // the selected tab would look fine, but the unselected ones and new tab button backgrounds diff --git a/macos/Sources/Ghostty/Ghostty.Action.swift b/macos/Sources/Ghostty/Ghostty.Action.swift index 8fce2199d..91f1491dd 100644 --- a/macos/Sources/Ghostty/Ghostty.Action.swift +++ b/macos/Sources/Ghostty/Ghostty.Action.swift @@ -127,6 +127,41 @@ extension Ghostty.Action { } } } + + enum PromptTitle { + case surface + case tab + + init(_ c: ghostty_action_prompt_title_e) { + switch c { + case GHOSTTY_PROMPT_TITLE_TAB: + self = .tab + default: + self = .surface + } + } + } + + enum KeyTable { + case activate(name: String) + case deactivate + case deactivateAll + + init?(c: ghostty_action_key_table_s) { + switch c.tag { + case GHOSTTY_KEY_TABLE_ACTIVATE: + let data = Data(bytes: c.value.activate.name, count: c.value.activate.len) + let name = String(data: data, encoding: .utf8) ?? "" + self = .activate(name: name) + case GHOSTTY_KEY_TABLE_DEACTIVATE: + self = .deactivate + case GHOSTTY_KEY_TABLE_DEACTIVATE_ALL: + self = .deactivateAll + default: + return nil + } + } + } } // Putting the initializer in an extension preserves the automatic one. diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 39ebbb51f..59389c5c0 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -29,6 +29,8 @@ extension Ghostty { /// configuration (i.e. font size) from the previously focused window. This would override this. @Published private(set) var config: Config + /// Preferred config file than the default ones + private var configPath: String? /// The ghostty app instance. We only have one of these for the entire app, although I guess /// in theory you can have multiple... I don't know why you would... @Published var app: ghostty_app_t? = nil { @@ -44,9 +46,10 @@ extension Ghostty { return ghostty_app_needs_confirm_quit(app) } - init() { + init(configPath: String? = nil) { + self.configPath = configPath // Initialize the global configuration. - self.config = Config() + self.config = Config(at: configPath) if self.config.config == nil { readiness = .error return @@ -143,7 +146,7 @@ extension Ghostty { } // Hard or full updates have to reload the full configuration - let newConfig = Config() + let newConfig = Config(at: configPath) guard newConfig.loaded else { Ghostty.logger.warning("failed to reload configuration") return @@ -163,7 +166,7 @@ extension Ghostty { // Hard or full updates have to reload the full configuration. // NOTE: We never set this on self.config because this is a surface-only // config. We free it after the call. - let newConfig = Config() + let newConfig = Config(at: configPath) guard newConfig.loaded else { Ghostty.logger.warning("failed to reload configuration") return @@ -501,14 +504,17 @@ extension Ghostty { case GHOSTTY_ACTION_GOTO_SPLIT: return gotoSplit(app, target: target, direction: action.action.goto_split) + case GHOSTTY_ACTION_GOTO_WINDOW: + return gotoWindow(app, target: target, direction: action.action.goto_window) + case GHOSTTY_ACTION_RESIZE_SPLIT: - resizeSplit(app, target: target, resize: action.action.resize_split) + return resizeSplit(app, target: target, resize: action.action.resize_split) case GHOSTTY_ACTION_EQUALIZE_SPLITS: equalizeSplits(app, target: target) case GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM: - toggleSplitZoom(app, target: target) + return toggleSplitZoom(app, target: target) case GHOSTTY_ACTION_INSPECTOR: controlInspector(app, target: target, mode: action.action.inspector) @@ -523,7 +529,7 @@ extension Ghostty { setTitle(app, target: target, v: action.action.set_title) case GHOSTTY_ACTION_PROMPT_TITLE: - return promptTitle(app, target: target) + return promptTitle(app, target: target, v: action.action.prompt_title) case GHOSTTY_ACTION_PWD: pwdChanged(app, target: target, v: action.action.pwd) @@ -570,9 +576,15 @@ extension Ghostty { case GHOSTTY_ACTION_TOGGLE_VISIBILITY: toggleVisibility(app, target: target) + case GHOSTTY_ACTION_TOGGLE_BACKGROUND_OPACITY: + toggleBackgroundOpacity(app, target: target) + case GHOSTTY_ACTION_KEY_SEQUENCE: keySequence(app, target: target, v: action.action.key_sequence) - + + case GHOSTTY_ACTION_KEY_TABLE: + keyTable(app, target: target, v: action.action.key_table) + case GHOSTTY_ACTION_PROGRESS_REPORT: progressReport(app, target: target, v: action.action.progress_report) @@ -588,6 +600,9 @@ extension Ghostty { case GHOSTTY_ACTION_RING_BELL: ringBell(app, target: target) + case GHOSTTY_ACTION_READONLY: + setReadonly(app, target: target, v: action.action.readonly) + case GHOSTTY_ACTION_CHECK_FOR_UPDATES: checkForUpdates(app) @@ -618,12 +633,13 @@ extension Ghostty { case GHOSTTY_ACTION_SEARCH_SELECTED: searchSelected(app, target: target, v: action.action.search_selected) + case GHOSTTY_ACTION_PRESENT_TERMINAL: + return presentTerminal(app, target: target) + case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW: fallthrough case GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS: fallthrough - case GHOSTTY_ACTION_PRESENT_TERMINAL: - fallthrough case GHOSTTY_ACTION_SIZE_LIMIT: fallthrough case GHOSTTY_ACTION_QUIT_TIMER: @@ -760,7 +776,7 @@ extension Ghostty { name: Notification.ghosttyNewWindow, object: surfaceView, userInfo: [ - Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface)), + Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface, GHOSTTY_SURFACE_CONTEXT_WINDOW)), ] ) @@ -797,7 +813,7 @@ extension Ghostty { name: Notification.ghosttyNewTab, object: surfaceView, userInfo: [ - Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface)), + Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface, GHOSTTY_SURFACE_CONTEXT_TAB)), ] ) @@ -826,7 +842,7 @@ extension Ghostty { object: surfaceView, userInfo: [ "direction": direction, - Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface)), + Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface, GHOSTTY_SURFACE_CONTEXT_SPLIT)), ] ) @@ -836,6 +852,30 @@ extension Ghostty { } } + private static func presentTerminal( + _ app: ghostty_app_t, + target: ghostty_target_s + ) -> Bool { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + return false + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return false } + guard let surfaceView = self.surfaceView(from: surface) else { return false } + + NotificationCenter.default.post( + name: Notification.ghosttyPresentTerminal, + object: surfaceView + ) + return true + + default: + assertionFailure() + return false + } + } + private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s, mode: ghostty_action_close_tab_mode_e) { switch (target.tag) { case GHOSTTY_TARGET_APP: @@ -861,6 +901,13 @@ extension Ghostty { ) return + case GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT: + NotificationCenter.default.post( + name: .ghosttyCloseTabsOnTheRight, + object: surfaceView + ) + return + default: assertionFailure() } @@ -1003,6 +1050,31 @@ extension Ghostty { } } + private static func setReadonly( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_action_readonly_e) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("set readonly does nothing with an app target") + return + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return } + guard let surfaceView = self.surfaceView(from: surface) else { return } + NotificationCenter.default.post( + name: .ghosttyDidChangeReadonly, + object: surfaceView, + userInfo: [ + SwiftUI.Notification.Name.ReadonlyKey: v == GHOSTTY_READONLY_ON, + ] + ) + + default: + assertionFailure() + } + } + private static func moveTab( _ app: ghostty_app_t, target: ghostty_target_s, @@ -1114,19 +1186,82 @@ extension Ghostty { } } + private static func gotoWindow( + _ app: ghostty_app_t, + target: ghostty_target_s, + direction: ghostty_action_goto_window_e + ) -> Bool { + // Collect candidate windows: visible terminal windows that are either + // standalone or the currently selected tab in their tab group. This + // treats each native tab group as a single "window" for navigation + // purposes, since goto_tab handles per-tab navigation. + let candidates: [NSWindow] = NSApplication.shared.windows.filter { window in + guard window.windowController is BaseTerminalController else { return false } + guard window.isVisible, !window.isMiniaturized else { return false } + // For native tabs, only include the selected tab in each group + if let group = window.tabGroup, group.selectedWindow !== window { + return false + } + return true + } + + // Need at least two windows to navigate between + guard candidates.count > 1 else { return false } + + // Find starting index from the current key/main window + let startIndex = candidates.firstIndex(where: { $0.isKeyWindow }) + ?? candidates.firstIndex(where: { $0.isMainWindow }) + ?? 0 + + let step: Int + switch direction { + case GHOSTTY_GOTO_WINDOW_NEXT: + step = 1 + case GHOSTTY_GOTO_WINDOW_PREVIOUS: + step = -1 + default: + return false + } + + // Iterate with wrap-around until we find a valid window or return to start + let count = candidates.count + var index = (startIndex + step + count) % count + + while index != startIndex { + let candidate = candidates[index] + if candidate.isVisible, !candidate.isMiniaturized { + candidate.makeKeyAndOrderFront(nil) + // Also focus the terminal surface within the window + if let controller = candidate.windowController as? BaseTerminalController, + let surface = controller.focusedSurface { + Ghostty.moveFocus(to: surface) + } + return true + } + index = (index + step + count) % count + } + + return false + } + private static func resizeSplit( _ app: ghostty_app_t, target: ghostty_target_s, - resize: ghostty_action_resize_split_s) { + resize: ghostty_action_resize_split_s) -> Bool { switch (target.tag) { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("resize split does nothing with an app target") - return + return false case GHOSTTY_TARGET_SURFACE: - guard let surface = target.target.surface else { return } - guard let surfaceView = self.surfaceView(from: surface) else { return } - guard let resizeDirection = SplitResizeDirection.from(direction: resize.direction) else { return } + guard let surface = target.target.surface else { return false } + guard let surfaceView = self.surfaceView(from: surface) else { return false } + guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { return false } + + // If the window has no splits, the action is not performable + guard controller.surfaceTree.isSplit else { return false } + + guard let resizeDirection = SplitResizeDirection.from(direction: resize.direction) else { return false } NotificationCenter.default.post( name: Notification.didResizeSplit, object: surfaceView, @@ -1135,9 +1270,11 @@ extension Ghostty { Notification.ResizeSplitAmountKey: resize.amount, ] ) + return true default: assertionFailure() + return false } } @@ -1165,23 +1302,30 @@ extension Ghostty { private static func toggleSplitZoom( _ app: ghostty_app_t, - target: ghostty_target_s) { + target: ghostty_target_s) -> Bool { switch (target.tag) { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle split zoom does nothing with an app target") - return + return false case GHOSTTY_TARGET_SURFACE: - guard let surface = target.target.surface else { return } - guard let surfaceView = self.surfaceView(from: surface) else { return } + guard let surface = target.target.surface else { return false } + guard let surfaceView = self.surfaceView(from: surface) else { return false } + guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { return false } + + // If the window has no splits, the action is not performable + guard controller.surfaceTree.isSplit else { return false } + NotificationCenter.default.post( name: Notification.didToggleSplitZoom, object: surfaceView ) + return true default: assertionFailure() + return false } } @@ -1279,6 +1423,27 @@ extension Ghostty { } } + private static func toggleBackgroundOpacity( + _ app: ghostty_app_t, + target: ghostty_target_s + ) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("toggle background opacity does nothing with an app target") + return + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface, + let surfaceView = self.surfaceView(from: surface), + let controller = surfaceView.window?.windowController as? BaseTerminalController else { return } + + controller.toggleBackgroundOpacity() + + default: + assertionFailure() + } + } + private static func toggleSecureInput( _ app: ghostty_app_t, target: ghostty_target_s, @@ -1343,22 +1508,50 @@ extension Ghostty { private static func promptTitle( _ app: ghostty_app_t, - target: ghostty_target_s) -> Bool { - switch (target.tag) { - case GHOSTTY_TARGET_APP: - Ghostty.logger.warning("set title prompt does nothing with an app target") - return false + target: ghostty_target_s, + v: ghostty_action_prompt_title_e) -> Bool { + let promptTitle = Action.PromptTitle(v) + switch promptTitle { + case .surface: + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("set title prompt does nothing with an app target") + return false - case GHOSTTY_TARGET_SURFACE: - guard let surface = target.target.surface else { return false } - guard let surfaceView = self.surfaceView(from: surface) else { return false } - surfaceView.promptTitle() + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return false } + guard let surfaceView = self.surfaceView(from: surface) else { return false } + surfaceView.promptTitle() + return true - default: - assertionFailure() + default: + assertionFailure() + return false + } + + case .tab: + switch (target.tag) { + case GHOSTTY_TARGET_APP: + guard let window = NSApp.mainWindow ?? NSApp.keyWindow, + let controller = window.windowController as? BaseTerminalController + else { return false } + controller.promptTabTitle() + return true + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return false } + guard let surfaceView = self.surfaceView(from: surface) else { return false } + guard let window = surfaceView.window, + let controller = window.windowController as? BaseTerminalController + else { return false } + controller.promptTabTitle() + return true + + default: + assertionFailure() + return false + } } - - return true } private static func pwdChanged( @@ -1598,7 +1791,32 @@ extension Ghostty { assertionFailure() } } - + + private static func keyTable( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_action_key_table_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("key table does nothing with an app target") + return + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return } + guard let surfaceView = self.surfaceView(from: surface) else { return } + guard let action = Ghostty.Action.KeyTable(c: v) else { return } + + NotificationCenter.default.post( + name: Notification.didChangeKeyTable, + object: surfaceView, + userInfo: [Notification.KeyTableKey: action] + ) + + default: + assertionFailure() + } + } + private static func progressReport( _ app: ghostty_app_t, target: ghostty_target_s, @@ -1668,11 +1886,15 @@ extension Ghostty { let startSearch = Ghostty.Action.StartSearch(c: v) DispatchQueue.main.async { - if surfaceView.searchState != nil { - NotificationCenter.default.post(name: .ghosttySearchFocus, object: surfaceView) + if let searchState = surfaceView.searchState { + if let needle = startSearch.needle, !needle.isEmpty { + searchState.needle = needle + } } else { surfaceView.searchState = Ghostty.SurfaceView.SearchState(from: startSearch) } + + NotificationCenter.default.post(name: .ghosttySearchFocus, object: surfaceView) } default: diff --git a/macos/Sources/Ghostty/Ghostty.Command.swift b/macos/Sources/Ghostty/Ghostty.Command.swift index 1479ae92d..797d469c5 100644 --- a/macos/Sources/Ghostty/Ghostty.Command.swift +++ b/macos/Sources/Ghostty/Ghostty.Command.swift @@ -3,28 +3,18 @@ import GhosttyKit extension Ghostty { /// `ghostty_command_s` struct Command: Sendable { - private let cValue: ghostty_command_s - /// The title of the command. - var title: String { - String(cString: cValue.title) - } + let title: String /// Human-friendly description of what this command will do. - var description: String { - String(cString: cValue.description) - } + let description: String /// The full action that must be performed to invoke this command. - var action: String { - String(cString: cValue.action) - } + let action: String /// Only the key portion of the action so you can compare action types, e.g. `goto_split` /// instead of `goto_split:left`. - var actionKey: String { - String(cString: cValue.action_key) - } + let actionKey: String /// True if this can be performed on this target. var isSupported: Bool { @@ -40,7 +30,10 @@ extension Ghostty { ] init(cValue: ghostty_command_s) { - self.cValue = cValue + self.title = String(cString: cValue.title) + self.description = String(cString: cValue.description) + self.action = String(cString: cValue.action) + self.actionKey = String(cString: cValue.action_key) } } } diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 2df0a8656..c64646e25 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -33,14 +33,16 @@ extension Ghostty { return diags } - init() { - if let cfg = Self.loadConfig() { - self.config = cfg - } + init(config: ghostty_config_t?) { + self.config = config } - init(clone config: ghostty_config_t) { - self.config = ghostty_config_clone(config) + convenience init(at path: String? = nil, finalize: Bool = true) { + self.init(config: Self.loadConfig(at: path, finalize: finalize)) + } + + convenience init(clone config: ghostty_config_t) { + self.init(config: ghostty_config_clone(config)) } deinit { @@ -48,7 +50,10 @@ extension Ghostty { } /// Initializes a new configuration and loads all the values. - static private func loadConfig() -> ghostty_config_t? { + /// - Parameters: + /// - path: An optional preferred config file path. Pass `nil` to load the default configuration files. + /// - finalize: Whether to finalize the configuration to populate default values. + static private func loadConfig(at path: String?, finalize: Bool) -> ghostty_config_t? { // Initialize the global configuration. guard let cfg = ghostty_config_new() else { logger.critical("ghostty_config_new failed") @@ -59,7 +64,11 @@ extension Ghostty { // We only do this on macOS because other Apple platforms do not have the // same filesystem concept. #if os(macOS) - ghostty_config_load_default_files(cfg); + if let path { + ghostty_config_load_file(cfg, path) + } else { + ghostty_config_load_default_files(cfg) + } // We only load CLI args when not running in Xcode because in Xcode we // pass some special parameters to control the debugger. @@ -74,9 +83,10 @@ extension Ghostty { // have to do this synchronously. When we support config updating we can do // this async and update later. - // Finalize will make our defaults available. - ghostty_config_finalize(cfg) - + if finalize { + // Finalize will make our defaults available. + ghostty_config_finalize(cfg) + } // Log any configuration errors. These will be automatically shown in a // pop-up window too. let diagsCount = ghostty_config_diagnostics_count(cfg) @@ -124,6 +134,14 @@ extension Ghostty { return .init(rawValue: v) } + var splitPreserveZoom: SplitPreserveZoom { + guard let config = self.config else { return .init() } + var v: CUnsignedInt = 0 + let key = "split-preserve-zoom" + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .init() } + return .init(rawValue: v) + } + var initialWindow: Bool { guard let config = self.config else { return true } var v = true; @@ -402,12 +420,12 @@ extension Ghostty { return v; } - var backgroundBlurRadius: Int { - guard let config = self.config else { return 1 } - var v: Int = 0 + var backgroundBlur: BackgroundBlur { + guard let config = self.config else { return .disabled } + var v: Int16 = 0 let key = "background-blur" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) - return v; + return BackgroundBlur(fromCValue: v) } var unfocusedSplitOpacity: Double { @@ -614,6 +632,16 @@ extension Ghostty { let str = String(cString: ptr) return Scrollbar(rawValue: str) ?? defaultValue } + + var commandPaletteEntries: [Ghostty.Command] { + guard let config = self.config else { return [] } + var v: ghostty_config_command_list_s = .init() + let key = "command-palette-entry" + guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return [] } + guard v.len > 0 else { return [] } + let buffer = UnsafeBufferPointer(start: v.commands, count: v.len) + return buffer.map { Ghostty.Command(cValue: $0) } + } } } @@ -626,6 +654,68 @@ extension Ghostty.Config { case download } + /// Background blur configuration that maps from the C API values. + /// Positive values represent blur radius, special negative values + /// represent macOS-specific glass effects. + enum BackgroundBlur: Equatable { + case disabled + case radius(Int) + case macosGlassRegular + case macosGlassClear + + init(fromCValue value: Int16) { + switch value { + case 0: + self = .disabled + case -1: + if #available(macOS 26.0, *) { + self = .macosGlassRegular + } else { + self = .disabled + } + case -2: + if #available(macOS 26.0, *) { + self = .macosGlassClear + } else { + self = .disabled + } + default: + self = .radius(Int(value)) + } + } + + var isEnabled: Bool { + switch self { + case .disabled: + return false + default: + return true + } + } + + /// Returns true if this is a macOS glass style (regular or clear). + var isGlassStyle: Bool { + switch self { + case .macosGlassRegular, .macosGlassClear: + return true + default: + return false + } + } + + /// Returns the blur radius if applicable, nil for glass effects. + var radius: Int? { + switch self { + case .disabled: + return nil + case .radius(let r): + return r + case .macosGlassRegular, .macosGlassClear: + return nil + } + } + } + struct BellFeatures: OptionSet { let rawValue: CUnsignedInt @@ -635,6 +725,12 @@ extension Ghostty.Config { static let title = BellFeatures(rawValue: 1 << 3) static let border = BellFeatures(rawValue: 1 << 4) } + + struct SplitPreserveZoom: OptionSet { + let rawValue: CUnsignedInt + + static let navigation = SplitPreserveZoom(rawValue: 1 << 0) + } enum MacDockDropBehavior: String { case new_tab = "new-tab" diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index e05911c06..7b2905abb 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -32,6 +32,10 @@ extension Ghostty { guard let scalar = UnicodeScalar(trigger.key.unicode) else { return nil } key = KeyEquivalent(Character(scalar)) + case GHOSTTY_TRIGGER_CATCH_ALL: + // catch_all matches any key, so it can't be represented as a KeyboardShortcut + return nil + default: return nil } @@ -64,7 +68,7 @@ extension Ghostty { if (flags.contains(.capsLock)) { mods |= GHOSTTY_MODS_CAPS.rawValue } // Handle sided input. We can't tell that both are pressed in the - // Ghostty structure but thats okay -- we don't use that information. + // Ghostty structure but that's okay -- we don't use that information. let rawFlags = flags.rawValue if (rawFlags & UInt(NX_DEVICERSHIFTKEYMASK) != 0) { mods |= GHOSTTY_MODS_SHIFT_RIGHT.rawValue } if (rawFlags & UInt(NX_DEVICERCTLKEYMASK) != 0) { mods |= GHOSTTY_MODS_CTRL_RIGHT.rawValue } @@ -96,6 +100,32 @@ extension Ghostty { ] } +// MARK: Ghostty.Input.BindingFlags + +extension Ghostty.Input { + /// `ghostty_binding_flags_e` + struct BindingFlags: OptionSet, Sendable { + let rawValue: UInt32 + + static let consumed = BindingFlags(rawValue: GHOSTTY_BINDING_FLAGS_CONSUMED.rawValue) + static let all = BindingFlags(rawValue: GHOSTTY_BINDING_FLAGS_ALL.rawValue) + static let global = BindingFlags(rawValue: GHOSTTY_BINDING_FLAGS_GLOBAL.rawValue) + static let performable = BindingFlags(rawValue: GHOSTTY_BINDING_FLAGS_PERFORMABLE.rawValue) + + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + init(cFlags: ghostty_binding_flags_e) { + self.rawValue = cFlags.rawValue + } + + var cFlags: ghostty_binding_flags_e { + ghostty_binding_flags_e(rawValue) + } + } +} + // MARK: Ghostty.Input.KeyEvent extension Ghostty.Input { @@ -135,7 +165,7 @@ extension Ghostty.Input { case GHOSTTY_ACTION_REPEAT: self.action = .repeat default: self.action = .press } - + // Convert key from keycode guard let key = Key(keyCode: UInt16(cValue.keycode)) else { return nil } self.key = key @@ -146,18 +176,18 @@ extension Ghostty.Input { } else { self.text = nil } - + // Set composing state self.composing = cValue.composing - + // Convert modifiers self.mods = Mods(cMods: cValue.mods) self.consumedMods = Mods(cMods: cValue.consumed_mods) - + // Set unshifted codepoint self.unshiftedCodepoint = cValue.unshifted_codepoint } - + /// Executes a closure with a temporary C representation of this KeyEvent. /// /// This method safely converts the Swift KeyEntity to a C `ghostty_input_key_s` struct @@ -176,7 +206,7 @@ extension Ghostty.Input { keyEvent.mods = mods.cMods keyEvent.consumed_mods = consumedMods.cMods keyEvent.unshifted_codepoint = unshiftedCodepoint - + // Handle text with proper memory management if let text = text { return text.withCString { textPtr in @@ -199,7 +229,7 @@ extension Ghostty.Input { case release case press case `repeat` - + var cAction: ghostty_input_action_e { switch self { case .release: GHOSTTY_ACTION_RELEASE @@ -228,7 +258,7 @@ extension Ghostty.Input { let action: MouseState let button: MouseButton let mods: Mods - + init( action: MouseState, button: MouseButton, @@ -238,7 +268,7 @@ extension Ghostty.Input { self.button = button self.mods = mods } - + /// Creates a MouseEvent from C enum values. /// /// This initializer converts C-style mouse input enums to Swift types. @@ -255,7 +285,7 @@ extension Ghostty.Input { case GHOSTTY_MOUSE_PRESS: self.action = .press default: return nil } - + // Convert button switch button { case GHOSTTY_MOUSE_UNKNOWN: self.button = .unknown @@ -264,7 +294,7 @@ extension Ghostty.Input { case GHOSTTY_MOUSE_MIDDLE: self.button = .middle default: return nil } - + // Convert modifiers self.mods = Mods(cMods: mods) } @@ -275,7 +305,7 @@ extension Ghostty.Input { let x: Double let y: Double let mods: Mods - + init( x: Double, y: Double, @@ -312,7 +342,7 @@ extension Ghostty.Input { enum MouseState: String, CaseIterable { case release case press - + var cMouseState: ghostty_input_mouse_state_e { switch self { case .release: GHOSTTY_MOUSE_RELEASE @@ -340,13 +370,48 @@ extension Ghostty.Input { case left case right case middle - + case four + case five + case six + case seven + case eight + case nine + case ten + case eleven + var cMouseButton: ghostty_input_mouse_button_e { switch self { case .unknown: GHOSTTY_MOUSE_UNKNOWN case .left: GHOSTTY_MOUSE_LEFT case .right: GHOSTTY_MOUSE_RIGHT case .middle: GHOSTTY_MOUSE_MIDDLE + case .four: GHOSTTY_MOUSE_FOUR + case .five: GHOSTTY_MOUSE_FIVE + case .six: GHOSTTY_MOUSE_SIX + case .seven: GHOSTTY_MOUSE_SEVEN + case .eight: GHOSTTY_MOUSE_EIGHT + case .nine: GHOSTTY_MOUSE_NINE + case .ten: GHOSTTY_MOUSE_TEN + case .eleven: GHOSTTY_MOUSE_ELEVEN + } + } + + /// Initialize from NSEvent.buttonNumber + /// NSEvent buttonNumber: 0=left, 1=right, 2=middle, 3=back (button 8), 4=forward (button 9), etc. + init(fromNSEventButtonNumber buttonNumber: Int) { + switch buttonNumber { + case 0: self = .left + case 1: self = .right + case 2: self = .middle + case 3: self = .eight // Back button + case 4: self = .nine // Forward button + case 5: self = .six + case 6: self = .seven + case 7: self = .four + case 8: self = .five + case 9: self = .ten + case 10: self = .eleven + default: self = .unknown } } } @@ -378,18 +443,18 @@ extension Ghostty.Input { /// for scroll events, matching the Zig `ScrollMods` packed struct. struct ScrollMods { let rawValue: Int32 - + /// True if this is a high-precision scroll event (e.g., trackpad, Magic Mouse) var precision: Bool { rawValue & 0b0000_0001 != 0 } - + /// The momentum phase of the scroll event for inertial scrolling var momentum: Momentum { let momentumBits = (rawValue >> 1) & 0b0000_0111 return Momentum(rawValue: UInt8(momentumBits)) ?? .none } - + init(precision: Bool = false, momentum: Momentum = .none) { var value: Int32 = 0 if precision { @@ -398,11 +463,11 @@ extension Ghostty.Input { value |= Int32(momentum.rawValue) << 1 self.rawValue = value } - + init(rawValue: Int32) { self.rawValue = rawValue } - + var cScrollMods: ghostty_input_scroll_mods_t { rawValue } @@ -421,7 +486,7 @@ extension Ghostty.Input { case ended = 4 case cancelled = 5 case mayBegin = 6 - + var cMomentum: ghostty_input_mouse_momentum_e { switch self { case .none: GHOSTTY_MOUSE_MOMENTUM_NONE @@ -438,7 +503,7 @@ extension Ghostty.Input { extension Ghostty.Input.Momentum: AppEnum { static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Scroll Momentum") - + static var caseDisplayRepresentations: [Ghostty.Input.Momentum : DisplayRepresentation] = [ .none: "None", .began: "Began", @@ -475,7 +540,7 @@ extension Ghostty.Input { /// `ghostty_input_mods_e` struct Mods: OptionSet { let rawValue: UInt32 - + static let none = Mods(rawValue: GHOSTTY_MODS_NONE.rawValue) static let shift = Mods(rawValue: GHOSTTY_MODS_SHIFT.rawValue) static let ctrl = Mods(rawValue: GHOSTTY_MODS_CTRL.rawValue) @@ -486,23 +551,23 @@ extension Ghostty.Input { static let ctrlRight = Mods(rawValue: GHOSTTY_MODS_CTRL_RIGHT.rawValue) static let altRight = Mods(rawValue: GHOSTTY_MODS_ALT_RIGHT.rawValue) static let superRight = Mods(rawValue: GHOSTTY_MODS_SUPER_RIGHT.rawValue) - + var cMods: ghostty_input_mods_e { ghostty_input_mods_e(rawValue) } - + init(rawValue: UInt32) { self.rawValue = rawValue } - + init(cMods: ghostty_input_mods_e) { self.rawValue = cMods.rawValue } - + init(nsFlags: NSEvent.ModifierFlags) { self.init(cMods: Ghostty.ghosttyMods(nsFlags)) } - + var nsFlags: NSEvent.ModifierFlags { Ghostty.eventModifierFlags(mods: cMods) } @@ -1116,43 +1181,43 @@ extension Ghostty.Input.Key: AppEnum { return [ // Letters (A-Z) .a, .b, .c, .d, .e, .f, .g, .h, .i, .j, .k, .l, .m, .n, .o, .p, .q, .r, .s, .t, .u, .v, .w, .x, .y, .z, - + // Numbers (0-9) .digit0, .digit1, .digit2, .digit3, .digit4, .digit5, .digit6, .digit7, .digit8, .digit9, - + // Common Control Keys .space, .enter, .tab, .backspace, .escape, .delete, - + // Arrow Keys .arrowUp, .arrowDown, .arrowLeft, .arrowRight, - + // Navigation Keys .home, .end, .pageUp, .pageDown, .insert, - + // Function Keys (F1-F20) .f1, .f2, .f3, .f4, .f5, .f6, .f7, .f8, .f9, .f10, .f11, .f12, .f13, .f14, .f15, .f16, .f17, .f18, .f19, .f20, - + // Modifier Keys .shiftLeft, .shiftRight, .controlLeft, .controlRight, .altLeft, .altRight, .metaLeft, .metaRight, .capsLock, - + // Punctuation & Symbols .minus, .equal, .backquote, .bracketLeft, .bracketRight, .backslash, .semicolon, .quote, .comma, .period, .slash, - + // Numpad .numLock, .numpad0, .numpad1, .numpad2, .numpad3, .numpad4, .numpad5, .numpad6, .numpad7, .numpad8, .numpad9, .numpadAdd, .numpadSubtract, .numpadMultiply, .numpadDivide, .numpadDecimal, .numpadEqual, .numpadEnter, .numpadComma, - + // Media Keys .audioVolumeUp, .audioVolumeDown, .audioVolumeMute, - + // International Keys .intlBackslash, .intlRo, .intlYen, - + // Other .contextMenu ] @@ -1163,11 +1228,11 @@ extension Ghostty.Input.Key: AppEnum { .a: "A", .b: "B", .c: "C", .d: "D", .e: "E", .f: "F", .g: "G", .h: "H", .i: "I", .j: "J", .k: "K", .l: "L", .m: "M", .n: "N", .o: "O", .p: "P", .q: "Q", .r: "R", .s: "S", .t: "T", .u: "U", .v: "V", .w: "W", .x: "X", .y: "Y", .z: "Z", - + // Numbers (0-9) .digit0: "0", .digit1: "1", .digit2: "2", .digit3: "3", .digit4: "4", .digit5: "5", .digit6: "6", .digit7: "7", .digit8: "8", .digit9: "9", - + // Common Control Keys .space: "Space", .enter: "Enter", @@ -1175,26 +1240,26 @@ extension Ghostty.Input.Key: AppEnum { .backspace: "Backspace", .escape: "Escape", .delete: "Delete", - + // Arrow Keys .arrowUp: "Up Arrow", .arrowDown: "Down Arrow", .arrowLeft: "Left Arrow", .arrowRight: "Right Arrow", - + // Navigation Keys .home: "Home", .end: "End", .pageUp: "Page Up", .pageDown: "Page Down", .insert: "Insert", - + // Function Keys (F1-F20) .f1: "F1", .f2: "F2", .f3: "F3", .f4: "F4", .f5: "F5", .f6: "F6", .f7: "F7", .f8: "F8", .f9: "F9", .f10: "F10", .f11: "F11", .f12: "F12", .f13: "F13", .f14: "F14", .f15: "F15", .f16: "F16", .f17: "F17", .f18: "F18", .f19: "F19", .f20: "F20", - + // Modifier Keys .shiftLeft: "Left Shift", .shiftRight: "Right Shift", @@ -1205,7 +1270,7 @@ extension Ghostty.Input.Key: AppEnum { .metaLeft: "Left Command", .metaRight: "Right Command", .capsLock: "Caps Lock", - + // Punctuation & Symbols .minus: "Minus (-)", .equal: "Equal (=)", @@ -1218,7 +1283,7 @@ extension Ghostty.Input.Key: AppEnum { .comma: "Comma (,)", .period: "Period (.)", .slash: "Slash (/)", - + // Numpad .numLock: "Num Lock", .numpad0: "Numpad 0", .numpad1: "Numpad 1", .numpad2: "Numpad 2", @@ -1232,17 +1297,17 @@ extension Ghostty.Input.Key: AppEnum { .numpadEqual: "Numpad Equal", .numpadEnter: "Numpad Enter", .numpadComma: "Numpad Comma", - + // Media Keys .audioVolumeUp: "Volume Up", .audioVolumeDown: "Volume Down", .audioVolumeMute: "Volume Mute", - + // International Keys .intlBackslash: "International Backslash", .intlRo: "International Ro", .intlYen: "International Yen", - + // Other .contextMenu: "Context Menu" ] diff --git a/macos/Sources/Ghostty/Ghostty.Surface.swift b/macos/Sources/Ghostty/Ghostty.Surface.swift index c7198e147..7cb32ed71 100644 --- a/macos/Sources/Ghostty/Ghostty.Surface.swift +++ b/macos/Sources/Ghostty/Ghostty.Surface.swift @@ -62,6 +62,26 @@ extension Ghostty { } } + /// Check if a key event matches a keybinding. + /// + /// This checks whether the given key event would trigger a keybinding in the terminal. + /// If it matches, returns the binding flags indicating properties of the matched binding. + /// + /// - Parameter event: The key event to check + /// - Returns: The binding flags if a binding matches, or nil if no binding matches + @MainActor + func keyIsBinding(_ event: ghostty_input_key_s) -> Input.BindingFlags? { + var flags = ghostty_binding_flags_e(0) + guard ghostty_surface_key_is_binding(surface, event, &flags) else { return nil } + return Input.BindingFlags(cFlags: flags) + } + + /// See `keyIsBinding(_ event: ghostty_input_key_s)`. + @MainActor + func keyIsBinding(_ event: Input.KeyEvent) -> Input.BindingFlags? { + event.withCValue { keyIsBinding($0) } + } + /// Whether the terminal has captured mouse input. /// /// When the mouse is captured, the terminal application is receiving mouse events @@ -134,16 +154,5 @@ extension Ghostty { ghostty_surface_binding_action(surface, cString, UInt(len - 1)) } } - - /// Command options for this surface. - @MainActor - func commands() throws -> [Command] { - var ptr: UnsafeMutablePointer? = nil - var count: Int = 0 - ghostty_surface_commands(surface, &ptr, &count) - guard let ptr else { throw Error.apiFailed } - let buffer = UnsafeBufferPointer(start: ptr, count: count) - return Array(buffer).map { Command(cValue: $0) }.filter { $0.isSupported } - } } } diff --git a/macos/Sources/Ghostty/GhosttyDelegate.swift b/macos/Sources/Ghostty/GhosttyDelegate.swift new file mode 100644 index 000000000..a9d255737 --- /dev/null +++ b/macos/Sources/Ghostty/GhosttyDelegate.swift @@ -0,0 +1,10 @@ +import Foundation + +extension Ghostty { + /// This is a delegate that should be applied to your global app delegate for GhosttyKit + /// to perform app-global operations. + protocol Delegate { + /// Look up a surface within the application by ID. + func ghosttySurface(id: UUID) -> SurfaceView? + } +} diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 7ee815caa..15cb3a51e 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -56,7 +56,7 @@ extension Ghostty { case app case zig_run } - + /// Returns the mechanism that launched the app. This is based on an env var so /// its up to the env var being set in the correct circumstance. static var launchSource: LaunchSource { @@ -65,7 +65,7 @@ extension Ghostty { // source. If its unset we assume we're in a CLI environment. return .cli } - + // If the env var is set but its unknown then we default back to the app. return LaunchSource(rawValue: envValue) ?? .app } @@ -76,17 +76,17 @@ extension Ghostty { extension Ghostty { class AllocatedString { private let cString: ghostty_string_s - + init(_ c: ghostty_string_s) { self.cString = c } - + var string: String { guard let ptr = cString.ptr else { return "" } let data = Data(bytes: ptr, count: Int(cString.len)) return String(data: data, encoding: .utf8) ?? "" } - + deinit { ghostty_string_free(cString) } @@ -330,6 +330,22 @@ extension Ghostty { case xray case custom case customStyle = "custom-style" + + /// Bundled asset name for built-in icons + var assetName: String? { + switch self { + case .official: return nil + case .blueprint: return "BlueprintImage" + case .chalkboard: return "ChalkboardImage" + case .microchip: return "MicrochipImage" + case .glass: return "GlassImage" + case .holographic: return "HolographicImage" + case .paper: return "PaperImage" + case .retro: return "RetroImage" + case .xray: return "XrayImage" + case .custom, .customStyle: return nil + } + } } /// macos-icon-frame @@ -380,6 +396,9 @@ extension Notification.Name { /// Close other tabs static let ghosttyCloseOtherTabs = Notification.Name("com.mitchellh.ghostty.closeOtherTabs") + /// Close tabs to the right of the focused tab + static let ghosttyCloseTabsOnTheRight = Notification.Name("com.mitchellh.ghostty.closeTabsOnTheRight") + /// Close window static let ghosttyCloseWindow = Notification.Name("com.mitchellh.ghostty.closeWindow") @@ -388,6 +407,10 @@ extension Notification.Name { /// Ring the bell static let ghosttyBellDidRing = Notification.Name("com.mitchellh.ghostty.ghosttyBellDidRing") + + /// Readonly mode changed + static let ghosttyDidChangeReadonly = Notification.Name("com.mitchellh.ghostty.didChangeReadonly") + static let ReadonlyKey = ghosttyDidChangeReadonly.rawValue + ".readonly" static let ghosttyCommandPaletteDidToggle = Notification.Name("com.mitchellh.ghostty.commandPaletteDidToggle") /// Toggle maximize of current window @@ -428,6 +451,9 @@ extension Ghostty.Notification { /// New window. Has base surface config requested in userinfo. static let ghosttyNewWindow = Notification.Name("com.mitchellh.ghostty.newWindow") + /// Present terminal. Bring the surface's window to focus without activating the app. + static let ghosttyPresentTerminal = Notification.Name("com.mitchellh.ghostty.presentTerminal") + /// Toggle fullscreen of current window static let ghosttyToggleFullscreen = Notification.Name("com.mitchellh.ghostty.toggleFullscreen") static let FullscreenModeKey = ghosttyToggleFullscreen.rawValue @@ -465,6 +491,10 @@ extension Ghostty.Notification { static let didContinueKeySequence = Notification.Name("com.mitchellh.ghostty.didContinueKeySequence") static let didEndKeySequence = Notification.Name("com.mitchellh.ghostty.didEndKeySequence") static let KeySequenceKey = didContinueKeySequence.rawValue + ".key" + + /// Notifications related to key tables + static let didChangeKeyTable = Notification.Name("com.mitchellh.ghostty.didChangeKeyTable") + static let KeyTableKey = didChangeKeyTable.rawValue + ".action" } // Make the input enum hashable. diff --git a/macos/Sources/Ghostty/InspectorView.swift b/macos/Sources/Ghostty/Surface View/InspectorView.swift similarity index 100% rename from macos/Sources/Ghostty/InspectorView.swift rename to macos/Sources/Ghostty/Surface View/InspectorView.swift diff --git a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift new file mode 100644 index 000000000..37a69852e --- /dev/null +++ b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift @@ -0,0 +1,268 @@ +import AppKit +import SwiftUI + +extension Ghostty { + /// A preference key that propagates the ID of the SurfaceView currently being dragged, + /// or nil if no surface is being dragged. + struct DraggingSurfaceKey: PreferenceKey { + static var defaultValue: SurfaceView.ID? = nil + + static func reduce(value: inout SurfaceView.ID?, nextValue: () -> SurfaceView.ID?) { + value = nextValue() ?? value + } + } + + /// A SwiftUI view that provides drag source functionality for terminal surfaces. + /// + /// This view wraps an AppKit-based drag source to enable drag-and-drop reordering + /// of terminal surfaces within split views. When the user drags this view, it initiates + /// an `NSDraggingSession` with the surface's UUID encoded in the pasteboard, allowing + /// drop targets to identify which surface is being moved. + /// + /// The view also publishes the dragging state via `DraggingSurfaceKey` preference, + /// enabling parent views to react to ongoing drag operations. + struct SurfaceDragSource: View { + /// The surface view that will be dragged. + let surfaceView: SurfaceView + + /// Binding that reflects whether a drag session is currently active. + @Binding var isDragging: Bool + + /// Binding that reflects whether the mouse is hovering over this view. + @Binding var isHovering: Bool + + var body: some View { + SurfaceDragSourceViewRepresentable( + surfaceView: surfaceView, + isDragging: $isDragging, + isHovering: $isHovering) + .preference(key: DraggingSurfaceKey.self, value: isDragging ? surfaceView.id : nil) + } + } + + /// An NSViewRepresentable that provides AppKit-based drag source functionality. + /// This gives us control over the drag lifecycle, particularly detecting drag start. + fileprivate struct SurfaceDragSourceViewRepresentable: NSViewRepresentable { + let surfaceView: SurfaceView + @Binding var isDragging: Bool + @Binding var isHovering: Bool + + func makeNSView(context: Context) -> SurfaceDragSourceView { + let view = SurfaceDragSourceView() + view.surfaceView = surfaceView + view.onDragStateChanged = { dragging in + isDragging = dragging + } + view.onHoverChanged = { hovering in + withAnimation(.easeInOut(duration: 0.15)) { + isHovering = hovering + } + } + return view + } + + func updateNSView(_ nsView: SurfaceDragSourceView, context: Context) { + nsView.surfaceView = surfaceView + nsView.onDragStateChanged = { dragging in + isDragging = dragging + } + nsView.onHoverChanged = { hovering in + withAnimation(.easeInOut(duration: 0.15)) { + isHovering = hovering + } + } + } + } + + /// The underlying NSView that handles drag operations. + /// + /// This view manages mouse tracking and drag initiation for surface reordering. + /// It uses a local event loop to detect drag gestures and initiates an + /// `NSDraggingSession` when the user drags beyond the threshold distance. + fileprivate class SurfaceDragSourceView: NSView, NSDraggingSource { + /// Scale factor applied to the surface snapshot for the drag preview image. + private static let previewScale: CGFloat = 0.2 + + /// The surface view that will be dragged. Its UUID is encoded into the + /// pasteboard for drop targets to identify which surface is being moved. + var surfaceView: SurfaceView? + + /// Callback invoked when the drag state changes. Called with `true` when + /// a drag session begins, and `false` when it ends (completed or cancelled). + var onDragStateChanged: ((Bool) -> Void)? + + /// Callback invoked when the mouse enters or exits this view's bounds. + /// Used to update the hover state for visual feedback in the parent view. + var onHoverChanged: ((Bool) -> Void)? + + /// Whether we are currently in a mouse tracking loop (between mouseDown + /// and either mouseUp or drag initiation). Used to determine cursor state. + private var isTracking: Bool = false + + /// Local event monitor to detect escape key presses during drag. + private var escapeMonitor: Any? + + /// Whether the current drag was cancelled by pressing escape. + private var dragCancelledByEscape: Bool = false + + deinit { + if let escapeMonitor { + NSEvent.removeMonitor(escapeMonitor) + } + } + + override func acceptsFirstMouse(for event: NSEvent?) -> Bool { + // Ensure this view gets the mouse event before window dragging handlers + return true + } + + override func mouseDown(with event: NSEvent) { + // Consume the mouseDown event to prevent it from propagating to the + // window's drag handler. This fixes issue #10110 where grab handles + // would drag the window instead of initiating pane drags. + // Don't call super - the drag will be initiated in mouseDragged. + } + + override func updateTrackingAreas() { + super.updateTrackingAreas() + + // To update our tracking area we just recreate it all. + trackingAreas.forEach { removeTrackingArea($0) } + + // Add our tracking area for mouse events + addTrackingArea(NSTrackingArea( + rect: bounds, + options: [.mouseEnteredAndExited, .activeInActiveApp], + owner: self, + userInfo: nil + )) + } + + override func resetCursorRects() { + addCursorRect(bounds, cursor: isTracking ? .closedHand : .openHand) + } + + override func mouseEntered(with event: NSEvent) { + onHoverChanged?(true) + } + + override func mouseExited(with event: NSEvent) { + onHoverChanged?(false) + } + + override func mouseDragged(with event: NSEvent) { + guard !isTracking, let surfaceView = surfaceView else { return } + + // Create our dragging item from our transferable + guard let pasteboardItem = surfaceView.pasteboardItem() else { return } + let item = NSDraggingItem(pasteboardWriter: pasteboardItem) + + // Create a scaled preview image from the surface snapshot + if let snapshot = surfaceView.asImage { + let imageSize = NSSize( + width: snapshot.size.width * Self.previewScale, + height: snapshot.size.height * Self.previewScale + ) + let scaledImage = NSImage(size: imageSize) + scaledImage.lockFocus() + snapshot.draw( + in: NSRect(origin: .zero, size: imageSize), + from: NSRect(origin: .zero, size: snapshot.size), + operation: .copy, + fraction: 1.0 + ) + scaledImage.unlockFocus() + + // Position the drag image so the mouse is at the center of the image. + // I personally like the top middle or top left corner best but + // this matches macOS native tab dragging behavior (at least, as of + // macOS 26.2 on Dec 29, 2025). + let mouseLocation = convert(event.locationInWindow, from: nil) + let origin = NSPoint( + x: mouseLocation.x - imageSize.width / 2, + y: mouseLocation.y - imageSize.height / 2 + ) + item.setDraggingFrame( + NSRect(origin: origin, size: imageSize), + contents: scaledImage + ) + } + + onDragStateChanged?(true) + let session = beginDraggingSession(with: [item], event: event, source: self) + + // We need to disable this so that endedAt happens immediately for our + // drags outside of any targets. + session.animatesToStartingPositionsOnCancelOrFail = false + } + + // MARK: NSDraggingSource + + func draggingSession( + _ session: NSDraggingSession, + sourceOperationMaskFor context: NSDraggingContext + ) -> NSDragOperation { + return context == .withinApplication ? .move : [] + } + + func draggingSession( + _ session: NSDraggingSession, + willBeginAt screenPoint: NSPoint + ) { + isTracking = true + + // Reset our escape tracking + dragCancelledByEscape = false + escapeMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in + if event.keyCode == 53 { // Escape key + self?.dragCancelledByEscape = true + } + return event + } + } + + func draggingSession( + _ session: NSDraggingSession, + movedTo screenPoint: NSPoint + ) { + NSCursor.closedHand.set() + } + + func draggingSession( + _ session: NSDraggingSession, + endedAt screenPoint: NSPoint, + operation: NSDragOperation + ) { + if let escapeMonitor { + NSEvent.removeMonitor(escapeMonitor) + self.escapeMonitor = nil + } + + if operation == [] && !dragCancelledByEscape { + let endsInWindow = NSApplication.shared.windows.contains { window in + window.isVisible && window.frame.contains(screenPoint) + } + if !endsInWindow { + NotificationCenter.default.post( + name: .ghosttySurfaceDragEndedNoTarget, + object: surfaceView, + userInfo: [Foundation.Notification.Name.ghosttySurfaceDragEndedNoTargetPointKey: screenPoint] + ) + } + } + + isTracking = false + onDragStateChanged?(false) + } + } +} + +extension Notification.Name { + /// Posted when a surface drag session ends with no operation (the drag was + /// released outside a valid drop target) and was not cancelled by the user + /// pressing escape. The notification's object is the SurfaceView that was dragged. + static let ghosttySurfaceDragEndedNoTarget = Notification.Name("ghosttySurfaceDragEndedNoTarget") + + /// Key for the screen point where the drag ended in the userInfo dictionary. + static let ghosttySurfaceDragEndedNoTargetPointKey = "endedAtPoint" +} diff --git a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift new file mode 100644 index 000000000..f3ee80874 --- /dev/null +++ b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift @@ -0,0 +1,41 @@ +import AppKit +import SwiftUI + +extension Ghostty { + /// A grab handle overlay at the top of the surface for dragging the window. + /// Only appears when hovering in the top region of the surface. + struct SurfaceGrabHandle: View { + private let handleHeight: CGFloat = 10 + + let surfaceView: SurfaceView + + @State private var isHovering: Bool = false + @State private var isDragging: Bool = false + + var body: some View { + VStack(spacing: 0) { + Rectangle() + .fill(Color.primary.opacity(isHovering || isDragging ? 0.15 : 0)) + .frame(height: handleHeight) + .overlay(alignment: .center) { + if isHovering || isDragging { + Image(systemName: "ellipsis") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(.primary.opacity(0.5)) + } + } + .contentShape(Rectangle()) + .overlay { + SurfaceDragSource( + surfaceView: surfaceView, + isDragging: $isDragging, + isHovering: $isHovering + ) + } + + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } +} diff --git a/macos/Sources/Ghostty/SurfaceProgressBar.swift b/macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift similarity index 100% rename from macos/Sources/Ghostty/SurfaceProgressBar.swift rename to macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift diff --git a/macos/Sources/Ghostty/SurfaceScrollView.swift b/macos/Sources/Ghostty/Surface View/SurfaceScrollView.swift similarity index 86% rename from macos/Sources/Ghostty/SurfaceScrollView.swift rename to macos/Sources/Ghostty/Surface View/SurfaceScrollView.swift index 4e81eda14..b55f2e231 100644 --- a/macos/Sources/Ghostty/SurfaceScrollView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceScrollView.swift @@ -34,10 +34,15 @@ class SurfaceScrollView: NSView { scrollView.hasHorizontalScroller = false scrollView.autohidesScrollers = false scrollView.usesPredominantAxisScrolling = true + // Always use the overlay style. See mouseMoved for how we make + // it usable without a scroll wheel or gestures. + scrollView.scrollerStyle = .overlay // hide default background to show blur effect properly scrollView.drawsBackground = false - // don't let the content view clip it's subviews, to enable the + // don't let the content view clip its subviews, to enable the // surface to draw the background behind non-overlay scrollers + // (we currently only use overlay scrollers, but might as well + // configure the views correctly in case we change our mind) scrollView.contentView.clipsToBounds = false // The document view is what the scrollview is actually going @@ -107,24 +112,29 @@ class SurfaceScrollView: NSView { observers.append(NotificationCenter.default.addObserver( forName: NSScroller.preferredScrollerStyleDidChangeNotification, object: nil, - queue: .main - ) { [weak self] _ in - self?.handleScrollerStyleChange() - }) - - // Listen for frame change events. See the docstring for - // handleFrameChange for why this is necessary. - observers.append(NotificationCenter.default.addObserver( - forName: NSView.frameDidChangeNotification, - object: nil, // Since this observer is used to immediately override the event // that produced the notification, we let it run synchronously on // the posting thread. queue: nil - ) { [weak self] notification in - self?.handleFrameChange(notification) + ) { [weak self] _ in + self?.handleScrollerStyleChange() }) + // Listen for frame change events on macOS 26.0. See the docstring for + // handleFrameChangeForNSScrollPocket for why this is necessary. + if #unavailable(macOS 26.1) { if #available(macOS 26.0, *) { + observers.append(NotificationCenter.default.addObserver( + forName: NSView.frameDidChangeNotification, + object: nil, + // Since this observer is used to immediately override the event + // that produced the notification, we let it run synchronously on + // the posting thread. + queue: nil + ) { [weak self] notification in + self?.handleFrameChangeForNSScrollPocket(notification) + }) + }} + // Listen for derived config changes to update scrollbar settings live surfaceView.$derivedConfig .sink { [weak self] _ in @@ -176,10 +186,10 @@ class SurfaceScrollView: NSView { private func synchronizeAppearance() { let scrollbarConfig = surfaceView.derivedConfig.scrollbar scrollView.hasVerticalScroller = scrollbarConfig != .never - scrollView.verticalScroller?.controlSize = .small let hasLightBackground = OSColor(surfaceView.derivedConfig.backgroundColor).isLightColor // Make sure the scroller’s appearance matches the surface's background color. scrollView.appearance = NSAppearance(named: hasLightBackground ? .aqua : .darkAqua) + updateTrackingAreas() } /// Positions the surface view to fill the currently visible rectangle. @@ -240,6 +250,7 @@ class SurfaceScrollView: NSView { /// Handles scrollbar style changes private func handleScrollerStyleChange() { + scrollView.scrollerStyle = .overlay synchronizeCoreSurface() } @@ -319,7 +330,10 @@ class SurfaceScrollView: NSView { /// and reset their frame to zero. /// /// See also https://developer.apple.com/forums/thread/798392. - private func handleFrameChange(_ notification: Notification) { + /// + /// This bug is only present in macOS 26.0. + @available(macOS, introduced: 26.0, obsoleted: 26.1) + private func handleFrameChangeForNSScrollPocket(_ notification: Notification) { guard let window = window as? HiddenTitlebarTerminalWindow else { return } guard !window.styleMask.contains(.fullScreen) else { return } guard let view = notification.object as? NSView else { return } @@ -350,4 +364,32 @@ class SurfaceScrollView: NSView { } return contentHeight } + + // MARK: Mouse events + + override func mouseMoved(with: NSEvent) { + // When the OS preferred style is .legacy, the user should be able to + // click and drag the scroller without using scroll wheels or gestures, + // so we flash it when the mouse is moved over the scrollbar area. + guard NSScroller.preferredScrollerStyle == .legacy else { return } + scrollView.flashScrollers() + } + + override func updateTrackingAreas() { + // To update our tracking area we just recreate it all. + trackingAreas.forEach { removeTrackingArea($0) } + + super.updateTrackingAreas() + + // Our tracking area is the scroller frame + guard let scroller = scrollView.verticalScroller else { return } + addTrackingArea(NSTrackingArea( + rect: convert(scroller.bounds, from: scroller), + options: [ + .mouseMoved, + .activeInKeyWindow, + ], + owner: self, + userInfo: nil)) + } } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView+Image.swift b/macos/Sources/Ghostty/Surface View/SurfaceView+Image.swift new file mode 100644 index 000000000..11b7b4694 --- /dev/null +++ b/macos/Sources/Ghostty/Surface View/SurfaceView+Image.swift @@ -0,0 +1,28 @@ +#if canImport(AppKit) +import AppKit +#elseif canImport(UIKit) +import UIKit +#endif + +extension Ghostty.SurfaceView { + #if canImport(AppKit) + /// A snapshot image of the current surface view. + var asImage: NSImage? { + guard let bitmapRep = bitmapImageRepForCachingDisplay(in: bounds) else { + return nil + } + cacheDisplay(in: bounds, to: bitmapRep) + let image = NSImage(size: bounds.size) + image.addRepresentation(bitmapRep) + return image + } + #elseif canImport(UIKit) + /// A snapshot image of the current surface view. + var asImage: UIImage? { + let renderer = UIGraphicsImageRenderer(bounds: bounds) + return renderer.image { _ in + drawHierarchy(in: bounds, afterScreenUpdates: true) + } + } + #endif +} diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift new file mode 100644 index 000000000..509713309 --- /dev/null +++ b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift @@ -0,0 +1,58 @@ +#if canImport(AppKit) +import AppKit +#endif +import CoreTransferable +import UniformTypeIdentifiers + +/// Conformance to `Transferable` enables drag-and-drop. +extension Ghostty.SurfaceView: Transferable { + static var transferRepresentation: some TransferRepresentation { + DataRepresentation(contentType: .ghosttySurfaceId) { surface in + withUnsafeBytes(of: surface.id.uuid) { Data($0) } + } importing: { data in + guard data.count == 16 else { + throw TransferError.invalidData + } + + let uuid = data.withUnsafeBytes { + $0.load(as: UUID.self) + } + + guard let imported = await Self.find(uuid: uuid) else { + throw TransferError.invalidData + } + + return imported + } + } + + enum TransferError: Error { + case invalidData + } + + @MainActor + static func find(uuid: UUID) -> Self? { + #if canImport(AppKit) + guard let del = NSApp.delegate as? Ghostty.Delegate else { return nil } + return del.ghosttySurface(id: uuid) as? Self + #elseif canImport(UIKit) + // We should be able to use UIApplication here. + return nil + #else + return nil + #endif + } +} + +extension UTType { + /// A format that encodes the bare UUID only for the surface. This can be used if you have + /// a way to look up a surface by ID. + static let ghosttySurfaceId = UTType(exportedAs: "com.mitchellh.ghosttySurfaceId") +} + +#if canImport(AppKit) +extension NSPasteboard.PasteboardType { + /// Pasteboard type for dragging surface IDs. + static let ghosttySurfaceId = NSPasteboard.PasteboardType(UTType.ghosttySurfaceId.identifier) +} +#endif diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift similarity index 67% rename from macos/Sources/Ghostty/SurfaceView.swift rename to macos/Sources/Ghostty/Surface View/SurfaceView.swift index ba678db59..c5c2ee97c 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -49,7 +49,7 @@ extension Ghostty { // True if we're hovering over the left URL view, so we can show it on the right. @State private var isHoveringURLLeft: Bool = false - + #if canImport(AppKit) // Observe SecureInput to detect when its enabled @ObservedObject private var secureInput = SecureInput.shared @@ -116,31 +116,18 @@ extension Ghostty { } #if canImport(AppKit) - // If we are in the middle of a key sequence, then we show a visual element. We only - // support this on macOS currently although in theory we can support mobile with keyboards! - if !surfaceView.keySequence.isEmpty { - let padding: CGFloat = 5 - VStack { - Spacer() - - HStack { - Text(verbatim: "Pending Key Sequence:") - ForEach(0.. dragThreshold { + position = .bottom + } + dragOffset = .zero + } + } + ) + } + + @ViewBuilder + private var indicatorContent: some View { + HStack(alignment: .center, spacing: 8) { + // Key table indicator + if !keyTables.isEmpty { + HStack(spacing: 5) { + Image(systemName: "keyboard.badge.ellipsis") + .font(.system(size: 13)) + .foregroundStyle(.secondary) + + // Show table stack with arrows between them + ForEach(Array(keyTables.enumerated()), id: \.offset) { index, table in + if index > 0 { + Image(systemName: "chevron.right") + .font(.system(size: 10, weight: .semibold)) + .foregroundStyle(.tertiary) + } + Text(verbatim: table) + .font(.system(size: 13, weight: .medium, design: .rounded)) + } + } + } + + // Separator when both are active + if !keyTables.isEmpty && !keySequence.isEmpty { + Divider() + .frame(height: 14) + } + + // Key sequence indicator + if !keySequence.isEmpty { + HStack(alignment: .center, spacing: 4) { + ForEach(Array(keySequence.enumerated()), id: \.offset) { index, key in + KeyCap(key.description) + } + + // Animated ellipsis to indicate waiting for next key + PendingIndicator(paused: isDragging) + } + } + } + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background { + Capsule() + .fill(.regularMaterial) + .overlay { + Capsule() + .strokeBorder(Color.primary.opacity(0.15), lineWidth: 1) + } + .shadow(color: .black.opacity(0.2), radius: 8, y: 2) + } + .contentShape(Capsule()) + .backport.pointerStyle(.link) + .popover(isPresented: $isShowingPopover, arrowEdge: position.popoverEdge) { + VStack(alignment: .leading, spacing: 8) { + if !keyTables.isEmpty { + VStack(alignment: .leading, spacing: 4) { + Label("Key Table", systemImage: "keyboard.badge.ellipsis") + .font(.headline) + Text("A key table is a named set of keybindings, activated by some other key. Keys are interpreted using this table until it is deactivated.") + .font(.subheadline) + .foregroundStyle(.secondary) + } + } + + if !keyTables.isEmpty && !keySequence.isEmpty { + Divider() + } + + if !keySequence.isEmpty { + VStack(alignment: .leading, spacing: 4) { + Label("Key Sequence", systemImage: "character.cursor.ibeam") + .font(.headline) + Text("A key sequence is a series of key presses that trigger an action. A pending key sequence is currently active.") + .font(.subheadline) + .foregroundStyle(.secondary) + } + } + } + .padding() + .frame(maxWidth: 400) + .fixedSize(horizontal: false, vertical: true) + } + .onTapGesture { + isShowingPopover.toggle() + } + } + + /// A small keycap-style view for displaying keyboard shortcuts + struct KeyCap: View { + let text: String + + init(_ text: String) { + self.text = text + } + + var body: some View { + Text(verbatim: text) + .font(.system(size: 12, weight: .medium, design: .rounded)) + .padding(.horizontal, 5) + .padding(.vertical, 2) + .background( + RoundedRectangle(cornerRadius: 4) + .fill(Color(NSColor.controlBackgroundColor)) + .shadow(color: .black.opacity(0.12), radius: 0.5, y: 0.5) + ) + .overlay( + RoundedRectangle(cornerRadius: 4) + .strokeBorder(Color.primary.opacity(0.15), lineWidth: 0.5) + ) + } + } + + /// Animated dots to indicate waiting for the next key + struct PendingIndicator: View { + @State private var animationPhase: Double = 0 + let paused: Bool + + var body: some View { + TimelineView(.animation(paused: paused)) { context in + HStack(spacing: 2) { + ForEach(0..<3, id: \.self) { index in + Circle() + .fill(Color.secondary) + .frame(width: 4, height: 4) + .opacity(dotOpacity(for: index)) + } + } + .onChange(of: context.date.timeIntervalSinceReferenceDate) { newValue in + animationPhase = newValue + } + } + } + + private func dotOpacity(for index: Int) -> Double { + let phase = animationPhase + let offset = Double(index) / 3.0 + let wave = sin((phase + offset) * .pi * 2) + return 0.3 + 0.7 * ((wave + 1) / 2) + } + } + } +#endif + /// Visual overlay that shows a border around the edges when the bell rings with border feature enabled. struct BellBorderOverlay: View { let bell: Bool @@ -757,6 +994,152 @@ extension Ghostty { } } + /// Visual overlay that briefly highlights a surface to draw attention to it. + /// Uses a soft, soothing highlight with a pulsing border effect. + struct HighlightOverlay: View { + let highlighted: Bool + + @State private var borderPulse: Bool = false + + var body: some View { + ZStack { + Rectangle() + .fill( + RadialGradient( + gradient: Gradient(colors: [ + Color.accentColor.opacity(0.12), + Color.accentColor.opacity(0.03), + Color.clear + ]), + center: .center, + startRadius: 0, + endRadius: 2000 + ) + ) + + Rectangle() + .strokeBorder( + LinearGradient( + gradient: Gradient(colors: [ + Color.accentColor.opacity(0.8), + Color.accentColor.opacity(0.5), + Color.accentColor.opacity(0.8) + ]), + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: borderPulse ? 4 : 2 + ) + .shadow(color: Color.accentColor.opacity(borderPulse ? 0.8 : 0.6), radius: borderPulse ? 12 : 8, x: 0, y: 0) + .shadow(color: Color.accentColor.opacity(borderPulse ? 0.5 : 0.3), radius: borderPulse ? 24 : 16, x: 0, y: 0) + } + .allowsHitTesting(false) + .opacity(highlighted ? 1.0 : 0.0) + .animation(.easeOut(duration: 0.4), value: highlighted) + .onChange(of: highlighted) { newValue in + if newValue { + withAnimation(.easeInOut(duration: 0.4).repeatForever(autoreverses: true)) { + borderPulse = true + } + } else { + withAnimation(.easeOut(duration: 0.4)) { + borderPulse = false + } + } + } + } + } + + // MARK: Readonly Badge + + /// A badge overlay that indicates a surface is in readonly mode. + /// Positioned in the top-right corner and styled to be noticeable but unobtrusive. + struct ReadonlyBadge: View { + let onDisable: () -> Void + + @State private var showingPopover = false + + private let badgeColor = Color(hue: 0.08, saturation: 0.5, brightness: 0.8) + + var body: some View { + VStack { + HStack { + Spacer() + + HStack(spacing: 5) { + Image(systemName: "eye.fill") + .font(.system(size: 12)) + Text("Read-only") + .font(.system(size: 12, weight: .medium)) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(badgeBackground) + .foregroundStyle(badgeColor) + .onTapGesture { + showingPopover = true + } + .backport.pointerStyle(.link) + .popover(isPresented: $showingPopover, arrowEdge: .bottom) { + ReadonlyPopoverView(onDisable: onDisable, isPresented: $showingPopover) + } + } + .padding(8) + + Spacer() + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("Read-only terminal") + } + + private var badgeBackground: some View { + RoundedRectangle(cornerRadius: 6) + .fill(.regularMaterial) + .overlay( + RoundedRectangle(cornerRadius: 6) + .strokeBorder(Color.orange.opacity(0.6), lineWidth: 1.5) + ) + } + } + + struct ReadonlyPopoverView: View { + let onDisable: () -> Void + @Binding var isPresented: Bool + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 8) { + Image(systemName: "eye.fill") + .foregroundColor(.orange) + .font(.system(size: 13)) + Text("Read-Only Mode") + .font(.system(size: 13, weight: .semibold)) + } + + Text("This terminal is in read-only mode. You can still view, select, and scroll through the content, but no input events will be sent to the running application.") + .font(.system(size: 11)) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + + HStack { + Spacer() + + Button("Disable") { + onDisable() + isPresented = false + } + .keyboardShortcut(.defaultAction) + .buttonStyle(.borderedProminent) + .controlSize(.small) + } + } + .padding(16) + .frame(width: 280) + } + } + #if canImport(AppKit) /// When changing the split state, or going full screen (native or non), the terminal view /// will lose focus. There has to be some nice SwiftUI-native way to fix this but I can't diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift similarity index 93% rename from macos/Sources/Ghostty/SurfaceView_AppKit.swift rename to macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index 03ef293af..0ddfe57b8 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -9,7 +9,7 @@ extension Ghostty { /// The NSView implementation for a terminal surface. class SurfaceView: OSView, ObservableObject, Codable, Identifiable { typealias ID = UUID - + /// Unique ID per surface let id: UUID @@ -44,14 +44,14 @@ extension Ghostty { // The hovered URL string @Published var hoverUrl: String? = nil - + // The progress report (if any) @Published var progressReport: Action.ProgressReport? = nil { didSet { // Cancel any existing timer progressReportTimer?.invalidate() progressReportTimer = nil - + // If we have a new progress report, start a timer to remove it after 15 seconds if progressReport != nil { progressReportTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: false) { [weak self] _ in @@ -65,6 +65,9 @@ extension Ghostty { // The currently active key sequence. The sequence is not active if this is empty. @Published var keySequence: [KeyboardShortcut] = [] + // The currently active key tables. Empty if no tables are active. + @Published var keyTables: [String] = [] + // The current search state. When non-nil, the search overlay should be shown. @Published var searchState: SearchState? = nil { didSet { @@ -98,7 +101,7 @@ extension Ghostty { } } } - + // Cancellable for search state needle changes private var searchNeedleCancellable: AnyCancellable? @@ -123,6 +126,12 @@ extension Ghostty { /// True when the bell is active. This is set inactive on focus or event. @Published private(set) var bell: Bool = false + /// True when the surface is in readonly mode. + @Published private(set) var readonly: Bool = false + + /// True when the surface should show a highlight effect (e.g., when presented via goto_split). + @Published private(set) var highlighted: Bool = false + // An initial size to request for a window. This will only affect // then the view is moved to a new window. var initialSize: NSSize? = nil @@ -210,7 +219,7 @@ extension Ghostty { // A timer to fallback to ghost emoji if no title is set within the grace period private var titleFallbackTimer: Timer? - + // Timer to remove progress report after 15 seconds private var progressReportTimer: Timer? @@ -318,6 +327,11 @@ extension Ghostty { selector: #selector(ghosttyDidEndKeySequence), name: Ghostty.Notification.didEndKeySequence, object: self) + center.addObserver( + self, + selector: #selector(ghosttyDidChangeKeyTable), + name: Ghostty.Notification.didChangeKeyTable, + object: self) center.addObserver( self, selector: #selector(ghosttyConfigDidChange(_:)), @@ -333,6 +347,11 @@ extension Ghostty { selector: #selector(ghosttyBellDidRing(_:)), name: .ghosttyBellDidRing, object: self) + center.addObserver( + self, + selector: #selector(ghosttyDidChangeReadonly(_:)), + name: .ghosttyDidChangeReadonly, + object: self) center.addObserver( self, selector: #selector(windowDidChangeScreen), @@ -369,26 +388,6 @@ extension Ghostty { // Setup our tracking area so we get mouse moved events updateTrackingAreas() - // Observe our appearance so we can report the correct value to libghostty. - // This is the best way I know of to get appearance change notifications. - self.appearanceObserver = observe(\.effectiveAppearance, options: [.new, .initial]) { view, change in - guard let appearance = change.newValue else { return } - guard let surface = view.surface else { return } - let scheme: ghostty_color_scheme_e - switch (appearance.name) { - case .aqua, .vibrantLight: - scheme = GHOSTTY_COLOR_SCHEME_LIGHT - - case .darkAqua, .vibrantDark: - scheme = GHOSTTY_COLOR_SCHEME_DARK - - default: - return - } - - ghostty_surface_set_color_scheme(surface, scheme) - } - // The UTTypes that can be dragged onto this view. registerForDraggedTypes(Array(Self.dropTypes)) } @@ -419,7 +418,7 @@ extension Ghostty { // Remove any notifications associated with this surface let identifiers = Array(self.notificationIdentifiers) UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers) - + // Cancel progress report timer progressReportTimer?.invalidate() } @@ -556,16 +555,16 @@ extension Ghostty { // Add buttons alert.addButton(withTitle: "OK") alert.addButton(withTitle: "Cancel") - + // Make the text field the first responder so it gets focus alert.window.initialFirstResponder = textField - + let completionHandler: (NSApplication.ModalResponse) -> Void = { [weak self] response in guard let self else { return } - + // Check if the user clicked "OK" guard response == .alertFirstButtonReturn else { return } - + // Get the input text let newTitle = textField.stringValue if newTitle.isEmpty { @@ -689,6 +688,22 @@ extension Ghostty { } } + @objc private func ghosttyDidChangeKeyTable(notification: SwiftUI.Notification) { + guard let action = notification.userInfo?[Ghostty.Notification.KeyTableKey] as? Ghostty.Action.KeyTable else { return } + + DispatchQueue.main.async { [weak self] in + guard let self else { return } + switch action { + case .activate(let name): + self.keyTables.append(name) + case .deactivate: + _ = self.keyTables.popLast() + case .deactivateAll: + self.keyTables.removeAll() + } + } + } + @objc private func ghosttyConfigDidChange(_ notification: SwiftUI.Notification) { // Get our managed configuration object out guard let config = notification.userInfo?[ @@ -723,6 +738,11 @@ extension Ghostty { bell = true } + @objc private func ghosttyDidChangeReadonly(_ notification: SwiftUI.Notification) { + guard let value = notification.userInfo?[SwiftUI.Notification.Name.ReadonlyKey] as? Bool else { return } + readonly = value + } + @objc private func windowDidChangeScreen(notification: SwiftUI.Notification) { guard let window = self.window else { return } guard let object = notification.object as? NSWindow, window == object else { return } @@ -840,16 +860,16 @@ extension Ghostty { override func otherMouseDown(with event: NSEvent) { guard let surface = self.surface else { return } - guard event.buttonNumber == 2 else { return } let mods = Ghostty.ghosttyMods(event.modifierFlags) - ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_MIDDLE, mods) + let button = Ghostty.Input.MouseButton(fromNSEventButtonNumber: event.buttonNumber) + ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, button.cMouseButton, mods) } override func otherMouseUp(with event: NSEvent) { guard let surface = self.surface else { return } - guard event.buttonNumber == 2 else { return } let mods = Ghostty.ghosttyMods(event.modifierFlags) - ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_MIDDLE, mods) + let button = Ghostty.Input.MouseButton(fromNSEventButtonNumber: event.buttonNumber) + ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, button.cMouseButton, mods) } @@ -968,7 +988,7 @@ extension Ghostty { var x = event.scrollingDeltaX var y = event.scrollingDeltaY let precision = event.hasPreciseScrollingDeltas - + if precision { // We do a 2x speed multiplier. This is subjective, it "feels" better to me. x *= 2; @@ -1161,17 +1181,10 @@ extension Ghostty { /// Special case handling for some control keys override func performKeyEquivalent(with event: NSEvent) -> Bool { - switch (event.type) { - case .keyDown: - // Continue, we care about key down events - break - - default: - // Any other key event we don't care about. I don't think its even - // possible to receive any other event type. - return false - } - + // We only care about key down events. It might not even be possible + // to receive any other event type here. + guard event.type == .keyDown else { return false } + // Only process events if we're focused. Some key events like C-/ macOS // appears to send to the first view in the hierarchy rather than the // the first responder (I don't know why). This prevents us from handling it. @@ -1181,18 +1194,35 @@ extension Ghostty { if (!focused) { return false } - - // If this event as-is would result in a key binding then we send it. - if let surface { + + // Get information about if this is a binding. + let bindingFlags = surfaceModel.flatMap { surface in var ghosttyEvent = event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS) - let match = (event.characters ?? "").withCString { ptr in + return (event.characters ?? "").withCString { ptr in ghosttyEvent.text = ptr - return ghostty_surface_key_is_binding(surface, ghosttyEvent) + return surface.keyIsBinding(ghosttyEvent) } - if match { - self.keyDown(with: event) - return true + } + + // If this is a binding then we want to perform it. + if let bindingFlags { + // Attempt to trigger a menu item for this key binding. We only do this if: + // - We're not in a key sequence or table (those are separate bindings) + // - The binding is NOT `all` (menu uses FirstResponder chain) + // - The binding is NOT `performable` (menu will always consume) + // - The binding is `consumed` (unconsumed bindings should pass through + // to the terminal, so we must not intercept them for the menu) + if keySequence.isEmpty, + keyTables.isEmpty, + bindingFlags.isDisjoint(with: [.all, .performable]), + bindingFlags.contains(.consumed) { + if let menu = NSApp.mainMenu, menu.performKeyEquivalent(with: event) { + return true + } } + + self.keyDown(with: event) + return true } let equivalent: String @@ -1330,7 +1360,7 @@ extension Ghostty { var key_ev = event.ghosttyKeyEvent(action, translationMods: translationEvent?.modifierFlags) key_ev.composing = composing - + // For text, we only encode UTF8 if we don't have a single control // character. Control characters are encoded by Ghostty itself. // Without this, `ctrl+enter` does the wrong thing. @@ -1436,9 +1466,13 @@ extension Ghostty { item.setImageIfDesired(systemSymbolName: "arrow.trianglehead.2.clockwise") item = menu.addItem(withTitle: "Toggle Terminal Inspector", action: #selector(toggleTerminalInspector(_:)), keyEquivalent: "") item.setImageIfDesired(systemSymbolName: "scope") + item = menu.addItem(withTitle: "Terminal Read-only", action: #selector(toggleReadonly(_:)), keyEquivalent: "") + item.setImageIfDesired(systemSymbolName: "eye.fill") + item.state = readonly ? .on : .off menu.addItem(.separator()) - item = menu.addItem(withTitle: "Change Title...", action: #selector(changeTitle(_:)), keyEquivalent: "") + item = menu.addItem(withTitle: "Change Tab Title...", action: #selector(BaseTerminalController.changeTabTitle(_:)), keyEquivalent: "") item.setImageIfDesired(systemSymbolName: "pencil.line") + item = menu.addItem(withTitle: "Change Terminal Title...", action: #selector(changeTitle(_:)), keyEquivalent: "") return menu } @@ -1485,7 +1519,7 @@ extension Ghostty { AppDelegate.logger.warning("action failed action=\(action)") } } - + @IBAction func find(_ sender: Any?) { guard let surface = self.surface else { return } let action = "start_search" @@ -1493,7 +1527,23 @@ extension Ghostty { AppDelegate.logger.warning("action failed action=\(action)") } } - + + @IBAction func selectionForFind(_ sender: Any?) { + guard let surface = self.surface else { return } + let action = "search_selection" + if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + AppDelegate.logger.warning("action failed action=\(action)") + } + } + + @IBAction func scrollToSelection(_ sender: Any?) { + guard let surface = self.surface else { return } + let action = "scroll_to_selection" + if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + AppDelegate.logger.warning("action failed action=\(action)") + } + } + @IBAction func findNext(_ sender: Any?) { guard let surface = self.surface else { return } let action = "search:next" @@ -1509,7 +1559,7 @@ extension Ghostty { AppDelegate.logger.warning("action failed action=\(action)") } } - + @IBAction func findHide(_ sender: Any?) { guard let surface = self.surface else { return } let action = "end_search" @@ -1518,6 +1568,22 @@ extension Ghostty { } } + @IBAction func toggleReadonly(_ sender: Any?) { + guard let surface = self.surface else { return } + let action = "toggle_readonly" + if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + AppDelegate.logger.warning("action failed action=\(action)") + } + } + + /// Triggers a brief highlight animation on this surface. + func highlight() { + highlighted = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in + self?.highlighted = false + } + } + @IBAction func splitRight(_ sender: Any) { guard let surface = self.surface else { return } ghostty_surface_split(surface, GHOSTTY_SPLIT_DIRECTION_RIGHT) @@ -1553,7 +1619,7 @@ extension Ghostty { AppDelegate.logger.warning("action failed action=\(action)") } } - + @IBAction func changeTitle(_ sender: Any) { promptTitle() } @@ -1614,6 +1680,7 @@ extension Ghostty { struct DerivedConfig { let backgroundColor: Color let backgroundOpacity: Double + let backgroundBlur: Ghostty.Config.BackgroundBlur let macosWindowShadow: Bool let windowTitleFontFamily: String? let windowAppearance: NSAppearance? @@ -1622,6 +1689,7 @@ extension Ghostty { init() { self.backgroundColor = Color(NSColor.windowBackgroundColor) self.backgroundOpacity = 1 + self.backgroundBlur = .disabled self.macosWindowShadow = true self.windowTitleFontFamily = nil self.windowAppearance = nil @@ -1631,6 +1699,7 @@ extension Ghostty { init(_ config: Ghostty.Config) { self.backgroundColor = config.backgroundColor self.backgroundOpacity = config.backgroundOpacity + self.backgroundBlur = config.backgroundBlur self.macosWindowShadow = config.macosWindowShadow self.windowTitleFontFamily = config.windowTitleFontFamily self.windowAppearance = .init(ghosttyConfig: config) @@ -1663,7 +1732,7 @@ extension Ghostty { let isUserSetTitle = try container.decodeIfPresent(Bool.self, forKey: .isUserSetTitle) ?? false self.init(app, baseConfig: config, uuid: uuid) - + // Restore the saved title after initialization if let title = savedTitle { self.title = title @@ -1880,6 +1949,17 @@ extension Ghostty.SurfaceView: NSTextInputClient { return } + guard let surfaceModel else { return } + // Process MacOS native scroll events + switch selector { + case #selector(moveToBeginningOfDocument(_:)): + _ = surfaceModel.perform(action: "scroll_to_top") + case #selector(moveToEndOfDocument(_:)): + _ = surfaceModel.perform(action: "scroll_to_bottom") + default: + break + } + print("SEL: \(selector)") } @@ -1920,14 +2000,14 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor { // The "COMBINATION" bit is key: we might get sent a string (we can handle that) // but get requested an image (we can't handle that at the time of writing this), // so we must bubble up. - + // Types we can receive let receivable: [NSPasteboard.PasteboardType] = [.string, .init("public.utf8-plain-text")] - + // Types that we can send. Currently the same as receivable but I'm separating // this out so we can modify this in the future. let sendable: [NSPasteboard.PasteboardType] = receivable - + // The sendable types that require a selection (currently all) let sendableRequiresSelection = sendable @@ -1944,7 +2024,7 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor { return super.validRequestor(forSendType: sendType, returnType: returnType) } } - + return self } @@ -1990,10 +2070,14 @@ extension Ghostty.SurfaceView: NSMenuItemValidation { let pb = NSPasteboard.ghosttySelection guard let str = pb.getOpinionatedStringContents() else { return false } return !str.isEmpty - + case #selector(findHide): return searchState != nil + case #selector(toggleReadonly): + item.state = readonly ? .on : .off + return true + default: return true } @@ -2091,7 +2175,7 @@ extension Ghostty.SurfaceView { override func accessibilitySelectedTextRange() -> NSRange { return selectedRange() } - + /// Returns the currently selected text as a string. /// This allows assistive technologies to read the selected content. override func accessibilitySelectedText() -> String? { @@ -2105,21 +2189,21 @@ extension Ghostty.SurfaceView { let str = String(cString: text.text) return str.isEmpty ? nil : str } - + /// Returns the number of characters in the terminal content. /// This helps assistive technologies understand the size of the content. override func accessibilityNumberOfCharacters() -> Int { let content = cachedScreenContents.get() return content.count } - + /// Returns the visible character range for the terminal. /// For terminals, we typically show all content as visible. override func accessibilityVisibleCharacterRange() -> NSRange { let content = cachedScreenContents.get() return NSRange(location: 0, length: content.count) } - + /// Returns the line number for a given character index. /// This helps assistive technologies navigate by line. override func accessibilityLine(for index: Int) -> Int { @@ -2127,7 +2211,7 @@ extension Ghostty.SurfaceView { let substring = String(content.prefix(index)) return substring.components(separatedBy: .newlines).count - 1 } - + /// Returns a substring for the given range. /// This allows assistive technologies to read specific portions of the content. override func accessibilityString(for range: NSRange) -> String? { @@ -2135,7 +2219,7 @@ extension Ghostty.SurfaceView { guard let swiftRange = Range(range, in: content) else { return nil } return String(content[swiftRange]) } - + /// Returns an attributed string for the given range. /// /// Note: right now this only applies font information. One day it'd be nice to extend @@ -2146,9 +2230,9 @@ extension Ghostty.SurfaceView { override func accessibilityAttributedString(for range: NSRange) -> NSAttributedString? { guard let surface = self.surface else { return nil } guard let plainString = accessibilityString(for: range) else { return nil } - + var attributes: [NSAttributedString.Key: Any] = [:] - + // Try to get the font from the surface if let fontRaw = ghostty_surface_quicklook_font(surface) { let font = Unmanaged.fromOpaque(fontRaw) @@ -2158,6 +2242,7 @@ extension Ghostty.SurfaceView { return NSAttributedString(string: plainString, attributes: attributes) } + } /// Caches a value for some period of time, evicting it automatically when that time expires. diff --git a/macos/Sources/Ghostty/SurfaceView_UIKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift similarity index 90% rename from macos/Sources/Ghostty/SurfaceView_UIKit.swift rename to macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift index 09c41c0b5..f9baf56c9 100644 --- a/macos/Sources/Ghostty/SurfaceView_UIKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift @@ -4,8 +4,10 @@ import GhosttyKit extension Ghostty { /// The UIView implementation for a terminal surface. class SurfaceView: UIView, ObservableObject { + typealias ID = UUID + /// Unique ID per surface - let uuid: UUID + let id: UUID // The current title of the surface as defined by the pty. This can be // changed with escape codes. This is public because the callbacks go @@ -44,6 +46,15 @@ extension Ghostty { // The current search state. When non-nil, the search overlay should be shown. @Published var searchState: SearchState? = nil + // The currently active key tables. Empty if no tables are active. + @Published var keyTables: [String] = [] + + /// True when the surface is in readonly mode. + @Published private(set) var readonly: Bool = false + + /// True when the surface should show a highlight effect (e.g., when presented via goto_split). + @Published private(set) var highlighted: Bool = false + // Returns sizing information for the surface. This is the raw C // structure because I'm lazy. var surfaceSize: ghostty_surface_size_s? { @@ -54,7 +65,7 @@ extension Ghostty { private(set) var surface: ghostty_surface_t? init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) { - self.uuid = uuid ?? .init() + self.id = uuid ?? .init() // Initialize with some default frame size. The important thing is that this // is non-zero so that our layer bounds are non-zero so that our renderer diff --git a/macos/Sources/Helpers/AnySortKey.swift b/macos/Sources/Helpers/AnySortKey.swift new file mode 100644 index 000000000..6813ccf45 --- /dev/null +++ b/macos/Sources/Helpers/AnySortKey.swift @@ -0,0 +1,25 @@ +import Foundation + +/// Type-erased wrapper for any Comparable type to use as a sort key. +struct AnySortKey: Comparable { + private let value: Any + private let comparator: (Any, Any) -> ComparisonResult + + init(_ value: T) { + self.value = value + self.comparator = { lhs, rhs in + guard let l = lhs as? T, let r = rhs as? T else { return .orderedSame } + if l < r { return .orderedAscending } + if l > r { return .orderedDescending } + return .orderedSame + } + } + + static func < (lhs: AnySortKey, rhs: AnySortKey) -> Bool { + lhs.comparator(lhs.value, rhs.value) == .orderedAscending + } + + static func == (lhs: AnySortKey, rhs: AnySortKey) -> Bool { + lhs.comparator(lhs.value, rhs.value) == .orderedSame + } +} diff --git a/macos/Sources/Helpers/DraggableWindowView.swift b/macos/Sources/Helpers/DraggableWindowView.swift deleted file mode 100644 index 8d88e2f66..000000000 --- a/macos/Sources/Helpers/DraggableWindowView.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Cocoa -import SwiftUI - -struct DraggableWindowView: NSViewRepresentable { - func makeNSView(context: Context) -> DraggableWindowNSView { - return DraggableWindowNSView() - } - - func updateNSView(_ nsView: DraggableWindowNSView, context: Context) { - // No need to update anything here - } -} - -class DraggableWindowNSView: NSView { - override func mouseDown(with event: NSEvent) { - guard let window = self.window else { return } - window.performDrag(with: event) - } -} diff --git a/macos/Sources/Helpers/Extensions/NSColor+Extension.swift b/macos/Sources/Helpers/Extensions/NSColor+Extension.swift new file mode 100644 index 000000000..63cf02ed4 --- /dev/null +++ b/macos/Sources/Helpers/Extensions/NSColor+Extension.swift @@ -0,0 +1,39 @@ +import AppKit + +extension NSColor { + /// Using a color list let's us get localized names. + private static let appleColorList: NSColorList? = NSColorList(named: "Apple") + + convenience init?(named name: String) { + guard let colorList = Self.appleColorList, + let color = colorList.color(withKey: name.capitalized) else { + return nil + } + guard let components = color.usingColorSpace(.sRGB) else { + return nil + } + self.init( + red: components.redComponent, + green: components.greenComponent, + blue: components.blueComponent, + alpha: components.alphaComponent + ) + } + + static var colorNames: [String] { + appleColorList?.allKeys.map { $0.lowercased() } ?? [] + } + + /// Calculates the perceptual distance to another color in RGB space. + func distance(to other: NSColor) -> Double { + guard let a = self.usingColorSpace(.sRGB), + let b = other.usingColorSpace(.sRGB) else { return .infinity } + + let dr = a.redComponent - b.redComponent + let dg = a.greenComponent - b.greenComponent + let db = a.blueComponent - b.blueComponent + + // Weighted Euclidean distance (human eye is more sensitive to green) + return sqrt(2 * dr * dr + 4 * dg * dg + 3 * db * db) + } +} diff --git a/macos/Sources/Helpers/Extensions/NSMenu+Extension.swift b/macos/Sources/Helpers/Extensions/NSMenu+Extension.swift new file mode 100644 index 000000000..82c0a3a41 --- /dev/null +++ b/macos/Sources/Helpers/Extensions/NSMenu+Extension.swift @@ -0,0 +1,42 @@ +import AppKit + +extension NSMenu { + /// Inserts a menu item after an existing item with the specified action selector. + /// + /// If an item with the same identifier already exists, it is removed first to avoid duplicates. + /// This is useful when menus are cached and reused across different targets. + /// + /// - Parameters: + /// - item: The menu item to insert. + /// - action: The action selector to search for. The new item will be inserted after the first + /// item with this action. + /// - Returns: The index where the item was inserted, or `nil` if the action was not found + /// and the item was not inserted. + @discardableResult + func insertItem(_ item: NSMenuItem, after action: Selector) -> UInt? { + if let identifier = item.identifier, + let existing = items.first(where: { $0.identifier == identifier }) { + removeItem(existing) + } + + guard let idx = items.firstIndex(where: { $0.action == action }) else { + return nil + } + + let insertionIndex = idx + 1 + insertItem(item, at: insertionIndex) + return UInt(insertionIndex) + } + + /// Removes all menu items whose identifier is in the given set. + /// + /// - Parameter identifiers: The set of identifiers to match for removal. + func removeItems(withIdentifiers identifiers: Set) { + for (index, item) in items.enumerated().reversed() { + guard let identifier = item.identifier else { continue } + if identifiers.contains(identifier) { + removeItem(at: index) + } + } + } +} diff --git a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift index d834f5e63..5d1831f26 100644 --- a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift @@ -10,25 +10,73 @@ extension NSWindow { return CGWindowID(windowNumber) } - /// True if this is the first window in the tab group. - var isFirstWindowInTabGroup: Bool { - guard let firstWindow = tabGroup?.windows.first else { return true } - return firstWindow === self - } - - /// Adjusts the window origin if necessary to ensure the window remains visible on screen. + /// Adjusts the window frame if necessary to ensure the window remains visible on screen. + /// This constrains both the size (to not exceed the screen) and the origin (to keep the window on screen). func constrainToScreen() { guard let screen = screen ?? NSScreen.main else { return } let visibleFrame = screen.visibleFrame var windowFrame = frame + windowFrame.size.width = min(windowFrame.size.width, visibleFrame.size.width) + windowFrame.size.height = min(windowFrame.size.height, visibleFrame.size.height) + windowFrame.origin.x = max(visibleFrame.minX, min(windowFrame.origin.x, visibleFrame.maxX - windowFrame.width)) windowFrame.origin.y = max(visibleFrame.minY, min(windowFrame.origin.y, visibleFrame.maxY - windowFrame.height)) - if windowFrame.origin != frame.origin { - setFrameOrigin(windowFrame.origin) + if windowFrame != frame { + setFrame(windowFrame, display: true) } } } + +// MARK: Native Tabbing + +extension NSWindow { + /// True if this is the first window in the tab group. + var isFirstWindowInTabGroup: Bool { + guard let firstWindow = tabGroup?.windows.first else { return true } + return firstWindow === self + } +} + +/// Native tabbing private API usage. :( +extension NSWindow { + var titlebarView: NSView? { + // In normal window, `NSTabBar` typically appears as a subview of `NSTitlebarView` within `NSThemeFrame`. + // In fullscreen, the system creates a dedicated fullscreen window and the view hierarchy changes; + // in that case, the `titlebarView` is only accessible via a reference on `NSThemeFrame`. + // ref: https://github.com/mozilla-firefox/firefox/blob/054e2b072785984455b3b59acad9444ba1eeffb4/widget/cocoa/nsCocoaWindow.mm#L7205 + guard let themeFrameView = contentView?.rootView else { return nil } + guard themeFrameView.responds(to: Selector(("titlebarView"))) else { return nil } + return themeFrameView.value(forKey: "titlebarView") as? NSView + } + + /// Returns the [private] NSTabBar view, if it exists. + var tabBarView: NSView? { + titlebarView?.firstDescendant(withClassName: "NSTabBar") + } + + /// Returns the index of the tab button at the given screen point, if any. + func tabIndex(atScreenPoint screenPoint: NSPoint) -> Int? { + guard let tabBarView else { return nil } + let locationInWindow = convertPoint(fromScreen: screenPoint) + let locationInTabBar = tabBarView.convert(locationInWindow, from: nil) + guard tabBarView.bounds.contains(locationInTabBar) else { return nil } + + // Find all tab buttons and sort by x position to get visual order. + // The view hierarchy order doesn't match the visual tab order. + let tabItemViews = tabBarView.descendants(withClassName: "NSTabButton") + .sorted { $0.frame.origin.x < $1.frame.origin.x } + + for (index, tabItemView) in tabItemViews.enumerated() { + let locationInTab = tabItemView.convert(locationInWindow, from: nil) + if tabItemView.bounds.contains(locationInTab) { + return index + } + } + + return nil + } +} diff --git a/macos/Sources/Helpers/Extensions/String+Extension.swift b/macos/Sources/Helpers/Extensions/String+Extension.swift index 0c1c4fe91..139a7892c 100644 --- a/macos/Sources/Helpers/Extensions/String+Extension.swift +++ b/macos/Sources/Helpers/Extensions/String+Extension.swift @@ -7,7 +7,7 @@ extension String { return self.prefix(maxLength) + trailing } - #if canImport(AppKit) +#if canImport(AppKit) func temporaryFile(_ filename: String = "temp") -> URL { let url = FileManager.default.temporaryDirectory .appendingPathComponent(filename) @@ -16,5 +16,14 @@ extension String { try? string.write(to: url, atomically: true, encoding: .utf8) return url } - #endif + + /// Returns the path with the home directory abbreviated as ~. + var abbreviatedPath: String { + let home = FileManager.default.homeDirectoryForCurrentUser.path + if hasPrefix(home) { + return "~" + dropFirst(home.count) + } + return self + } +#endif } diff --git a/macos/Sources/Helpers/Extensions/Transferable+Extension.swift b/macos/Sources/Helpers/Extensions/Transferable+Extension.swift new file mode 100644 index 000000000..3bcc9057f --- /dev/null +++ b/macos/Sources/Helpers/Extensions/Transferable+Extension.swift @@ -0,0 +1,58 @@ +import AppKit +import CoreTransferable +import UniformTypeIdentifiers + +extension Transferable { + /// Converts this Transferable to an NSPasteboardItem with lazy data loading. + /// Data is only fetched when the pasteboard consumer requests it. This allows + /// bridging a Transferable to NSDraggingSource. + func pasteboardItem() -> NSPasteboardItem? { + let itemProvider = NSItemProvider() + itemProvider.register(self) + + let types = itemProvider.registeredTypeIdentifiers.compactMap { UTType($0) } + guard !types.isEmpty else { return nil } + + let item = NSPasteboardItem() + let dataProvider = TransferableDataProvider(itemProvider: itemProvider) + let pasteboardTypes = types.map { NSPasteboard.PasteboardType($0.identifier) } + item.setDataProvider(dataProvider, forTypes: pasteboardTypes) + + return item + } +} + +private final class TransferableDataProvider: NSObject, NSPasteboardItemDataProvider { + private let itemProvider: NSItemProvider + + init(itemProvider: NSItemProvider) { + self.itemProvider = itemProvider + super.init() + } + + func pasteboard( + _ pasteboard: NSPasteboard?, + item: NSPasteboardItem, + provideDataForType type: NSPasteboard.PasteboardType + ) { + // NSPasteboardItemDataProvider requires synchronous data return, but + // NSItemProvider.loadDataRepresentation is async. We use a semaphore + // to block until the async load completes. This is safe because AppKit + // calls this method on a background thread during drag operations. + let semaphore = DispatchSemaphore(value: 0) + + var result: Data? + itemProvider.loadDataRepresentation(forTypeIdentifier: type.rawValue) { data, _ in + result = data + semaphore.signal() + } + + // Wait for the data to load + semaphore.wait() + + // Set it. I honestly don't know what happens here if this fails. + if let data = result { + item.setData(data, forType: type) + } + } +} diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index 78c967661..8ab476267 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -130,7 +130,7 @@ class NativeFullscreen: FullscreenBase, FullscreenStyle { class NonNativeFullscreen: FullscreenBase, FullscreenStyle { var fullscreenMode: FullscreenMode { .nonNative } - + // Non-native fullscreen never supports tabs because tabs require // the "titled" style and we don't have it for non-native fullscreen. var supportsTabs: Bool { false } @@ -223,7 +223,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // Being untitled let's our content take up the full frame. window.styleMask.remove(.titled) - // We dont' want the non-native fullscreen window to be resizable + // We don't want the non-native fullscreen window to be resizable // from the edges. window.styleMask.remove(.resizable) @@ -277,7 +277,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { if let window = window as? TerminalWindow, window.isTabBar(c) { continue } - + if window.titlebarAccessoryViewControllers.firstIndex(of: c) == nil { window.addTitlebarAccessoryViewController(c) } @@ -286,7 +286,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // Removing "titled" also clears our toolbar window.toolbar = savedState.toolbar window.toolbarStyle = savedState.toolbarStyle - + // If the window was previously in a tab group that isn't empty now, // we re-add it. We have to do this because our process of doing non-native // fullscreen removes the window from the tab group. @@ -412,7 +412,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { self.toolbar = window.toolbar self.toolbarStyle = window.toolbarStyle self.dock = window.screen?.hasDock ?? false - + self.titlebarAccessoryViewControllers = if (window.hasTitleBar) { // Accessing titlebarAccessoryViewControllers without a titlebar triggers a crash. window.titlebarAccessoryViewControllers diff --git a/macos/Tests/Helpers/TransferablePasteboardTests.swift b/macos/Tests/Helpers/TransferablePasteboardTests.swift new file mode 100644 index 000000000..055dd5785 --- /dev/null +++ b/macos/Tests/Helpers/TransferablePasteboardTests.swift @@ -0,0 +1,124 @@ +import Testing +import AppKit +import CoreTransferable +import UniformTypeIdentifiers +@testable import Ghostty + +struct TransferablePasteboardTests { + // MARK: - Test Helpers + + /// A simple Transferable type for testing pasteboard conversion. + private struct DummyTransferable: Transferable, Equatable { + let payload: String + + static var transferRepresentation: some TransferRepresentation { + DataRepresentation(contentType: .utf8PlainText) { value in + value.payload.data(using: .utf8)! + } importing: { data in + let string = String(data: data, encoding: .utf8)! + return DummyTransferable(payload: string) + } + } + } + + /// A Transferable type that registers multiple content types. + private struct MultiTypeTransferable: Transferable { + let text: String + + static var transferRepresentation: some TransferRepresentation { + DataRepresentation(contentType: .utf8PlainText) { value in + value.text.data(using: .utf8)! + } importing: { data in + MultiTypeTransferable(text: String(data: data, encoding: .utf8)!) + } + DataRepresentation(contentType: .plainText) { value in + value.text.data(using: .utf8)! + } importing: { data in + MultiTypeTransferable(text: String(data: data, encoding: .utf8)!) + } + } + } + + // MARK: - Basic Functionality + + @Test func pasteboardItemIsCreated() { + let transferable = DummyTransferable(payload: "hello") + let item = transferable.pasteboardItem() + #expect(item != nil) + } + + @Test func pasteboardItemContainsExpectedType() { + let transferable = DummyTransferable(payload: "hello") + guard let item = transferable.pasteboardItem() else { + Issue.record("Expected pasteboard item to be created") + return + } + + let expectedType = NSPasteboard.PasteboardType(UTType.utf8PlainText.identifier) + #expect(item.types.contains(expectedType)) + } + + @Test func pasteboardItemProvidesCorrectData() { + let transferable = DummyTransferable(payload: "test data") + guard let item = transferable.pasteboardItem() else { + Issue.record("Expected pasteboard item to be created") + return + } + + let pasteboardType = NSPasteboard.PasteboardType(UTType.utf8PlainText.identifier) + + // Write to a pasteboard to trigger data provider + let pasteboard = NSPasteboard(name: .init("test-\(UUID().uuidString)")) + pasteboard.clearContents() + pasteboard.writeObjects([item]) + + // Read back the data + guard let data = pasteboard.data(forType: pasteboardType) else { + Issue.record("Expected data to be available on pasteboard") + return + } + + let string = String(data: data, encoding: .utf8) + #expect(string == "test data") + } + + // MARK: - Multiple Content Types + + @Test func multipleTypesAreRegistered() { + let transferable = MultiTypeTransferable(text: "multi") + guard let item = transferable.pasteboardItem() else { + Issue.record("Expected pasteboard item to be created") + return + } + + let utf8Type = NSPasteboard.PasteboardType(UTType.utf8PlainText.identifier) + let plainType = NSPasteboard.PasteboardType(UTType.plainText.identifier) + + #expect(item.types.contains(utf8Type)) + #expect(item.types.contains(plainType)) + } + + @Test func multipleTypesProvideCorrectData() { + let transferable = MultiTypeTransferable(text: "shared content") + guard let item = transferable.pasteboardItem() else { + Issue.record("Expected pasteboard item to be created") + return + } + + let pasteboard = NSPasteboard(name: .init("test-\(UUID().uuidString)")) + pasteboard.clearContents() + pasteboard.writeObjects([item]) + + // Both types should provide the same content + let utf8Type = NSPasteboard.PasteboardType(UTType.utf8PlainText.identifier) + let plainType = NSPasteboard.PasteboardType(UTType.plainText.identifier) + + if let utf8Data = pasteboard.data(forType: utf8Type) { + #expect(String(data: utf8Data, encoding: .utf8) == "shared content") + } + + if let plainData = pasteboard.data(forType: plainType) { + #expect(String(data: plainData, encoding: .utf8) == "shared content") + } + } +} diff --git a/macos/Tests/Splits/TerminalSplitDropZoneTests.swift b/macos/Tests/Splits/TerminalSplitDropZoneTests.swift new file mode 100644 index 000000000..5c956fcc8 --- /dev/null +++ b/macos/Tests/Splits/TerminalSplitDropZoneTests.swift @@ -0,0 +1,128 @@ +import Testing +import Foundation +@testable import Ghostty + +struct TerminalSplitDropZoneTests { + private let standardSize = CGSize(width: 100, height: 100) + + // MARK: - Basic Edge Detection + + @Test func topEdge() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 50, y: 5), in: standardSize) + #expect(zone == .top) + } + + @Test func bottomEdge() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 50, y: 95), in: standardSize) + #expect(zone == .bottom) + } + + @Test func leftEdge() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 5, y: 50), in: standardSize) + #expect(zone == .left) + } + + @Test func rightEdge() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 95, y: 50), in: standardSize) + #expect(zone == .right) + } + + // MARK: - Corner Tie-Breaking + // When distances are equal, the check order determines the result: + // left -> right -> top -> bottom + + @Test func topLeftCornerSelectsLeft() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 0, y: 0), in: standardSize) + #expect(zone == .left) + } + + @Test func topRightCornerSelectsRight() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 100, y: 0), in: standardSize) + #expect(zone == .right) + } + + @Test func bottomLeftCornerSelectsLeft() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 0, y: 100), in: standardSize) + #expect(zone == .left) + } + + @Test func bottomRightCornerSelectsRight() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 100, y: 100), in: standardSize) + #expect(zone == .right) + } + + // MARK: - Center Point (All Distances Equal) + + @Test func centerSelectsLeft() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 50, y: 50), in: standardSize) + #expect(zone == .left) + } + + // MARK: - Non-Square Aspect Ratio + + @Test func rectangularViewTopEdge() { + let size = CGSize(width: 200, height: 100) + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 100, y: 10), in: size) + #expect(zone == .top) + } + + @Test func rectangularViewLeftEdge() { + let size = CGSize(width: 200, height: 100) + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 10, y: 50), in: size) + #expect(zone == .left) + } + + @Test func tallRectangleTopEdge() { + let size = CGSize(width: 100, height: 200) + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 50, y: 10), in: size) + #expect(zone == .top) + } + + // MARK: - Out-of-Bounds Points + + @Test func pointLeftOfViewSelectsLeft() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: -10, y: 50), in: standardSize) + #expect(zone == .left) + } + + @Test func pointAboveViewSelectsTop() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 50, y: -10), in: standardSize) + #expect(zone == .top) + } + + @Test func pointRightOfViewSelectsRight() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 110, y: 50), in: standardSize) + #expect(zone == .right) + } + + @Test func pointBelowViewSelectsBottom() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 50, y: 110), in: standardSize) + #expect(zone == .bottom) + } + + // MARK: - Diagonal Regions (Triangular Zones) + + @Test func upperLeftTriangleSelectsLeft() { + // Point in the upper-left triangle, closer to left than top + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 20, y: 30), in: standardSize) + #expect(zone == .left) + } + + @Test func upperRightTriangleSelectsRight() { + // Point in the upper-right triangle, closer to right than top + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 80, y: 30), in: standardSize) + #expect(zone == .right) + } + + @Test func lowerLeftTriangleSelectsLeft() { + // Point in the lower-left triangle, closer to left than bottom + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 20, y: 70), in: standardSize) + #expect(zone == .left) + } + + @Test func lowerRightTriangleSelectsRight() { + // Point in the lower-right triangle, closer to right than bottom + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 80, y: 70), in: standardSize) + #expect(zone == .right) + } +} diff --git a/nix/devShell.nix b/nix/devShell.nix index 4aaf4ef5c..90059a730 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -26,6 +26,7 @@ wasmtime, wraptest, zig, + zig_0_15, zip, llvmPackages_latest, bzip2, @@ -70,7 +71,6 @@ wayland-scanner, wayland-protocols, zon2nix, - system, pkgs, # needed by GTK for loading SVG icons while running from within the # developer shell @@ -100,7 +100,7 @@ in scdoc zig zip - zon2nix.packages.${system}.zon2nix + zon2nix.packages.${stdenv.hostPlatform.system}.zon2nix # For web and wasm stuff nodejs diff --git a/nix/package.nix b/nix/package.nix index 3d00648ec..b2decc7bc 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -20,16 +20,6 @@ wayland-scanner, pkgs, }: let - # The Zig hook has no way to select the release type without actual - # overriding of the default flags. - # - # TODO: Once - # https://github.com/ziglang/zig/issues/14281#issuecomment-1624220653 is - # ultimately acted on and has made its way to a nixpkgs implementation, this - # can probably be removed in favor of that. - zig_hook = zig_0_15.hook.overrideAttrs { - zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize} --color off"; - }; gi_typelib_path = import ./build-support/gi-typelib-path.nix { inherit pkgs lib stdenv; }; @@ -73,7 +63,7 @@ in ncurses pandoc pkg-config - zig_hook + zig_0_15 gobject-introspection wrapGAppsHook4 blueprint-compiler @@ -92,12 +82,16 @@ in GI_TYPELIB_PATH = gi_typelib_path; + dontSetZigDefaultFlags = true; + zigBuildFlags = [ "--system" "${finalAttrs.deps}" "-Dversion-string=${finalAttrs.version}-${revision}-nix" "-Dgtk-x11=${lib.boolToString enableX11}" "-Dgtk-wayland=${lib.boolToString enableWayland}" + "-Dcpu=baseline" + "-Doptimize=${optimize}" "-Dstrip=${lib.boolToString strip}" ]; diff --git a/nix/tests.nix b/nix/tests.nix new file mode 100644 index 000000000..3949877cf --- /dev/null +++ b/nix/tests.nix @@ -0,0 +1,283 @@ +{ + self, + system, + nixpkgs, + home-manager, + ... +}: let + nixos-version = nixpkgs.lib.trivial.release; + + pkgs = import nixpkgs { + inherit system; + overlays = [ + self.overlays.debug + ]; + }; + + pink_value = "#FF0087"; + + color_test = '' + import tempfile + import subprocess + + def check_for_pink(final=False) -> bool: + with tempfile.NamedTemporaryFile() as tmpin: + machine.send_monitor_command("screendump {}".format(tmpin.name)) + + cmd = 'convert {} -define histogram:unique-colors=true -format "%c" histogram:info:'.format( + tmpin.name + ) + ret = subprocess.run(cmd, shell=True, capture_output=True) + if ret.returncode != 0: + raise Exception( + "image analysis failed with exit code {}".format(ret.returncode) + ) + + text = ret.stdout.decode("utf-8") + return "${pink_value}" in text + ''; + + mkNodeGnome = { + config, + pkgs, + settings, + sshPort ? null, + ... + }: { + imports = [ + ./vm/wayland-gnome.nix + settings + ]; + + virtualisation = { + forwardPorts = pkgs.lib.optionals (sshPort != null) [ + { + from = "host"; + host.port = sshPort; + guest.port = 22; + } + ]; + + vmVariant = { + virtualisation.host.pkgs = pkgs; + }; + }; + + services.openssh = { + enable = true; + settings = { + PermitRootLogin = "yes"; + PermitEmptyPasswords = "yes"; + }; + }; + + security.pam.services.sshd.allowNullPassword = true; + + users.groups.ghostty = { + gid = 1000; + }; + + users.users.ghostty = { + uid = 1000; + }; + + home-manager = { + users = { + ghostty = { + home = { + username = config.users.users.ghostty.name; + homeDirectory = config.users.users.ghostty.home; + stateVersion = nixos-version; + }; + programs.ssh = { + enable = true; + extraOptionOverrides = { + StrictHostKeyChecking = "accept-new"; + UserKnownHostsFile = "/dev/null"; + }; + }; + }; + }; + }; + + system.stateVersion = nixos-version; + }; + + mkTestGnome = { + name, + settings, + testScript, + ocr ? false, + }: + pkgs.testers.runNixOSTest { + name = name; + + enableOCR = ocr; + + extraBaseModules = { + imports = [ + home-manager.nixosModules.home-manager + ]; + }; + + nodes = { + machine = { + config, + pkgs, + ... + }: + mkNodeGnome { + inherit config pkgs settings; + sshPort = 2222; + }; + }; + + testScript = testScript; + }; +in { + basic-version-check = pkgs.testers.runNixOSTest { + name = "basic-version-check"; + nodes = { + machine = {pkgs, ...}: { + users.groups.ghostty = {}; + users.users.ghostty = { + isNormalUser = true; + group = "ghostty"; + extraGroups = ["wheel"]; + hashedPassword = ""; + packages = [ + pkgs.ghostty + ]; + }; + }; + }; + testScript = {...}: '' + machine.succeed("su - ghostty -c 'ghostty +version'") + ''; + }; + + basic-window-check-gnome = mkTestGnome { + name = "basic-window-check-gnome"; + settings = { + home-manager.users.ghostty = { + xdg.configFile = { + "ghostty/config".text = '' + background = ${pink_value} + ''; + }; + }; + }; + ocr = true; + testScript = {nodes, ...}: let + user = nodes.machine.users.users.ghostty; + bus_path = "/run/user/${toString user.uid}/bus"; + bus = "DBUS_SESSION_BUS_ADDRESS=unix:path=${bus_path}"; + gdbus = "${bus} gdbus"; + ghostty = "${bus} ghostty"; + su = command: "su - ${user.name} -c '${command}'"; + gseval = "call --session -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval"; + wm_class = su "${gdbus} ${gseval} global.display.focus_window.wm_class"; + in '' + ${color_test} + + with subtest("wait for x"): + start_all() + machine.wait_for_x() + + machine.wait_for_file("${bus_path}") + + with subtest("Ensuring no pink is present without the terminal."): + assert ( + check_for_pink() == False + ), "Pink was present on the screen before we even launched a terminal!" + + machine.systemctl("enable app-com.mitchellh.ghostty-debug.service", user="${user.name}") + machine.succeed("${su "${ghostty} +new-window"}") + machine.wait_until_succeeds("${wm_class} | grep -q 'com.mitchellh.ghostty-debug'") + + machine.sleep(2) + + with subtest("Have the terminal display a color."): + assert( + check_for_pink() == True + ), "Pink was not found on the screen!" + + machine.systemctl("stop app-com.mitchellh.ghostty-debug.service", user="${user.name}") + ''; + }; + + ssh-integration-test = pkgs.testers.runNixOSTest { + name = "ssh-integration-test"; + extraBaseModules = { + imports = [ + home-manager.nixosModules.home-manager + ]; + }; + nodes = { + server = {...}: { + users.groups.ghostty = {}; + users.users.ghostty = { + isNormalUser = true; + group = "ghostty"; + extraGroups = ["wheel"]; + hashedPassword = ""; + packages = []; + }; + services.openssh = { + enable = true; + settings = { + PermitRootLogin = "yes"; + PermitEmptyPasswords = "yes"; + }; + }; + security.pam.services.sshd.allowNullPassword = true; + }; + client = { + config, + pkgs, + ... + }: + mkNodeGnome { + inherit config pkgs; + settings = { + home-manager.users.ghostty = { + xdg.configFile = { + "ghostty/config".text = let + in '' + shell-integration-features = ssh-terminfo + ''; + }; + }; + }; + sshPort = 2222; + }; + }; + testScript = {nodes, ...}: let + user = nodes.client.users.users.ghostty; + bus_path = "/run/user/${toString user.uid}/bus"; + bus = "DBUS_SESSION_BUS_ADDRESS=unix:path=${bus_path}"; + gdbus = "${bus} gdbus"; + ghostty = "${bus} ghostty"; + su = command: "su - ${user.name} -c '${command}'"; + gseval = "call --session -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval"; + wm_class = su "${gdbus} ${gseval} global.display.focus_window.wm_class"; + in '' + with subtest("Start server and wait for ssh to be ready."): + server.start() + server.wait_for_open_port(22) + + with subtest("Start client and wait for ghostty window."): + client.start() + client.wait_for_x() + client.wait_for_file("${bus_path}") + client.systemctl("enable app-com.mitchellh.ghostty-debug.service", user="${user.name}") + client.succeed("${su "${ghostty} +new-window"}") + client.wait_until_succeeds("${wm_class} | grep -q 'com.mitchellh.ghostty-debug'") + + with subtest("SSH from client to server and verify that the Ghostty terminfo is copied."): + client.sleep(2) + client.send_chars("ssh ghostty@server\n") + server.wait_for_file("${user.home}/.terminfo/x/xterm-ghostty", timeout=30) + ''; + }; +} diff --git a/nix/vm/common-gnome.nix b/nix/vm/common-gnome.nix index 0c2bef150..d8d484071 100644 --- a/nix/vm/common-gnome.nix +++ b/nix/vm/common-gnome.nix @@ -8,7 +8,7 @@ ./common.nix ]; - services.xserver = { + services = { displayManager = { gdm = { enable = true; @@ -22,6 +22,19 @@ }; }; + systemd.user.services = { + "org.gnome.Shell@wayland" = { + serviceConfig = { + ExecStart = [ + # Clear the list before overriding it. + "" + # Eval API is now internal so Shell needs to run in unsafe mode. + "${pkgs.gnome-shell}/bin/gnome-shell --unsafe-mode" + ]; + }; + }; + }; + environment.systemPackages = [ pkgs.gnomeExtensions.no-overview ]; diff --git a/nix/vm/common.nix b/nix/vm/common.nix index eefd7c1c0..b2fec28e8 100644 --- a/nix/vm/common.nix +++ b/nix/vm/common.nix @@ -4,9 +4,6 @@ documentation.nixos.enable = false; - networking.hostName = "ghostty"; - networking.domain = "mitchellh.com"; - virtualisation.vmVariant = { virtualisation.memorySize = 2048; }; @@ -28,17 +25,11 @@ users.groups.ghostty = {}; users.users.ghostty = { + isNormalUser = true; description = "Ghostty"; group = "ghostty"; extraGroups = ["wheel"]; - isNormalUser = true; - initialPassword = "ghostty"; - }; - - environment.etc = { - "xdg/autostart/com.mitchellh.ghostty.desktop" = { - source = "${pkgs.ghostty}/share/applications/com.mitchellh.ghostty.desktop"; - }; + hashedPassword = ""; }; environment.systemPackages = [ @@ -61,6 +52,7 @@ services.displayManager = { autoLogin = { + enable = true; user = "ghostty"; }; }; diff --git a/nix/vm/x11-gnome.nix b/nix/vm/x11-gnome.nix deleted file mode 100644 index 1994aea82..000000000 --- a/nix/vm/x11-gnome.nix +++ /dev/null @@ -1,9 +0,0 @@ -{...}: { - imports = [ - ./common-gnome.nix - ]; - - services.displayManager = { - defaultSession = "gnome-xorg"; - }; -} diff --git a/pkg/cimgui/build.zig b/pkg/cimgui/build.zig deleted file mode 100644 index b94f11943..000000000 --- a/pkg/cimgui/build.zig +++ /dev/null @@ -1,125 +0,0 @@ -const std = @import("std"); -const NativeTargetInfo = std.zig.system.NativeTargetInfo; - -pub fn build(b: *std.Build) !void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - const module = b.addModule("cimgui", .{ - .root_source_file = b.path("main.zig"), - .target = target, - .optimize = optimize, - }); - - const imgui_ = b.lazyDependency("imgui", .{}); - const lib = b.addLibrary(.{ - .name = "cimgui", - .root_module = b.createModule(.{ - .target = target, - .optimize = optimize, - }), - .linkage = .static, - }); - lib.linkLibC(); - lib.linkLibCpp(); - if (target.result.os.tag == .windows) { - lib.linkSystemLibrary("imm32"); - } - - // For dynamic linking, we prefer dynamic linking and to search by - // mode first. Mode first will search all paths for a dynamic library - // before falling back to static. - const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{ - .preferred_link_mode = .dynamic, - .search_strategy = .mode_first, - }; - - if (b.systemIntegrationOption("freetype", .{})) { - lib.linkSystemLibrary2("freetype2", dynamic_link_opts); - } else { - const freetype = b.dependency("freetype", .{ - .target = target, - .optimize = optimize, - .@"enable-libpng" = true, - }); - lib.linkLibrary(freetype.artifact("freetype")); - - if (freetype.builder.lazyDependency( - "freetype", - .{}, - )) |freetype_dep| { - module.addIncludePath(freetype_dep.path("include")); - } - } - - if (imgui_) |imgui| lib.addIncludePath(imgui.path("")); - module.addIncludePath(b.path("vendor")); - - var flags: std.ArrayList([]const u8) = .empty; - defer flags.deinit(b.allocator); - try flags.appendSlice(b.allocator, &.{ - "-DCIMGUI_FREETYPE=1", - "-DIMGUI_USE_WCHAR32=1", - "-DIMGUI_DISABLE_OBSOLETE_FUNCTIONS=1", - }); - if (target.result.os.tag == .windows) { - try flags.appendSlice(b.allocator, &.{ - "-DIMGUI_IMPL_API=extern\t\"C\"\t__declspec(dllexport)", - }); - } else { - try flags.appendSlice(b.allocator, &.{ - "-DIMGUI_IMPL_API=extern\t\"C\"", - }); - } - - if (imgui_) |imgui| { - lib.addCSourceFile(.{ .file = b.path("vendor/cimgui.cpp"), .flags = flags.items }); - lib.addCSourceFile(.{ .file = imgui.path("imgui.cpp"), .flags = flags.items }); - lib.addCSourceFile(.{ .file = imgui.path("imgui_draw.cpp"), .flags = flags.items }); - lib.addCSourceFile(.{ .file = imgui.path("imgui_demo.cpp"), .flags = flags.items }); - lib.addCSourceFile(.{ .file = imgui.path("imgui_widgets.cpp"), .flags = flags.items }); - lib.addCSourceFile(.{ .file = imgui.path("imgui_tables.cpp"), .flags = flags.items }); - lib.addCSourceFile(.{ .file = imgui.path("misc/freetype/imgui_freetype.cpp"), .flags = flags.items }); - lib.addCSourceFile(.{ - .file = imgui.path("backends/imgui_impl_opengl3.cpp"), - .flags = flags.items, - }); - - if (target.result.os.tag.isDarwin()) { - if (!target.query.isNative()) { - try @import("apple_sdk").addPaths(b, lib); - } - lib.addCSourceFile(.{ - .file = imgui.path("backends/imgui_impl_metal.mm"), - .flags = flags.items, - }); - if (target.result.os.tag == .macos) { - lib.addCSourceFile(.{ - .file = imgui.path("backends/imgui_impl_osx.mm"), - .flags = flags.items, - }); - } - } - } - - lib.installHeadersDirectory( - b.path("vendor"), - "", - .{ .include_extensions = &.{".h"} }, - ); - - b.installArtifact(lib); - - const test_exe = b.addTest(.{ - .name = "test", - .root_module = b.createModule(.{ - .root_source_file = b.path("main.zig"), - .target = target, - .optimize = optimize, - }), - }); - test_exe.linkLibrary(lib); - const tests_run = b.addRunArtifact(test_exe); - const test_step = b.step("test", "Run tests"); - test_step.dependOn(&tests_run.step); -} diff --git a/pkg/cimgui/build.zig.zon b/pkg/cimgui/build.zig.zon deleted file mode 100644 index f539a8fd6..000000000 --- a/pkg/cimgui/build.zig.zon +++ /dev/null @@ -1,19 +0,0 @@ -.{ - .name = .cimgui, - .version = "1.90.6", // -docking branch - .fingerprint = 0x49726f5f8acbc90d, - .paths = .{""}, - .dependencies = .{ - // This should be kept in sync with the submodule in the cimgui source - // code in ./vendor/ to be safe that they're compatible. - .imgui = .{ - // ocornut/imgui - .url = "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", - .hash = "N-V-__8AAH0GaQC8a52s6vfIxg88OZgFgEW6DFxfSK4lX_l3", - .lazy = true, - }, - - .apple_sdk = .{ .path = "../apple-sdk" }, - .freetype = .{ .path = "../freetype" }, - }, -} diff --git a/pkg/cimgui/c.zig b/pkg/cimgui/c.zig deleted file mode 100644 index f9b8ff920..000000000 --- a/pkg/cimgui/c.zig +++ /dev/null @@ -1,4 +0,0 @@ -pub const c = @cImport({ - @cDefine("CIMGUI_DEFINE_ENUMS_AND_STRUCTS", "1"); - @cInclude("cimgui.h"); -}); diff --git a/pkg/cimgui/main.zig b/pkg/cimgui/main.zig deleted file mode 100644 index b890a49ee..000000000 --- a/pkg/cimgui/main.zig +++ /dev/null @@ -1,20 +0,0 @@ -pub const c = @import("c.zig").c; - -// OpenGL -pub extern fn ImGui_ImplOpenGL3_Init(?[*:0]const u8) callconv(.c) bool; -pub extern fn ImGui_ImplOpenGL3_Shutdown() callconv(.c) void; -pub extern fn ImGui_ImplOpenGL3_NewFrame() callconv(.c) void; -pub extern fn ImGui_ImplOpenGL3_RenderDrawData(*c.ImDrawData) callconv(.c) void; - -// Metal -pub extern fn ImGui_ImplMetal_Init(*anyopaque) callconv(.c) bool; -pub extern fn ImGui_ImplMetal_Shutdown() callconv(.c) void; -pub extern fn ImGui_ImplMetal_NewFrame(*anyopaque) callconv(.c) void; -pub extern fn ImGui_ImplMetal_RenderDrawData(*c.ImDrawData, *anyopaque, *anyopaque) callconv(.c) void; - -// OSX -pub extern fn ImGui_ImplOSX_Init(*anyopaque) callconv(.c) bool; -pub extern fn ImGui_ImplOSX_Shutdown() callconv(.c) void; -pub extern fn ImGui_ImplOSX_NewFrame(*anyopaque) callconv(.c) void; - -test {} diff --git a/pkg/cimgui/vendor/cimgui.cpp b/pkg/cimgui/vendor/cimgui.cpp deleted file mode 100644 index 3b36df7d9..000000000 --- a/pkg/cimgui/vendor/cimgui.cpp +++ /dev/null @@ -1,5943 +0,0 @@ -// This file is automatically generated by generator.lua from -// https://github.com/cimgui/cimgui based on imgui.h file version "1.90.6" 19060 -// from Dear ImGui https://github.com/ocornut/imgui with imgui_internal.h api -// docking branch -#define IMGUI_ENABLE_FREETYPE -#ifdef IMGUI_ENABLE_FREETYPE -#ifndef CIMGUI_FREETYPE -#error "IMGUI_FREETYPE should be defined for Freetype linking" -#endif -#else -#ifdef CIMGUI_FREETYPE -#error "IMGUI_FREETYPE should not be defined without freetype generated cimgui" -#endif -#endif -#include "imgui.h" -#ifdef IMGUI_ENABLE_FREETYPE -#include "misc/freetype/imgui_freetype.h" -#endif -#include "imgui_internal.h" - -#include "cimgui.h" - -CIMGUI_API ImVec2* ImVec2_ImVec2_Nil(void) { - return IM_NEW(ImVec2)(); -} -CIMGUI_API void ImVec2_destroy(ImVec2* self) { - IM_DELETE(self); -} -CIMGUI_API ImVec2* ImVec2_ImVec2_Float(float _x, float _y) { - return IM_NEW(ImVec2)(_x, _y); -} -CIMGUI_API ImVec4* ImVec4_ImVec4_Nil(void) { - return IM_NEW(ImVec4)(); -} -CIMGUI_API void ImVec4_destroy(ImVec4* self) { - IM_DELETE(self); -} -CIMGUI_API ImVec4* ImVec4_ImVec4_Float(float _x, float _y, float _z, float _w) { - return IM_NEW(ImVec4)(_x, _y, _z, _w); -} -CIMGUI_API ImGuiContext* igCreateContext(ImFontAtlas* shared_font_atlas) { - return ImGui::CreateContext(shared_font_atlas); -} -CIMGUI_API void igDestroyContext(ImGuiContext* ctx) { - return ImGui::DestroyContext(ctx); -} -CIMGUI_API ImGuiContext* igGetCurrentContext() { - return ImGui::GetCurrentContext(); -} -CIMGUI_API void igSetCurrentContext(ImGuiContext* ctx) { - return ImGui::SetCurrentContext(ctx); -} -CIMGUI_API ImGuiIO* igGetIO() { - return &ImGui::GetIO(); -} -CIMGUI_API ImGuiStyle* igGetStyle() { - return &ImGui::GetStyle(); -} -CIMGUI_API void igNewFrame() { - return ImGui::NewFrame(); -} -CIMGUI_API void igEndFrame() { - return ImGui::EndFrame(); -} -CIMGUI_API void igRender() { - return ImGui::Render(); -} -CIMGUI_API ImDrawData* igGetDrawData() { - return ImGui::GetDrawData(); -} -CIMGUI_API void igShowDemoWindow(bool* p_open) { - return ImGui::ShowDemoWindow(p_open); -} -CIMGUI_API void igShowMetricsWindow(bool* p_open) { - return ImGui::ShowMetricsWindow(p_open); -} -CIMGUI_API void igShowDebugLogWindow(bool* p_open) { - return ImGui::ShowDebugLogWindow(p_open); -} -CIMGUI_API void igShowIDStackToolWindow(bool* p_open) { - return ImGui::ShowIDStackToolWindow(p_open); -} -CIMGUI_API void igShowAboutWindow(bool* p_open) { - return ImGui::ShowAboutWindow(p_open); -} -CIMGUI_API void igShowStyleEditor(ImGuiStyle* ref) { - return ImGui::ShowStyleEditor(ref); -} -CIMGUI_API bool igShowStyleSelector(const char* label) { - return ImGui::ShowStyleSelector(label); -} -CIMGUI_API void igShowFontSelector(const char* label) { - return ImGui::ShowFontSelector(label); -} -CIMGUI_API void igShowUserGuide() { - return ImGui::ShowUserGuide(); -} -CIMGUI_API const char* igGetVersion() { - return ImGui::GetVersion(); -} -CIMGUI_API void igStyleColorsDark(ImGuiStyle* dst) { - return ImGui::StyleColorsDark(dst); -} -CIMGUI_API void igStyleColorsLight(ImGuiStyle* dst) { - return ImGui::StyleColorsLight(dst); -} -CIMGUI_API void igStyleColorsClassic(ImGuiStyle* dst) { - return ImGui::StyleColorsClassic(dst); -} -CIMGUI_API bool igBegin(const char* name, - bool* p_open, - ImGuiWindowFlags flags) { - return ImGui::Begin(name, p_open, flags); -} -CIMGUI_API void igEnd() { - return ImGui::End(); -} -CIMGUI_API bool igBeginChild_Str(const char* str_id, - const ImVec2 size, - ImGuiChildFlags child_flags, - ImGuiWindowFlags window_flags) { - return ImGui::BeginChild(str_id, size, child_flags, window_flags); -} -CIMGUI_API bool igBeginChild_ID(ImGuiID id, - const ImVec2 size, - ImGuiChildFlags child_flags, - ImGuiWindowFlags window_flags) { - return ImGui::BeginChild(id, size, child_flags, window_flags); -} -CIMGUI_API void igEndChild() { - return ImGui::EndChild(); -} -CIMGUI_API bool igIsWindowAppearing() { - return ImGui::IsWindowAppearing(); -} -CIMGUI_API bool igIsWindowCollapsed() { - return ImGui::IsWindowCollapsed(); -} -CIMGUI_API bool igIsWindowFocused(ImGuiFocusedFlags flags) { - return ImGui::IsWindowFocused(flags); -} -CIMGUI_API bool igIsWindowHovered(ImGuiHoveredFlags flags) { - return ImGui::IsWindowHovered(flags); -} -CIMGUI_API ImDrawList* igGetWindowDrawList() { - return ImGui::GetWindowDrawList(); -} -CIMGUI_API float igGetWindowDpiScale() { - return ImGui::GetWindowDpiScale(); -} -CIMGUI_API void igGetWindowPos(ImVec2* pOut) { - *pOut = ImGui::GetWindowPos(); -} -CIMGUI_API void igGetWindowSize(ImVec2* pOut) { - *pOut = ImGui::GetWindowSize(); -} -CIMGUI_API float igGetWindowWidth() { - return ImGui::GetWindowWidth(); -} -CIMGUI_API float igGetWindowHeight() { - return ImGui::GetWindowHeight(); -} -CIMGUI_API ImGuiViewport* igGetWindowViewport() { - return ImGui::GetWindowViewport(); -} -CIMGUI_API void igSetNextWindowPos(const ImVec2 pos, - ImGuiCond cond, - const ImVec2 pivot) { - return ImGui::SetNextWindowPos(pos, cond, pivot); -} -CIMGUI_API void igSetNextWindowSize(const ImVec2 size, ImGuiCond cond) { - return ImGui::SetNextWindowSize(size, cond); -} -CIMGUI_API void igSetNextWindowSizeConstraints( - const ImVec2 size_min, - const ImVec2 size_max, - ImGuiSizeCallback custom_callback, - void* custom_callback_data) { - return ImGui::SetNextWindowSizeConstraints( - size_min, size_max, custom_callback, custom_callback_data); -} -CIMGUI_API void igSetNextWindowContentSize(const ImVec2 size) { - return ImGui::SetNextWindowContentSize(size); -} -CIMGUI_API void igSetNextWindowCollapsed(bool collapsed, ImGuiCond cond) { - return ImGui::SetNextWindowCollapsed(collapsed, cond); -} -CIMGUI_API void igSetNextWindowFocus() { - return ImGui::SetNextWindowFocus(); -} -CIMGUI_API void igSetNextWindowScroll(const ImVec2 scroll) { - return ImGui::SetNextWindowScroll(scroll); -} -CIMGUI_API void igSetNextWindowBgAlpha(float alpha) { - return ImGui::SetNextWindowBgAlpha(alpha); -} -CIMGUI_API void igSetNextWindowViewport(ImGuiID viewport_id) { - return ImGui::SetNextWindowViewport(viewport_id); -} -CIMGUI_API void igSetWindowPos_Vec2(const ImVec2 pos, ImGuiCond cond) { - return ImGui::SetWindowPos(pos, cond); -} -CIMGUI_API void igSetWindowSize_Vec2(const ImVec2 size, ImGuiCond cond) { - return ImGui::SetWindowSize(size, cond); -} -CIMGUI_API void igSetWindowCollapsed_Bool(bool collapsed, ImGuiCond cond) { - return ImGui::SetWindowCollapsed(collapsed, cond); -} -CIMGUI_API void igSetWindowFocus_Nil() { - return ImGui::SetWindowFocus(); -} -CIMGUI_API void igSetWindowFontScale(float scale) { - return ImGui::SetWindowFontScale(scale); -} -CIMGUI_API void igSetWindowPos_Str(const char* name, - const ImVec2 pos, - ImGuiCond cond) { - return ImGui::SetWindowPos(name, pos, cond); -} -CIMGUI_API void igSetWindowSize_Str(const char* name, - const ImVec2 size, - ImGuiCond cond) { - return ImGui::SetWindowSize(name, size, cond); -} -CIMGUI_API void igSetWindowCollapsed_Str(const char* name, - bool collapsed, - ImGuiCond cond) { - return ImGui::SetWindowCollapsed(name, collapsed, cond); -} -CIMGUI_API void igSetWindowFocus_Str(const char* name) { - return ImGui::SetWindowFocus(name); -} -CIMGUI_API void igGetContentRegionAvail(ImVec2* pOut) { - *pOut = ImGui::GetContentRegionAvail(); -} -CIMGUI_API void igGetContentRegionMax(ImVec2* pOut) { - *pOut = ImGui::GetContentRegionMax(); -} -CIMGUI_API void igGetWindowContentRegionMin(ImVec2* pOut) { - *pOut = ImGui::GetWindowContentRegionMin(); -} -CIMGUI_API void igGetWindowContentRegionMax(ImVec2* pOut) { - *pOut = ImGui::GetWindowContentRegionMax(); -} -CIMGUI_API float igGetScrollX() { - return ImGui::GetScrollX(); -} -CIMGUI_API float igGetScrollY() { - return ImGui::GetScrollY(); -} -CIMGUI_API void igSetScrollX_Float(float scroll_x) { - return ImGui::SetScrollX(scroll_x); -} -CIMGUI_API void igSetScrollY_Float(float scroll_y) { - return ImGui::SetScrollY(scroll_y); -} -CIMGUI_API float igGetScrollMaxX() { - return ImGui::GetScrollMaxX(); -} -CIMGUI_API float igGetScrollMaxY() { - return ImGui::GetScrollMaxY(); -} -CIMGUI_API void igSetScrollHereX(float center_x_ratio) { - return ImGui::SetScrollHereX(center_x_ratio); -} -CIMGUI_API void igSetScrollHereY(float center_y_ratio) { - return ImGui::SetScrollHereY(center_y_ratio); -} -CIMGUI_API void igSetScrollFromPosX_Float(float local_x, float center_x_ratio) { - return ImGui::SetScrollFromPosX(local_x, center_x_ratio); -} -CIMGUI_API void igSetScrollFromPosY_Float(float local_y, float center_y_ratio) { - return ImGui::SetScrollFromPosY(local_y, center_y_ratio); -} -CIMGUI_API void igPushFont(ImFont* font) { - return ImGui::PushFont(font); -} -CIMGUI_API void igPopFont() { - return ImGui::PopFont(); -} -CIMGUI_API void igPushStyleColor_U32(ImGuiCol idx, ImU32 col) { - return ImGui::PushStyleColor(idx, col); -} -CIMGUI_API void igPushStyleColor_Vec4(ImGuiCol idx, const ImVec4 col) { - return ImGui::PushStyleColor(idx, col); -} -CIMGUI_API void igPopStyleColor(int count) { - return ImGui::PopStyleColor(count); -} -CIMGUI_API void igPushStyleVar_Float(ImGuiStyleVar idx, float val) { - return ImGui::PushStyleVar(idx, val); -} -CIMGUI_API void igPushStyleVar_Vec2(ImGuiStyleVar idx, const ImVec2 val) { - return ImGui::PushStyleVar(idx, val); -} -CIMGUI_API void igPopStyleVar(int count) { - return ImGui::PopStyleVar(count); -} -CIMGUI_API void igPushTabStop(bool tab_stop) { - return ImGui::PushTabStop(tab_stop); -} -CIMGUI_API void igPopTabStop() { - return ImGui::PopTabStop(); -} -CIMGUI_API void igPushButtonRepeat(bool repeat) { - return ImGui::PushButtonRepeat(repeat); -} -CIMGUI_API void igPopButtonRepeat() { - return ImGui::PopButtonRepeat(); -} -CIMGUI_API void igPushItemWidth(float item_width) { - return ImGui::PushItemWidth(item_width); -} -CIMGUI_API void igPopItemWidth() { - return ImGui::PopItemWidth(); -} -CIMGUI_API void igSetNextItemWidth(float item_width) { - return ImGui::SetNextItemWidth(item_width); -} -CIMGUI_API float igCalcItemWidth() { - return ImGui::CalcItemWidth(); -} -CIMGUI_API void igPushTextWrapPos(float wrap_local_pos_x) { - return ImGui::PushTextWrapPos(wrap_local_pos_x); -} -CIMGUI_API void igPopTextWrapPos() { - return ImGui::PopTextWrapPos(); -} -CIMGUI_API ImFont* igGetFont() { - return ImGui::GetFont(); -} -CIMGUI_API float igGetFontSize() { - return ImGui::GetFontSize(); -} -CIMGUI_API void igGetFontTexUvWhitePixel(ImVec2* pOut) { - *pOut = ImGui::GetFontTexUvWhitePixel(); -} -CIMGUI_API ImU32 igGetColorU32_Col(ImGuiCol idx, float alpha_mul) { - return ImGui::GetColorU32(idx, alpha_mul); -} -CIMGUI_API ImU32 igGetColorU32_Vec4(const ImVec4 col) { - return ImGui::GetColorU32(col); -} -CIMGUI_API ImU32 igGetColorU32_U32(ImU32 col, float alpha_mul) { - return ImGui::GetColorU32(col, alpha_mul); -} -CIMGUI_API const ImVec4* igGetStyleColorVec4(ImGuiCol idx) { - return &ImGui::GetStyleColorVec4(idx); -} -CIMGUI_API void igGetCursorScreenPos(ImVec2* pOut) { - *pOut = ImGui::GetCursorScreenPos(); -} -CIMGUI_API void igSetCursorScreenPos(const ImVec2 pos) { - return ImGui::SetCursorScreenPos(pos); -} -CIMGUI_API void igGetCursorPos(ImVec2* pOut) { - *pOut = ImGui::GetCursorPos(); -} -CIMGUI_API float igGetCursorPosX() { - return ImGui::GetCursorPosX(); -} -CIMGUI_API float igGetCursorPosY() { - return ImGui::GetCursorPosY(); -} -CIMGUI_API void igSetCursorPos(const ImVec2 local_pos) { - return ImGui::SetCursorPos(local_pos); -} -CIMGUI_API void igSetCursorPosX(float local_x) { - return ImGui::SetCursorPosX(local_x); -} -CIMGUI_API void igSetCursorPosY(float local_y) { - return ImGui::SetCursorPosY(local_y); -} -CIMGUI_API void igGetCursorStartPos(ImVec2* pOut) { - *pOut = ImGui::GetCursorStartPos(); -} -CIMGUI_API void igSeparator() { - return ImGui::Separator(); -} -CIMGUI_API void igSameLine(float offset_from_start_x, float spacing) { - return ImGui::SameLine(offset_from_start_x, spacing); -} -CIMGUI_API void igNewLine() { - return ImGui::NewLine(); -} -CIMGUI_API void igSpacing() { - return ImGui::Spacing(); -} -CIMGUI_API void igDummy(const ImVec2 size) { - return ImGui::Dummy(size); -} -CIMGUI_API void igIndent(float indent_w) { - return ImGui::Indent(indent_w); -} -CIMGUI_API void igUnindent(float indent_w) { - return ImGui::Unindent(indent_w); -} -CIMGUI_API void igBeginGroup() { - return ImGui::BeginGroup(); -} -CIMGUI_API void igEndGroup() { - return ImGui::EndGroup(); -} -CIMGUI_API void igAlignTextToFramePadding() { - return ImGui::AlignTextToFramePadding(); -} -CIMGUI_API float igGetTextLineHeight() { - return ImGui::GetTextLineHeight(); -} -CIMGUI_API float igGetTextLineHeightWithSpacing() { - return ImGui::GetTextLineHeightWithSpacing(); -} -CIMGUI_API float igGetFrameHeight() { - return ImGui::GetFrameHeight(); -} -CIMGUI_API float igGetFrameHeightWithSpacing() { - return ImGui::GetFrameHeightWithSpacing(); -} -CIMGUI_API void igPushID_Str(const char* str_id) { - return ImGui::PushID(str_id); -} -CIMGUI_API void igPushID_StrStr(const char* str_id_begin, - const char* str_id_end) { - return ImGui::PushID(str_id_begin, str_id_end); -} -CIMGUI_API void igPushID_Ptr(const void* ptr_id) { - return ImGui::PushID(ptr_id); -} -CIMGUI_API void igPushID_Int(int int_id) { - return ImGui::PushID(int_id); -} -CIMGUI_API void igPopID() { - return ImGui::PopID(); -} -CIMGUI_API ImGuiID igGetID_Str(const char* str_id) { - return ImGui::GetID(str_id); -} -CIMGUI_API ImGuiID igGetID_StrStr(const char* str_id_begin, - const char* str_id_end) { - return ImGui::GetID(str_id_begin, str_id_end); -} -CIMGUI_API ImGuiID igGetID_Ptr(const void* ptr_id) { - return ImGui::GetID(ptr_id); -} -CIMGUI_API void igTextUnformatted(const char* text, const char* text_end) { - return ImGui::TextUnformatted(text, text_end); -} -CIMGUI_API void igText(const char* fmt, ...) { - va_list args; - va_start(args, fmt); - ImGui::TextV(fmt, args); - va_end(args); -} -CIMGUI_API void igTextV(const char* fmt, va_list args) { - return ImGui::TextV(fmt, args); -} -CIMGUI_API void igTextColored(const ImVec4 col, const char* fmt, ...) { - va_list args; - va_start(args, fmt); - ImGui::TextColoredV(col, fmt, args); - va_end(args); -} -CIMGUI_API void igTextColoredV(const ImVec4 col, - const char* fmt, - va_list args) { - return ImGui::TextColoredV(col, fmt, args); -} -CIMGUI_API void igTextDisabled(const char* fmt, ...) { - va_list args; - va_start(args, fmt); - ImGui::TextDisabledV(fmt, args); - va_end(args); -} -CIMGUI_API void igTextDisabledV(const char* fmt, va_list args) { - return ImGui::TextDisabledV(fmt, args); -} -CIMGUI_API void igTextWrapped(const char* fmt, ...) { - va_list args; - va_start(args, fmt); - ImGui::TextWrappedV(fmt, args); - va_end(args); -} -CIMGUI_API void igTextWrappedV(const char* fmt, va_list args) { - return ImGui::TextWrappedV(fmt, args); -} -CIMGUI_API void igLabelText(const char* label, const char* fmt, ...) { - va_list args; - va_start(args, fmt); - ImGui::LabelTextV(label, fmt, args); - va_end(args); -} -CIMGUI_API void igLabelTextV(const char* label, const char* fmt, va_list args) { - return ImGui::LabelTextV(label, fmt, args); -} -CIMGUI_API void igBulletText(const char* fmt, ...) { - va_list args; - va_start(args, fmt); - ImGui::BulletTextV(fmt, args); - va_end(args); -} -CIMGUI_API void igBulletTextV(const char* fmt, va_list args) { - return ImGui::BulletTextV(fmt, args); -} -CIMGUI_API void igSeparatorText(const char* label) { - return ImGui::SeparatorText(label); -} -CIMGUI_API bool igButton(const char* label, const ImVec2 size) { - return ImGui::Button(label, size); -} -CIMGUI_API bool igSmallButton(const char* label) { - return ImGui::SmallButton(label); -} -CIMGUI_API bool igInvisibleButton(const char* str_id, - const ImVec2 size, - ImGuiButtonFlags flags) { - return ImGui::InvisibleButton(str_id, size, flags); -} -CIMGUI_API bool igArrowButton(const char* str_id, ImGuiDir dir) { - return ImGui::ArrowButton(str_id, dir); -} -CIMGUI_API bool igCheckbox(const char* label, bool* v) { - return ImGui::Checkbox(label, v); -} -CIMGUI_API bool igCheckboxFlags_IntPtr(const char* label, - int* flags, - int flags_value) { - return ImGui::CheckboxFlags(label, flags, flags_value); -} -CIMGUI_API bool igCheckboxFlags_UintPtr(const char* label, - unsigned int* flags, - unsigned int flags_value) { - return ImGui::CheckboxFlags(label, flags, flags_value); -} -CIMGUI_API bool igRadioButton_Bool(const char* label, bool active) { - return ImGui::RadioButton(label, active); -} -CIMGUI_API bool igRadioButton_IntPtr(const char* label, int* v, int v_button) { - return ImGui::RadioButton(label, v, v_button); -} -CIMGUI_API void igProgressBar(float fraction, - const ImVec2 size_arg, - const char* overlay) { - return ImGui::ProgressBar(fraction, size_arg, overlay); -} -CIMGUI_API void igBullet() { - return ImGui::Bullet(); -} -CIMGUI_API void igImage(ImTextureID user_texture_id, - const ImVec2 image_size, - const ImVec2 uv0, - const ImVec2 uv1, - const ImVec4 tint_col, - const ImVec4 border_col) { - return ImGui::Image(user_texture_id, image_size, uv0, uv1, tint_col, - border_col); -} -CIMGUI_API bool igImageButton(const char* str_id, - ImTextureID user_texture_id, - const ImVec2 image_size, - const ImVec2 uv0, - const ImVec2 uv1, - const ImVec4 bg_col, - const ImVec4 tint_col) { - return ImGui::ImageButton(str_id, user_texture_id, image_size, uv0, uv1, - bg_col, tint_col); -} -CIMGUI_API bool igBeginCombo(const char* label, - const char* preview_value, - ImGuiComboFlags flags) { - return ImGui::BeginCombo(label, preview_value, flags); -} -CIMGUI_API void igEndCombo() { - return ImGui::EndCombo(); -} -CIMGUI_API bool igCombo_Str_arr(const char* label, - int* current_item, - const char* const items[], - int items_count, - int popup_max_height_in_items) { - return ImGui::Combo(label, current_item, items, items_count, - popup_max_height_in_items); -} -CIMGUI_API bool igCombo_Str(const char* label, - int* current_item, - const char* items_separated_by_zeros, - int popup_max_height_in_items) { - return ImGui::Combo(label, current_item, items_separated_by_zeros, - popup_max_height_in_items); -} -CIMGUI_API bool igCombo_FnStrPtr(const char* label, - int* current_item, - const char* (*getter)(void* user_data, - int idx), - void* user_data, - int items_count, - int popup_max_height_in_items) { - return ImGui::Combo(label, current_item, getter, user_data, items_count, - popup_max_height_in_items); -} -CIMGUI_API bool igDragFloat(const char* label, - float* v, - float v_speed, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragFloat(label, v, v_speed, v_min, v_max, format, flags); -} -CIMGUI_API bool igDragFloat2(const char* label, - float v[2], - float v_speed, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragFloat2(label, v, v_speed, v_min, v_max, format, flags); -} -CIMGUI_API bool igDragFloat3(const char* label, - float v[3], - float v_speed, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragFloat3(label, v, v_speed, v_min, v_max, format, flags); -} -CIMGUI_API bool igDragFloat4(const char* label, - float v[4], - float v_speed, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragFloat4(label, v, v_speed, v_min, v_max, format, flags); -} -CIMGUI_API bool igDragFloatRange2(const char* label, - float* v_current_min, - float* v_current_max, - float v_speed, - float v_min, - float v_max, - const char* format, - const char* format_max, - ImGuiSliderFlags flags) { - return ImGui::DragFloatRange2(label, v_current_min, v_current_max, v_speed, - v_min, v_max, format, format_max, flags); -} -CIMGUI_API bool igDragInt(const char* label, - int* v, - float v_speed, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragInt(label, v, v_speed, v_min, v_max, format, flags); -} -CIMGUI_API bool igDragInt2(const char* label, - int v[2], - float v_speed, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragInt2(label, v, v_speed, v_min, v_max, format, flags); -} -CIMGUI_API bool igDragInt3(const char* label, - int v[3], - float v_speed, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragInt3(label, v, v_speed, v_min, v_max, format, flags); -} -CIMGUI_API bool igDragInt4(const char* label, - int v[4], - float v_speed, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragInt4(label, v, v_speed, v_min, v_max, format, flags); -} -CIMGUI_API bool igDragIntRange2(const char* label, - int* v_current_min, - int* v_current_max, - float v_speed, - int v_min, - int v_max, - const char* format, - const char* format_max, - ImGuiSliderFlags flags) { - return ImGui::DragIntRange2(label, v_current_min, v_current_max, v_speed, - v_min, v_max, format, format_max, flags); -} -CIMGUI_API bool igDragScalar(const char* label, - ImGuiDataType data_type, - void* p_data, - float v_speed, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragScalar(label, data_type, p_data, v_speed, p_min, p_max, - format, flags); -} -CIMGUI_API bool igDragScalarN(const char* label, - ImGuiDataType data_type, - void* p_data, - int components, - float v_speed, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragScalarN(label, data_type, p_data, components, v_speed, - p_min, p_max, format, flags); -} -CIMGUI_API bool igSliderFloat(const char* label, - float* v, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderFloat(label, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igSliderFloat2(const char* label, - float v[2], - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderFloat2(label, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igSliderFloat3(const char* label, - float v[3], - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderFloat3(label, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igSliderFloat4(const char* label, - float v[4], - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderFloat4(label, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igSliderAngle(const char* label, - float* v_rad, - float v_degrees_min, - float v_degrees_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderAngle(label, v_rad, v_degrees_min, v_degrees_max, format, - flags); -} -CIMGUI_API bool igSliderInt(const char* label, - int* v, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderInt(label, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igSliderInt2(const char* label, - int v[2], - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderInt2(label, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igSliderInt3(const char* label, - int v[3], - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderInt3(label, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igSliderInt4(const char* label, - int v[4], - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderInt4(label, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igSliderScalar(const char* label, - ImGuiDataType data_type, - void* p_data, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderScalar(label, data_type, p_data, p_min, p_max, format, - flags); -} -CIMGUI_API bool igSliderScalarN(const char* label, - ImGuiDataType data_type, - void* p_data, - int components, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderScalarN(label, data_type, p_data, components, p_min, - p_max, format, flags); -} -CIMGUI_API bool igVSliderFloat(const char* label, - const ImVec2 size, - float* v, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::VSliderFloat(label, size, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igVSliderInt(const char* label, - const ImVec2 size, - int* v, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::VSliderInt(label, size, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igVSliderScalar(const char* label, - const ImVec2 size, - ImGuiDataType data_type, - void* p_data, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::VSliderScalar(label, size, data_type, p_data, p_min, p_max, - format, flags); -} -CIMGUI_API bool igInputText(const char* label, - char* buf, - size_t buf_size, - ImGuiInputTextFlags flags, - ImGuiInputTextCallback callback, - void* user_data) { - return ImGui::InputText(label, buf, buf_size, flags, callback, user_data); -} -CIMGUI_API bool igInputTextMultiline(const char* label, - char* buf, - size_t buf_size, - const ImVec2 size, - ImGuiInputTextFlags flags, - ImGuiInputTextCallback callback, - void* user_data) { - return ImGui::InputTextMultiline(label, buf, buf_size, size, flags, callback, - user_data); -} -CIMGUI_API bool igInputTextWithHint(const char* label, - const char* hint, - char* buf, - size_t buf_size, - ImGuiInputTextFlags flags, - ImGuiInputTextCallback callback, - void* user_data) { - return ImGui::InputTextWithHint(label, hint, buf, buf_size, flags, callback, - user_data); -} -CIMGUI_API bool igInputFloat(const char* label, - float* v, - float step, - float step_fast, - const char* format, - ImGuiInputTextFlags flags) { - return ImGui::InputFloat(label, v, step, step_fast, format, flags); -} -CIMGUI_API bool igInputFloat2(const char* label, - float v[2], - const char* format, - ImGuiInputTextFlags flags) { - return ImGui::InputFloat2(label, v, format, flags); -} -CIMGUI_API bool igInputFloat3(const char* label, - float v[3], - const char* format, - ImGuiInputTextFlags flags) { - return ImGui::InputFloat3(label, v, format, flags); -} -CIMGUI_API bool igInputFloat4(const char* label, - float v[4], - const char* format, - ImGuiInputTextFlags flags) { - return ImGui::InputFloat4(label, v, format, flags); -} -CIMGUI_API bool igInputInt(const char* label, - int* v, - int step, - int step_fast, - ImGuiInputTextFlags flags) { - return ImGui::InputInt(label, v, step, step_fast, flags); -} -CIMGUI_API bool igInputInt2(const char* label, - int v[2], - ImGuiInputTextFlags flags) { - return ImGui::InputInt2(label, v, flags); -} -CIMGUI_API bool igInputInt3(const char* label, - int v[3], - ImGuiInputTextFlags flags) { - return ImGui::InputInt3(label, v, flags); -} -CIMGUI_API bool igInputInt4(const char* label, - int v[4], - ImGuiInputTextFlags flags) { - return ImGui::InputInt4(label, v, flags); -} -CIMGUI_API bool igInputDouble(const char* label, - double* v, - double step, - double step_fast, - const char* format, - ImGuiInputTextFlags flags) { - return ImGui::InputDouble(label, v, step, step_fast, format, flags); -} -CIMGUI_API bool igInputScalar(const char* label, - ImGuiDataType data_type, - void* p_data, - const void* p_step, - const void* p_step_fast, - const char* format, - ImGuiInputTextFlags flags) { - return ImGui::InputScalar(label, data_type, p_data, p_step, p_step_fast, - format, flags); -} -CIMGUI_API bool igInputScalarN(const char* label, - ImGuiDataType data_type, - void* p_data, - int components, - const void* p_step, - const void* p_step_fast, - const char* format, - ImGuiInputTextFlags flags) { - return ImGui::InputScalarN(label, data_type, p_data, components, p_step, - p_step_fast, format, flags); -} -CIMGUI_API bool igColorEdit3(const char* label, - float col[3], - ImGuiColorEditFlags flags) { - return ImGui::ColorEdit3(label, col, flags); -} -CIMGUI_API bool igColorEdit4(const char* label, - float col[4], - ImGuiColorEditFlags flags) { - return ImGui::ColorEdit4(label, col, flags); -} -CIMGUI_API bool igColorPicker3(const char* label, - float col[3], - ImGuiColorEditFlags flags) { - return ImGui::ColorPicker3(label, col, flags); -} -CIMGUI_API bool igColorPicker4(const char* label, - float col[4], - ImGuiColorEditFlags flags, - const float* ref_col) { - return ImGui::ColorPicker4(label, col, flags, ref_col); -} -CIMGUI_API bool igColorButton(const char* desc_id, - const ImVec4 col, - ImGuiColorEditFlags flags, - const ImVec2 size) { - return ImGui::ColorButton(desc_id, col, flags, size); -} -CIMGUI_API void igSetColorEditOptions(ImGuiColorEditFlags flags) { - return ImGui::SetColorEditOptions(flags); -} -CIMGUI_API bool igTreeNode_Str(const char* label) { - return ImGui::TreeNode(label); -} -CIMGUI_API bool igTreeNode_StrStr(const char* str_id, const char* fmt, ...) { - va_list args; - va_start(args, fmt); - bool ret = ImGui::TreeNodeV(str_id, fmt, args); - va_end(args); - return ret; -} -CIMGUI_API bool igTreeNode_Ptr(const void* ptr_id, const char* fmt, ...) { - va_list args; - va_start(args, fmt); - bool ret = ImGui::TreeNodeV(ptr_id, fmt, args); - va_end(args); - return ret; -} -CIMGUI_API bool igTreeNodeV_Str(const char* str_id, - const char* fmt, - va_list args) { - return ImGui::TreeNodeV(str_id, fmt, args); -} -CIMGUI_API bool igTreeNodeV_Ptr(const void* ptr_id, - const char* fmt, - va_list args) { - return ImGui::TreeNodeV(ptr_id, fmt, args); -} -CIMGUI_API bool igTreeNodeEx_Str(const char* label, ImGuiTreeNodeFlags flags) { - return ImGui::TreeNodeEx(label, flags); -} -CIMGUI_API bool igTreeNodeEx_StrStr(const char* str_id, - ImGuiTreeNodeFlags flags, - const char* fmt, - ...) { - va_list args; - va_start(args, fmt); - bool ret = ImGui::TreeNodeExV(str_id, flags, fmt, args); - va_end(args); - return ret; -} -CIMGUI_API bool igTreeNodeEx_Ptr(const void* ptr_id, - ImGuiTreeNodeFlags flags, - const char* fmt, - ...) { - va_list args; - va_start(args, fmt); - bool ret = ImGui::TreeNodeExV(ptr_id, flags, fmt, args); - va_end(args); - return ret; -} -CIMGUI_API bool igTreeNodeExV_Str(const char* str_id, - ImGuiTreeNodeFlags flags, - const char* fmt, - va_list args) { - return ImGui::TreeNodeExV(str_id, flags, fmt, args); -} -CIMGUI_API bool igTreeNodeExV_Ptr(const void* ptr_id, - ImGuiTreeNodeFlags flags, - const char* fmt, - va_list args) { - return ImGui::TreeNodeExV(ptr_id, flags, fmt, args); -} -CIMGUI_API void igTreePush_Str(const char* str_id) { - return ImGui::TreePush(str_id); -} -CIMGUI_API void igTreePush_Ptr(const void* ptr_id) { - return ImGui::TreePush(ptr_id); -} -CIMGUI_API void igTreePop() { - return ImGui::TreePop(); -} -CIMGUI_API float igGetTreeNodeToLabelSpacing() { - return ImGui::GetTreeNodeToLabelSpacing(); -} -CIMGUI_API bool igCollapsingHeader_TreeNodeFlags(const char* label, - ImGuiTreeNodeFlags flags) { - return ImGui::CollapsingHeader(label, flags); -} -CIMGUI_API bool igCollapsingHeader_BoolPtr(const char* label, - bool* p_visible, - ImGuiTreeNodeFlags flags) { - return ImGui::CollapsingHeader(label, p_visible, flags); -} -CIMGUI_API void igSetNextItemOpen(bool is_open, ImGuiCond cond) { - return ImGui::SetNextItemOpen(is_open, cond); -} -CIMGUI_API bool igSelectable_Bool(const char* label, - bool selected, - ImGuiSelectableFlags flags, - const ImVec2 size) { - return ImGui::Selectable(label, selected, flags, size); -} -CIMGUI_API bool igSelectable_BoolPtr(const char* label, - bool* p_selected, - ImGuiSelectableFlags flags, - const ImVec2 size) { - return ImGui::Selectable(label, p_selected, flags, size); -} -CIMGUI_API bool igBeginListBox(const char* label, const ImVec2 size) { - return ImGui::BeginListBox(label, size); -} -CIMGUI_API void igEndListBox() { - return ImGui::EndListBox(); -} -CIMGUI_API bool igListBox_Str_arr(const char* label, - int* current_item, - const char* const items[], - int items_count, - int height_in_items) { - return ImGui::ListBox(label, current_item, items, items_count, - height_in_items); -} -CIMGUI_API bool igListBox_FnStrPtr(const char* label, - int* current_item, - const char* (*getter)(void* user_data, - int idx), - void* user_data, - int items_count, - int height_in_items) { - return ImGui::ListBox(label, current_item, getter, user_data, items_count, - height_in_items); -} -CIMGUI_API void igPlotLines_FloatPtr(const char* label, - const float* values, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - ImVec2 graph_size, - int stride) { - return ImGui::PlotLines(label, values, values_count, values_offset, - overlay_text, scale_min, scale_max, graph_size, - stride); -} -CIMGUI_API void igPlotLines_FnFloatPtr(const char* label, - float (*values_getter)(void* data, - int idx), - void* data, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - ImVec2 graph_size) { - return ImGui::PlotLines(label, values_getter, data, values_count, - values_offset, overlay_text, scale_min, scale_max, - graph_size); -} -CIMGUI_API void igPlotHistogram_FloatPtr(const char* label, - const float* values, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - ImVec2 graph_size, - int stride) { - return ImGui::PlotHistogram(label, values, values_count, values_offset, - overlay_text, scale_min, scale_max, graph_size, - stride); -} -CIMGUI_API void igPlotHistogram_FnFloatPtr(const char* label, - float (*values_getter)(void* data, - int idx), - void* data, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - ImVec2 graph_size) { - return ImGui::PlotHistogram(label, values_getter, data, values_count, - values_offset, overlay_text, scale_min, scale_max, - graph_size); -} -CIMGUI_API void igValue_Bool(const char* prefix, bool b) { - return ImGui::Value(prefix, b); -} -CIMGUI_API void igValue_Int(const char* prefix, int v) { - return ImGui::Value(prefix, v); -} -CIMGUI_API void igValue_Uint(const char* prefix, unsigned int v) { - return ImGui::Value(prefix, v); -} -CIMGUI_API void igValue_Float(const char* prefix, - float v, - const char* float_format) { - return ImGui::Value(prefix, v, float_format); -} -CIMGUI_API bool igBeginMenuBar() { - return ImGui::BeginMenuBar(); -} -CIMGUI_API void igEndMenuBar() { - return ImGui::EndMenuBar(); -} -CIMGUI_API bool igBeginMainMenuBar() { - return ImGui::BeginMainMenuBar(); -} -CIMGUI_API void igEndMainMenuBar() { - return ImGui::EndMainMenuBar(); -} -CIMGUI_API bool igBeginMenu(const char* label, bool enabled) { - return ImGui::BeginMenu(label, enabled); -} -CIMGUI_API void igEndMenu() { - return ImGui::EndMenu(); -} -CIMGUI_API bool igMenuItem_Bool(const char* label, - const char* shortcut, - bool selected, - bool enabled) { - return ImGui::MenuItem(label, shortcut, selected, enabled); -} -CIMGUI_API bool igMenuItem_BoolPtr(const char* label, - const char* shortcut, - bool* p_selected, - bool enabled) { - return ImGui::MenuItem(label, shortcut, p_selected, enabled); -} -CIMGUI_API bool igBeginTooltip() { - return ImGui::BeginTooltip(); -} -CIMGUI_API void igEndTooltip() { - return ImGui::EndTooltip(); -} -CIMGUI_API void igSetTooltip(const char* fmt, ...) { - va_list args; - va_start(args, fmt); - ImGui::SetTooltipV(fmt, args); - va_end(args); -} -CIMGUI_API void igSetTooltipV(const char* fmt, va_list args) { - return ImGui::SetTooltipV(fmt, args); -} -CIMGUI_API bool igBeginItemTooltip() { - return ImGui::BeginItemTooltip(); -} -CIMGUI_API void igSetItemTooltip(const char* fmt, ...) { - va_list args; - va_start(args, fmt); - ImGui::SetItemTooltipV(fmt, args); - va_end(args); -} -CIMGUI_API void igSetItemTooltipV(const char* fmt, va_list args) { - return ImGui::SetItemTooltipV(fmt, args); -} -CIMGUI_API bool igBeginPopup(const char* str_id, ImGuiWindowFlags flags) { - return ImGui::BeginPopup(str_id, flags); -} -CIMGUI_API bool igBeginPopupModal(const char* name, - bool* p_open, - ImGuiWindowFlags flags) { - return ImGui::BeginPopupModal(name, p_open, flags); -} -CIMGUI_API void igEndPopup() { - return ImGui::EndPopup(); -} -CIMGUI_API void igOpenPopup_Str(const char* str_id, - ImGuiPopupFlags popup_flags) { - return ImGui::OpenPopup(str_id, popup_flags); -} -CIMGUI_API void igOpenPopup_ID(ImGuiID id, ImGuiPopupFlags popup_flags) { - return ImGui::OpenPopup(id, popup_flags); -} -CIMGUI_API void igOpenPopupOnItemClick(const char* str_id, - ImGuiPopupFlags popup_flags) { - return ImGui::OpenPopupOnItemClick(str_id, popup_flags); -} -CIMGUI_API void igCloseCurrentPopup() { - return ImGui::CloseCurrentPopup(); -} -CIMGUI_API bool igBeginPopupContextItem(const char* str_id, - ImGuiPopupFlags popup_flags) { - return ImGui::BeginPopupContextItem(str_id, popup_flags); -} -CIMGUI_API bool igBeginPopupContextWindow(const char* str_id, - ImGuiPopupFlags popup_flags) { - return ImGui::BeginPopupContextWindow(str_id, popup_flags); -} -CIMGUI_API bool igBeginPopupContextVoid(const char* str_id, - ImGuiPopupFlags popup_flags) { - return ImGui::BeginPopupContextVoid(str_id, popup_flags); -} -CIMGUI_API bool igIsPopupOpen_Str(const char* str_id, ImGuiPopupFlags flags) { - return ImGui::IsPopupOpen(str_id, flags); -} -CIMGUI_API bool igBeginTable(const char* str_id, - int column, - ImGuiTableFlags flags, - const ImVec2 outer_size, - float inner_width) { - return ImGui::BeginTable(str_id, column, flags, outer_size, inner_width); -} -CIMGUI_API void igEndTable() { - return ImGui::EndTable(); -} -CIMGUI_API void igTableNextRow(ImGuiTableRowFlags row_flags, - float min_row_height) { - return ImGui::TableNextRow(row_flags, min_row_height); -} -CIMGUI_API bool igTableNextColumn() { - return ImGui::TableNextColumn(); -} -CIMGUI_API bool igTableSetColumnIndex(int column_n) { - return ImGui::TableSetColumnIndex(column_n); -} -CIMGUI_API void igTableSetupColumn(const char* label, - ImGuiTableColumnFlags flags, - float init_width_or_weight, - ImGuiID user_id) { - return ImGui::TableSetupColumn(label, flags, init_width_or_weight, user_id); -} -CIMGUI_API void igTableSetupScrollFreeze(int cols, int rows) { - return ImGui::TableSetupScrollFreeze(cols, rows); -} -CIMGUI_API void igTableHeader(const char* label) { - return ImGui::TableHeader(label); -} -CIMGUI_API void igTableHeadersRow() { - return ImGui::TableHeadersRow(); -} -CIMGUI_API void igTableAngledHeadersRow() { - return ImGui::TableAngledHeadersRow(); -} -CIMGUI_API ImGuiTableSortSpecs* igTableGetSortSpecs() { - return ImGui::TableGetSortSpecs(); -} -CIMGUI_API int igTableGetColumnCount() { - return ImGui::TableGetColumnCount(); -} -CIMGUI_API int igTableGetColumnIndex() { - return ImGui::TableGetColumnIndex(); -} -CIMGUI_API int igTableGetRowIndex() { - return ImGui::TableGetRowIndex(); -} -CIMGUI_API const char* igTableGetColumnName_Int(int column_n) { - return ImGui::TableGetColumnName(column_n); -} -CIMGUI_API ImGuiTableColumnFlags igTableGetColumnFlags(int column_n) { - return ImGui::TableGetColumnFlags(column_n); -} -CIMGUI_API void igTableSetColumnEnabled(int column_n, bool v) { - return ImGui::TableSetColumnEnabled(column_n, v); -} -CIMGUI_API void igTableSetBgColor(ImGuiTableBgTarget target, - ImU32 color, - int column_n) { - return ImGui::TableSetBgColor(target, color, column_n); -} -CIMGUI_API void igColumns(int count, const char* id, bool border) { - return ImGui::Columns(count, id, border); -} -CIMGUI_API void igNextColumn() { - return ImGui::NextColumn(); -} -CIMGUI_API int igGetColumnIndex() { - return ImGui::GetColumnIndex(); -} -CIMGUI_API float igGetColumnWidth(int column_index) { - return ImGui::GetColumnWidth(column_index); -} -CIMGUI_API void igSetColumnWidth(int column_index, float width) { - return ImGui::SetColumnWidth(column_index, width); -} -CIMGUI_API float igGetColumnOffset(int column_index) { - return ImGui::GetColumnOffset(column_index); -} -CIMGUI_API void igSetColumnOffset(int column_index, float offset_x) { - return ImGui::SetColumnOffset(column_index, offset_x); -} -CIMGUI_API int igGetColumnsCount() { - return ImGui::GetColumnsCount(); -} -CIMGUI_API bool igBeginTabBar(const char* str_id, ImGuiTabBarFlags flags) { - return ImGui::BeginTabBar(str_id, flags); -} -CIMGUI_API void igEndTabBar() { - return ImGui::EndTabBar(); -} -CIMGUI_API bool igBeginTabItem(const char* label, - bool* p_open, - ImGuiTabItemFlags flags) { - return ImGui::BeginTabItem(label, p_open, flags); -} -CIMGUI_API void igEndTabItem() { - return ImGui::EndTabItem(); -} -CIMGUI_API bool igTabItemButton(const char* label, ImGuiTabItemFlags flags) { - return ImGui::TabItemButton(label, flags); -} -CIMGUI_API void igSetTabItemClosed(const char* tab_or_docked_window_label) { - return ImGui::SetTabItemClosed(tab_or_docked_window_label); -} -CIMGUI_API ImGuiID igDockSpace(ImGuiID id, - const ImVec2 size, - ImGuiDockNodeFlags flags, - const ImGuiWindowClass* window_class) { - return ImGui::DockSpace(id, size, flags, window_class); -} -CIMGUI_API ImGuiID -igDockSpaceOverViewport(const ImGuiViewport* viewport, - ImGuiDockNodeFlags flags, - const ImGuiWindowClass* window_class) { - return ImGui::DockSpaceOverViewport(viewport, flags, window_class); -} -CIMGUI_API void igSetNextWindowDockID(ImGuiID dock_id, ImGuiCond cond) { - return ImGui::SetNextWindowDockID(dock_id, cond); -} -CIMGUI_API void igSetNextWindowClass(const ImGuiWindowClass* window_class) { - return ImGui::SetNextWindowClass(window_class); -} -CIMGUI_API ImGuiID igGetWindowDockID() { - return ImGui::GetWindowDockID(); -} -CIMGUI_API bool igIsWindowDocked() { - return ImGui::IsWindowDocked(); -} -CIMGUI_API void igLogToTTY(int auto_open_depth) { - return ImGui::LogToTTY(auto_open_depth); -} -CIMGUI_API void igLogToFile(int auto_open_depth, const char* filename) { - return ImGui::LogToFile(auto_open_depth, filename); -} -CIMGUI_API void igLogToClipboard(int auto_open_depth) { - return ImGui::LogToClipboard(auto_open_depth); -} -CIMGUI_API void igLogFinish() { - return ImGui::LogFinish(); -} -CIMGUI_API void igLogButtons() { - return ImGui::LogButtons(); -} -CIMGUI_API void igLogTextV(const char* fmt, va_list args) { - return ImGui::LogTextV(fmt, args); -} -CIMGUI_API bool igBeginDragDropSource(ImGuiDragDropFlags flags) { - return ImGui::BeginDragDropSource(flags); -} -CIMGUI_API bool igSetDragDropPayload(const char* type, - const void* data, - size_t sz, - ImGuiCond cond) { - return ImGui::SetDragDropPayload(type, data, sz, cond); -} -CIMGUI_API void igEndDragDropSource() { - return ImGui::EndDragDropSource(); -} -CIMGUI_API bool igBeginDragDropTarget() { - return ImGui::BeginDragDropTarget(); -} -CIMGUI_API const ImGuiPayload* igAcceptDragDropPayload( - const char* type, - ImGuiDragDropFlags flags) { - return ImGui::AcceptDragDropPayload(type, flags); -} -CIMGUI_API void igEndDragDropTarget() { - return ImGui::EndDragDropTarget(); -} -CIMGUI_API const ImGuiPayload* igGetDragDropPayload() { - return ImGui::GetDragDropPayload(); -} -CIMGUI_API void igBeginDisabled(bool disabled) { - return ImGui::BeginDisabled(disabled); -} -CIMGUI_API void igEndDisabled() { - return ImGui::EndDisabled(); -} -CIMGUI_API void igPushClipRect(const ImVec2 clip_rect_min, - const ImVec2 clip_rect_max, - bool intersect_with_current_clip_rect) { - return ImGui::PushClipRect(clip_rect_min, clip_rect_max, - intersect_with_current_clip_rect); -} -CIMGUI_API void igPopClipRect() { - return ImGui::PopClipRect(); -} -CIMGUI_API void igSetItemDefaultFocus() { - return ImGui::SetItemDefaultFocus(); -} -CIMGUI_API void igSetKeyboardFocusHere(int offset) { - return ImGui::SetKeyboardFocusHere(offset); -} -CIMGUI_API void igSetNextItemAllowOverlap() { - return ImGui::SetNextItemAllowOverlap(); -} -CIMGUI_API bool igIsItemHovered(ImGuiHoveredFlags flags) { - return ImGui::IsItemHovered(flags); -} -CIMGUI_API bool igIsItemActive() { - return ImGui::IsItemActive(); -} -CIMGUI_API bool igIsItemFocused() { - return ImGui::IsItemFocused(); -} -CIMGUI_API bool igIsItemClicked(ImGuiMouseButton mouse_button) { - return ImGui::IsItemClicked(mouse_button); -} -CIMGUI_API bool igIsItemVisible() { - return ImGui::IsItemVisible(); -} -CIMGUI_API bool igIsItemEdited() { - return ImGui::IsItemEdited(); -} -CIMGUI_API bool igIsItemActivated() { - return ImGui::IsItemActivated(); -} -CIMGUI_API bool igIsItemDeactivated() { - return ImGui::IsItemDeactivated(); -} -CIMGUI_API bool igIsItemDeactivatedAfterEdit() { - return ImGui::IsItemDeactivatedAfterEdit(); -} -CIMGUI_API bool igIsItemToggledOpen() { - return ImGui::IsItemToggledOpen(); -} -CIMGUI_API bool igIsAnyItemHovered() { - return ImGui::IsAnyItemHovered(); -} -CIMGUI_API bool igIsAnyItemActive() { - return ImGui::IsAnyItemActive(); -} -CIMGUI_API bool igIsAnyItemFocused() { - return ImGui::IsAnyItemFocused(); -} -CIMGUI_API ImGuiID igGetItemID() { - return ImGui::GetItemID(); -} -CIMGUI_API void igGetItemRectMin(ImVec2* pOut) { - *pOut = ImGui::GetItemRectMin(); -} -CIMGUI_API void igGetItemRectMax(ImVec2* pOut) { - *pOut = ImGui::GetItemRectMax(); -} -CIMGUI_API void igGetItemRectSize(ImVec2* pOut) { - *pOut = ImGui::GetItemRectSize(); -} -CIMGUI_API ImGuiViewport* igGetMainViewport() { - return ImGui::GetMainViewport(); -} -CIMGUI_API ImDrawList* igGetBackgroundDrawList_Nil() { - return ImGui::GetBackgroundDrawList(); -} -CIMGUI_API ImDrawList* igGetForegroundDrawList_Nil() { - return ImGui::GetForegroundDrawList(); -} -CIMGUI_API ImDrawList* igGetBackgroundDrawList_ViewportPtr( - ImGuiViewport* viewport) { - return ImGui::GetBackgroundDrawList(viewport); -} -CIMGUI_API ImDrawList* igGetForegroundDrawList_ViewportPtr( - ImGuiViewport* viewport) { - return ImGui::GetForegroundDrawList(viewport); -} -CIMGUI_API bool igIsRectVisible_Nil(const ImVec2 size) { - return ImGui::IsRectVisible(size); -} -CIMGUI_API bool igIsRectVisible_Vec2(const ImVec2 rect_min, - const ImVec2 rect_max) { - return ImGui::IsRectVisible(rect_min, rect_max); -} -CIMGUI_API double igGetTime() { - return ImGui::GetTime(); -} -CIMGUI_API int igGetFrameCount() { - return ImGui::GetFrameCount(); -} -CIMGUI_API ImDrawListSharedData* igGetDrawListSharedData() { - return ImGui::GetDrawListSharedData(); -} -CIMGUI_API const char* igGetStyleColorName(ImGuiCol idx) { - return ImGui::GetStyleColorName(idx); -} -CIMGUI_API void igSetStateStorage(ImGuiStorage* storage) { - return ImGui::SetStateStorage(storage); -} -CIMGUI_API ImGuiStorage* igGetStateStorage() { - return ImGui::GetStateStorage(); -} -CIMGUI_API void igCalcTextSize(ImVec2* pOut, - const char* text, - const char* text_end, - bool hide_text_after_double_hash, - float wrap_width) { - *pOut = ImGui::CalcTextSize(text, text_end, hide_text_after_double_hash, - wrap_width); -} -CIMGUI_API void igColorConvertU32ToFloat4(ImVec4* pOut, ImU32 in) { - *pOut = ImGui::ColorConvertU32ToFloat4(in); -} -CIMGUI_API ImU32 igColorConvertFloat4ToU32(const ImVec4 in) { - return ImGui::ColorConvertFloat4ToU32(in); -} -CIMGUI_API void igColorConvertRGBtoHSV(float r, - float g, - float b, - float* out_h, - float* out_s, - float* out_v) { - return ImGui::ColorConvertRGBtoHSV(r, g, b, *out_h, *out_s, *out_v); -} -CIMGUI_API void igColorConvertHSVtoRGB(float h, - float s, - float v, - float* out_r, - float* out_g, - float* out_b) { - return ImGui::ColorConvertHSVtoRGB(h, s, v, *out_r, *out_g, *out_b); -} -CIMGUI_API bool igIsKeyDown_Nil(ImGuiKey key) { - return ImGui::IsKeyDown(key); -} -CIMGUI_API bool igIsKeyPressed_Bool(ImGuiKey key, bool repeat) { - return ImGui::IsKeyPressed(key, repeat); -} -CIMGUI_API bool igIsKeyReleased_Nil(ImGuiKey key) { - return ImGui::IsKeyReleased(key); -} -CIMGUI_API bool igIsKeyChordPressed_Nil(ImGuiKeyChord key_chord) { - return ImGui::IsKeyChordPressed(key_chord); -} -CIMGUI_API int igGetKeyPressedAmount(ImGuiKey key, - float repeat_delay, - float rate) { - return ImGui::GetKeyPressedAmount(key, repeat_delay, rate); -} -CIMGUI_API const char* igGetKeyName(ImGuiKey key) { - return ImGui::GetKeyName(key); -} -CIMGUI_API void igSetNextFrameWantCaptureKeyboard(bool want_capture_keyboard) { - return ImGui::SetNextFrameWantCaptureKeyboard(want_capture_keyboard); -} -CIMGUI_API bool igIsMouseDown_Nil(ImGuiMouseButton button) { - return ImGui::IsMouseDown(button); -} -CIMGUI_API bool igIsMouseClicked_Bool(ImGuiMouseButton button, bool repeat) { - return ImGui::IsMouseClicked(button, repeat); -} -CIMGUI_API bool igIsMouseReleased_Nil(ImGuiMouseButton button) { - return ImGui::IsMouseReleased(button); -} -CIMGUI_API bool igIsMouseDoubleClicked_Nil(ImGuiMouseButton button) { - return ImGui::IsMouseDoubleClicked(button); -} -CIMGUI_API int igGetMouseClickedCount(ImGuiMouseButton button) { - return ImGui::GetMouseClickedCount(button); -} -CIMGUI_API bool igIsMouseHoveringRect(const ImVec2 r_min, - const ImVec2 r_max, - bool clip) { - return ImGui::IsMouseHoveringRect(r_min, r_max, clip); -} -CIMGUI_API bool igIsMousePosValid(const ImVec2* mouse_pos) { - return ImGui::IsMousePosValid(mouse_pos); -} -CIMGUI_API bool igIsAnyMouseDown() { - return ImGui::IsAnyMouseDown(); -} -CIMGUI_API void igGetMousePos(ImVec2* pOut) { - *pOut = ImGui::GetMousePos(); -} -CIMGUI_API void igGetMousePosOnOpeningCurrentPopup(ImVec2* pOut) { - *pOut = ImGui::GetMousePosOnOpeningCurrentPopup(); -} -CIMGUI_API bool igIsMouseDragging(ImGuiMouseButton button, - float lock_threshold) { - return ImGui::IsMouseDragging(button, lock_threshold); -} -CIMGUI_API void igGetMouseDragDelta(ImVec2* pOut, - ImGuiMouseButton button, - float lock_threshold) { - *pOut = ImGui::GetMouseDragDelta(button, lock_threshold); -} -CIMGUI_API void igResetMouseDragDelta(ImGuiMouseButton button) { - return ImGui::ResetMouseDragDelta(button); -} -CIMGUI_API ImGuiMouseCursor igGetMouseCursor() { - return ImGui::GetMouseCursor(); -} -CIMGUI_API void igSetMouseCursor(ImGuiMouseCursor cursor_type) { - return ImGui::SetMouseCursor(cursor_type); -} -CIMGUI_API void igSetNextFrameWantCaptureMouse(bool want_capture_mouse) { - return ImGui::SetNextFrameWantCaptureMouse(want_capture_mouse); -} -CIMGUI_API const char* igGetClipboardText() { - return ImGui::GetClipboardText(); -} -CIMGUI_API void igSetClipboardText(const char* text) { - return ImGui::SetClipboardText(text); -} -CIMGUI_API void igLoadIniSettingsFromDisk(const char* ini_filename) { - return ImGui::LoadIniSettingsFromDisk(ini_filename); -} -CIMGUI_API void igLoadIniSettingsFromMemory(const char* ini_data, - size_t ini_size) { - return ImGui::LoadIniSettingsFromMemory(ini_data, ini_size); -} -CIMGUI_API void igSaveIniSettingsToDisk(const char* ini_filename) { - return ImGui::SaveIniSettingsToDisk(ini_filename); -} -CIMGUI_API const char* igSaveIniSettingsToMemory(size_t* out_ini_size) { - return ImGui::SaveIniSettingsToMemory(out_ini_size); -} -CIMGUI_API void igDebugTextEncoding(const char* text) { - return ImGui::DebugTextEncoding(text); -} -CIMGUI_API void igDebugFlashStyleColor(ImGuiCol idx) { - return ImGui::DebugFlashStyleColor(idx); -} -CIMGUI_API void igDebugStartItemPicker() { - return ImGui::DebugStartItemPicker(); -} -CIMGUI_API bool igDebugCheckVersionAndDataLayout(const char* version_str, - size_t sz_io, - size_t sz_style, - size_t sz_vec2, - size_t sz_vec4, - size_t sz_drawvert, - size_t sz_drawidx) { - return ImGui::DebugCheckVersionAndDataLayout( - version_str, sz_io, sz_style, sz_vec2, sz_vec4, sz_drawvert, sz_drawidx); -} -CIMGUI_API void igSetAllocatorFunctions(ImGuiMemAllocFunc alloc_func, - ImGuiMemFreeFunc free_func, - void* user_data) { - return ImGui::SetAllocatorFunctions(alloc_func, free_func, user_data); -} -CIMGUI_API void igGetAllocatorFunctions(ImGuiMemAllocFunc* p_alloc_func, - ImGuiMemFreeFunc* p_free_func, - void** p_user_data) { - return ImGui::GetAllocatorFunctions(p_alloc_func, p_free_func, p_user_data); -} -CIMGUI_API void* igMemAlloc(size_t size) { - return ImGui::MemAlloc(size); -} -CIMGUI_API void igMemFree(void* ptr) { - return ImGui::MemFree(ptr); -} -CIMGUI_API ImGuiPlatformIO* igGetPlatformIO() { - return &ImGui::GetPlatformIO(); -} -CIMGUI_API void igUpdatePlatformWindows() { - return ImGui::UpdatePlatformWindows(); -} -CIMGUI_API void igRenderPlatformWindowsDefault(void* platform_render_arg, - void* renderer_render_arg) { - return ImGui::RenderPlatformWindowsDefault(platform_render_arg, - renderer_render_arg); -} -CIMGUI_API void igDestroyPlatformWindows() { - return ImGui::DestroyPlatformWindows(); -} -CIMGUI_API ImGuiViewport* igFindViewportByID(ImGuiID id) { - return ImGui::FindViewportByID(id); -} -CIMGUI_API ImGuiViewport* igFindViewportByPlatformHandle( - void* platform_handle) { - return ImGui::FindViewportByPlatformHandle(platform_handle); -} -CIMGUI_API ImGuiTableSortSpecs* ImGuiTableSortSpecs_ImGuiTableSortSpecs(void) { - return IM_NEW(ImGuiTableSortSpecs)(); -} -CIMGUI_API void ImGuiTableSortSpecs_destroy(ImGuiTableSortSpecs* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTableColumnSortSpecs* -ImGuiTableColumnSortSpecs_ImGuiTableColumnSortSpecs(void) { - return IM_NEW(ImGuiTableColumnSortSpecs)(); -} -CIMGUI_API void ImGuiTableColumnSortSpecs_destroy( - ImGuiTableColumnSortSpecs* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiStyle* ImGuiStyle_ImGuiStyle(void) { - return IM_NEW(ImGuiStyle)(); -} -CIMGUI_API void ImGuiStyle_destroy(ImGuiStyle* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiStyle_ScaleAllSizes(ImGuiStyle* self, float scale_factor) { - return self->ScaleAllSizes(scale_factor); -} -CIMGUI_API void ImGuiIO_AddKeyEvent(ImGuiIO* self, ImGuiKey key, bool down) { - return self->AddKeyEvent(key, down); -} -CIMGUI_API void ImGuiIO_AddKeyAnalogEvent(ImGuiIO* self, - ImGuiKey key, - bool down, - float v) { - return self->AddKeyAnalogEvent(key, down, v); -} -CIMGUI_API void ImGuiIO_AddMousePosEvent(ImGuiIO* self, float x, float y) { - return self->AddMousePosEvent(x, y); -} -CIMGUI_API void ImGuiIO_AddMouseButtonEvent(ImGuiIO* self, - int button, - bool down) { - return self->AddMouseButtonEvent(button, down); -} -CIMGUI_API void ImGuiIO_AddMouseWheelEvent(ImGuiIO* self, - float wheel_x, - float wheel_y) { - return self->AddMouseWheelEvent(wheel_x, wheel_y); -} -CIMGUI_API void ImGuiIO_AddMouseSourceEvent(ImGuiIO* self, - ImGuiMouseSource source) { - return self->AddMouseSourceEvent(source); -} -CIMGUI_API void ImGuiIO_AddMouseViewportEvent(ImGuiIO* self, ImGuiID id) { - return self->AddMouseViewportEvent(id); -} -CIMGUI_API void ImGuiIO_AddFocusEvent(ImGuiIO* self, bool focused) { - return self->AddFocusEvent(focused); -} -CIMGUI_API void ImGuiIO_AddInputCharacter(ImGuiIO* self, unsigned int c) { - return self->AddInputCharacter(c); -} -CIMGUI_API void ImGuiIO_AddInputCharacterUTF16(ImGuiIO* self, ImWchar16 c) { - return self->AddInputCharacterUTF16(c); -} -CIMGUI_API void ImGuiIO_AddInputCharactersUTF8(ImGuiIO* self, const char* str) { - return self->AddInputCharactersUTF8(str); -} -CIMGUI_API void ImGuiIO_SetKeyEventNativeData(ImGuiIO* self, - ImGuiKey key, - int native_keycode, - int native_scancode, - int native_legacy_index) { - return self->SetKeyEventNativeData(key, native_keycode, native_scancode, - native_legacy_index); -} -CIMGUI_API void ImGuiIO_SetAppAcceptingEvents(ImGuiIO* self, - bool accepting_events) { - return self->SetAppAcceptingEvents(accepting_events); -} -CIMGUI_API void ImGuiIO_ClearEventsQueue(ImGuiIO* self) { - return self->ClearEventsQueue(); -} -CIMGUI_API void ImGuiIO_ClearInputKeys(ImGuiIO* self) { - return self->ClearInputKeys(); -} -CIMGUI_API ImGuiIO* ImGuiIO_ImGuiIO(void) { - return IM_NEW(ImGuiIO)(); -} -CIMGUI_API void ImGuiIO_destroy(ImGuiIO* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiInputTextCallbackData* -ImGuiInputTextCallbackData_ImGuiInputTextCallbackData(void) { - return IM_NEW(ImGuiInputTextCallbackData)(); -} -CIMGUI_API void ImGuiInputTextCallbackData_destroy( - ImGuiInputTextCallbackData* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiInputTextCallbackData_DeleteChars( - ImGuiInputTextCallbackData* self, - int pos, - int bytes_count) { - return self->DeleteChars(pos, bytes_count); -} -CIMGUI_API void ImGuiInputTextCallbackData_InsertChars( - ImGuiInputTextCallbackData* self, - int pos, - const char* text, - const char* text_end) { - return self->InsertChars(pos, text, text_end); -} -CIMGUI_API void ImGuiInputTextCallbackData_SelectAll( - ImGuiInputTextCallbackData* self) { - return self->SelectAll(); -} -CIMGUI_API void ImGuiInputTextCallbackData_ClearSelection( - ImGuiInputTextCallbackData* self) { - return self->ClearSelection(); -} -CIMGUI_API bool ImGuiInputTextCallbackData_HasSelection( - ImGuiInputTextCallbackData* self) { - return self->HasSelection(); -} -CIMGUI_API ImGuiWindowClass* ImGuiWindowClass_ImGuiWindowClass(void) { - return IM_NEW(ImGuiWindowClass)(); -} -CIMGUI_API void ImGuiWindowClass_destroy(ImGuiWindowClass* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiPayload* ImGuiPayload_ImGuiPayload(void) { - return IM_NEW(ImGuiPayload)(); -} -CIMGUI_API void ImGuiPayload_destroy(ImGuiPayload* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiPayload_Clear(ImGuiPayload* self) { - return self->Clear(); -} -CIMGUI_API bool ImGuiPayload_IsDataType(ImGuiPayload* self, const char* type) { - return self->IsDataType(type); -} -CIMGUI_API bool ImGuiPayload_IsPreview(ImGuiPayload* self) { - return self->IsPreview(); -} -CIMGUI_API bool ImGuiPayload_IsDelivery(ImGuiPayload* self) { - return self->IsDelivery(); -} -CIMGUI_API ImGuiOnceUponAFrame* ImGuiOnceUponAFrame_ImGuiOnceUponAFrame(void) { - return IM_NEW(ImGuiOnceUponAFrame)(); -} -CIMGUI_API void ImGuiOnceUponAFrame_destroy(ImGuiOnceUponAFrame* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTextFilter* ImGuiTextFilter_ImGuiTextFilter( - const char* default_filter) { - return IM_NEW(ImGuiTextFilter)(default_filter); -} -CIMGUI_API void ImGuiTextFilter_destroy(ImGuiTextFilter* self) { - IM_DELETE(self); -} -CIMGUI_API bool ImGuiTextFilter_Draw(ImGuiTextFilter* self, - const char* label, - float width) { - return self->Draw(label, width); -} -CIMGUI_API bool ImGuiTextFilter_PassFilter(ImGuiTextFilter* self, - const char* text, - const char* text_end) { - return self->PassFilter(text, text_end); -} -CIMGUI_API void ImGuiTextFilter_Build(ImGuiTextFilter* self) { - return self->Build(); -} -CIMGUI_API void ImGuiTextFilter_Clear(ImGuiTextFilter* self) { - return self->Clear(); -} -CIMGUI_API bool ImGuiTextFilter_IsActive(ImGuiTextFilter* self) { - return self->IsActive(); -} -CIMGUI_API ImGuiTextRange* ImGuiTextRange_ImGuiTextRange_Nil(void) { - return IM_NEW(ImGuiTextRange)(); -} -CIMGUI_API void ImGuiTextRange_destroy(ImGuiTextRange* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTextRange* ImGuiTextRange_ImGuiTextRange_Str(const char* _b, - const char* _e) { - return IM_NEW(ImGuiTextRange)(_b, _e); -} -CIMGUI_API bool ImGuiTextRange_empty(ImGuiTextRange* self) { - return self->empty(); -} -CIMGUI_API void ImGuiTextRange_split(ImGuiTextRange* self, - char separator, - ImVector_ImGuiTextRange* out) { - return self->split(separator, out); -} -CIMGUI_API ImGuiTextBuffer* ImGuiTextBuffer_ImGuiTextBuffer(void) { - return IM_NEW(ImGuiTextBuffer)(); -} -CIMGUI_API void ImGuiTextBuffer_destroy(ImGuiTextBuffer* self) { - IM_DELETE(self); -} -CIMGUI_API const char* ImGuiTextBuffer_begin(ImGuiTextBuffer* self) { - return self->begin(); -} -CIMGUI_API const char* ImGuiTextBuffer_end(ImGuiTextBuffer* self) { - return self->end(); -} -CIMGUI_API int ImGuiTextBuffer_size(ImGuiTextBuffer* self) { - return self->size(); -} -CIMGUI_API bool ImGuiTextBuffer_empty(ImGuiTextBuffer* self) { - return self->empty(); -} -CIMGUI_API void ImGuiTextBuffer_clear(ImGuiTextBuffer* self) { - return self->clear(); -} -CIMGUI_API void ImGuiTextBuffer_reserve(ImGuiTextBuffer* self, int capacity) { - return self->reserve(capacity); -} -CIMGUI_API const char* ImGuiTextBuffer_c_str(ImGuiTextBuffer* self) { - return self->c_str(); -} -CIMGUI_API void ImGuiTextBuffer_append(ImGuiTextBuffer* self, - const char* str, - const char* str_end) { - return self->append(str, str_end); -} -CIMGUI_API void ImGuiTextBuffer_appendfv(ImGuiTextBuffer* self, - const char* fmt, - va_list args) { - return self->appendfv(fmt, args); -} -CIMGUI_API ImGuiStoragePair* ImGuiStoragePair_ImGuiStoragePair_Int(ImGuiID _key, - int _val) { - return IM_NEW(ImGuiStoragePair)(_key, _val); -} -CIMGUI_API void ImGuiStoragePair_destroy(ImGuiStoragePair* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiStoragePair* ImGuiStoragePair_ImGuiStoragePair_Float( - ImGuiID _key, - float _val) { - return IM_NEW(ImGuiStoragePair)(_key, _val); -} -CIMGUI_API ImGuiStoragePair* ImGuiStoragePair_ImGuiStoragePair_Ptr(ImGuiID _key, - void* _val) { - return IM_NEW(ImGuiStoragePair)(_key, _val); -} -CIMGUI_API void ImGuiStorage_Clear(ImGuiStorage* self) { - return self->Clear(); -} -CIMGUI_API int ImGuiStorage_GetInt(ImGuiStorage* self, - ImGuiID key, - int default_val) { - return self->GetInt(key, default_val); -} -CIMGUI_API void ImGuiStorage_SetInt(ImGuiStorage* self, ImGuiID key, int val) { - return self->SetInt(key, val); -} -CIMGUI_API bool ImGuiStorage_GetBool(ImGuiStorage* self, - ImGuiID key, - bool default_val) { - return self->GetBool(key, default_val); -} -CIMGUI_API void ImGuiStorage_SetBool(ImGuiStorage* self, - ImGuiID key, - bool val) { - return self->SetBool(key, val); -} -CIMGUI_API float ImGuiStorage_GetFloat(ImGuiStorage* self, - ImGuiID key, - float default_val) { - return self->GetFloat(key, default_val); -} -CIMGUI_API void ImGuiStorage_SetFloat(ImGuiStorage* self, - ImGuiID key, - float val) { - return self->SetFloat(key, val); -} -CIMGUI_API void* ImGuiStorage_GetVoidPtr(ImGuiStorage* self, ImGuiID key) { - return self->GetVoidPtr(key); -} -CIMGUI_API void ImGuiStorage_SetVoidPtr(ImGuiStorage* self, - ImGuiID key, - void* val) { - return self->SetVoidPtr(key, val); -} -CIMGUI_API int* ImGuiStorage_GetIntRef(ImGuiStorage* self, - ImGuiID key, - int default_val) { - return self->GetIntRef(key, default_val); -} -CIMGUI_API bool* ImGuiStorage_GetBoolRef(ImGuiStorage* self, - ImGuiID key, - bool default_val) { - return self->GetBoolRef(key, default_val); -} -CIMGUI_API float* ImGuiStorage_GetFloatRef(ImGuiStorage* self, - ImGuiID key, - float default_val) { - return self->GetFloatRef(key, default_val); -} -CIMGUI_API void** ImGuiStorage_GetVoidPtrRef(ImGuiStorage* self, - ImGuiID key, - void* default_val) { - return self->GetVoidPtrRef(key, default_val); -} -CIMGUI_API void ImGuiStorage_BuildSortByKey(ImGuiStorage* self) { - return self->BuildSortByKey(); -} -CIMGUI_API void ImGuiStorage_SetAllInt(ImGuiStorage* self, int val) { - return self->SetAllInt(val); -} -CIMGUI_API ImGuiListClipper* ImGuiListClipper_ImGuiListClipper(void) { - return IM_NEW(ImGuiListClipper)(); -} -CIMGUI_API void ImGuiListClipper_destroy(ImGuiListClipper* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiListClipper_Begin(ImGuiListClipper* self, - int items_count, - float items_height) { - return self->Begin(items_count, items_height); -} -CIMGUI_API void ImGuiListClipper_End(ImGuiListClipper* self) { - return self->End(); -} -CIMGUI_API bool ImGuiListClipper_Step(ImGuiListClipper* self) { - return self->Step(); -} -CIMGUI_API void ImGuiListClipper_IncludeItemByIndex(ImGuiListClipper* self, - int item_index) { - return self->IncludeItemByIndex(item_index); -} -CIMGUI_API void ImGuiListClipper_IncludeItemsByIndex(ImGuiListClipper* self, - int item_begin, - int item_end) { - return self->IncludeItemsByIndex(item_begin, item_end); -} -CIMGUI_API ImColor* ImColor_ImColor_Nil(void) { - return IM_NEW(ImColor)(); -} -CIMGUI_API void ImColor_destroy(ImColor* self) { - IM_DELETE(self); -} -CIMGUI_API ImColor* ImColor_ImColor_Float(float r, float g, float b, float a) { - return IM_NEW(ImColor)(r, g, b, a); -} -CIMGUI_API ImColor* ImColor_ImColor_Vec4(const ImVec4 col) { - return IM_NEW(ImColor)(col); -} -CIMGUI_API ImColor* ImColor_ImColor_Int(int r, int g, int b, int a) { - return IM_NEW(ImColor)(r, g, b, a); -} -CIMGUI_API ImColor* ImColor_ImColor_U32(ImU32 rgba) { - return IM_NEW(ImColor)(rgba); -} -CIMGUI_API void ImColor_SetHSV(ImColor* self, - float h, - float s, - float v, - float a) { - return self->SetHSV(h, s, v, a); -} -CIMGUI_API void ImColor_HSV(ImColor* pOut, float h, float s, float v, float a) { - *pOut = ImColor::HSV(h, s, v, a); -} -CIMGUI_API ImDrawCmd* ImDrawCmd_ImDrawCmd(void) { - return IM_NEW(ImDrawCmd)(); -} -CIMGUI_API void ImDrawCmd_destroy(ImDrawCmd* self) { - IM_DELETE(self); -} -CIMGUI_API ImTextureID ImDrawCmd_GetTexID(ImDrawCmd* self) { - return self->GetTexID(); -} -CIMGUI_API ImDrawListSplitter* ImDrawListSplitter_ImDrawListSplitter(void) { - return IM_NEW(ImDrawListSplitter)(); -} -CIMGUI_API void ImDrawListSplitter_destroy(ImDrawListSplitter* self) { - IM_DELETE(self); -} -CIMGUI_API void ImDrawListSplitter_Clear(ImDrawListSplitter* self) { - return self->Clear(); -} -CIMGUI_API void ImDrawListSplitter_ClearFreeMemory(ImDrawListSplitter* self) { - return self->ClearFreeMemory(); -} -CIMGUI_API void ImDrawListSplitter_Split(ImDrawListSplitter* self, - ImDrawList* draw_list, - int count) { - return self->Split(draw_list, count); -} -CIMGUI_API void ImDrawListSplitter_Merge(ImDrawListSplitter* self, - ImDrawList* draw_list) { - return self->Merge(draw_list); -} -CIMGUI_API void ImDrawListSplitter_SetCurrentChannel(ImDrawListSplitter* self, - ImDrawList* draw_list, - int channel_idx) { - return self->SetCurrentChannel(draw_list, channel_idx); -} -CIMGUI_API ImDrawList* ImDrawList_ImDrawList( - ImDrawListSharedData* shared_data) { - return IM_NEW(ImDrawList)(shared_data); -} -CIMGUI_API void ImDrawList_destroy(ImDrawList* self) { - IM_DELETE(self); -} -CIMGUI_API void ImDrawList_PushClipRect(ImDrawList* self, - const ImVec2 clip_rect_min, - const ImVec2 clip_rect_max, - bool intersect_with_current_clip_rect) { - return self->PushClipRect(clip_rect_min, clip_rect_max, - intersect_with_current_clip_rect); -} -CIMGUI_API void ImDrawList_PushClipRectFullScreen(ImDrawList* self) { - return self->PushClipRectFullScreen(); -} -CIMGUI_API void ImDrawList_PopClipRect(ImDrawList* self) { - return self->PopClipRect(); -} -CIMGUI_API void ImDrawList_PushTextureID(ImDrawList* self, - ImTextureID texture_id) { - return self->PushTextureID(texture_id); -} -CIMGUI_API void ImDrawList_PopTextureID(ImDrawList* self) { - return self->PopTextureID(); -} -CIMGUI_API void ImDrawList_GetClipRectMin(ImVec2* pOut, ImDrawList* self) { - *pOut = self->GetClipRectMin(); -} -CIMGUI_API void ImDrawList_GetClipRectMax(ImVec2* pOut, ImDrawList* self) { - *pOut = self->GetClipRectMax(); -} -CIMGUI_API void ImDrawList_AddLine(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - ImU32 col, - float thickness) { - return self->AddLine(p1, p2, col, thickness); -} -CIMGUI_API void ImDrawList_AddRect(ImDrawList* self, - const ImVec2 p_min, - const ImVec2 p_max, - ImU32 col, - float rounding, - ImDrawFlags flags, - float thickness) { - return self->AddRect(p_min, p_max, col, rounding, flags, thickness); -} -CIMGUI_API void ImDrawList_AddRectFilled(ImDrawList* self, - const ImVec2 p_min, - const ImVec2 p_max, - ImU32 col, - float rounding, - ImDrawFlags flags) { - return self->AddRectFilled(p_min, p_max, col, rounding, flags); -} -CIMGUI_API void ImDrawList_AddRectFilledMultiColor(ImDrawList* self, - const ImVec2 p_min, - const ImVec2 p_max, - ImU32 col_upr_left, - ImU32 col_upr_right, - ImU32 col_bot_right, - ImU32 col_bot_left) { - return self->AddRectFilledMultiColor( - p_min, p_max, col_upr_left, col_upr_right, col_bot_right, col_bot_left); -} -CIMGUI_API void ImDrawList_AddQuad(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - ImU32 col, - float thickness) { - return self->AddQuad(p1, p2, p3, p4, col, thickness); -} -CIMGUI_API void ImDrawList_AddQuadFilled(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - ImU32 col) { - return self->AddQuadFilled(p1, p2, p3, p4, col); -} -CIMGUI_API void ImDrawList_AddTriangle(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - ImU32 col, - float thickness) { - return self->AddTriangle(p1, p2, p3, col, thickness); -} -CIMGUI_API void ImDrawList_AddTriangleFilled(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - ImU32 col) { - return self->AddTriangleFilled(p1, p2, p3, col); -} -CIMGUI_API void ImDrawList_AddCircle(ImDrawList* self, - const ImVec2 center, - float radius, - ImU32 col, - int num_segments, - float thickness) { - return self->AddCircle(center, radius, col, num_segments, thickness); -} -CIMGUI_API void ImDrawList_AddCircleFilled(ImDrawList* self, - const ImVec2 center, - float radius, - ImU32 col, - int num_segments) { - return self->AddCircleFilled(center, radius, col, num_segments); -} -CIMGUI_API void ImDrawList_AddNgon(ImDrawList* self, - const ImVec2 center, - float radius, - ImU32 col, - int num_segments, - float thickness) { - return self->AddNgon(center, radius, col, num_segments, thickness); -} -CIMGUI_API void ImDrawList_AddNgonFilled(ImDrawList* self, - const ImVec2 center, - float radius, - ImU32 col, - int num_segments) { - return self->AddNgonFilled(center, radius, col, num_segments); -} -CIMGUI_API void ImDrawList_AddEllipse(ImDrawList* self, - const ImVec2 center, - const ImVec2 radius, - ImU32 col, - float rot, - int num_segments, - float thickness) { - return self->AddEllipse(center, radius, col, rot, num_segments, thickness); -} -CIMGUI_API void ImDrawList_AddEllipseFilled(ImDrawList* self, - const ImVec2 center, - const ImVec2 radius, - ImU32 col, - float rot, - int num_segments) { - return self->AddEllipseFilled(center, radius, col, rot, num_segments); -} -CIMGUI_API void ImDrawList_AddText_Vec2(ImDrawList* self, - const ImVec2 pos, - ImU32 col, - const char* text_begin, - const char* text_end) { - return self->AddText(pos, col, text_begin, text_end); -} -CIMGUI_API void ImDrawList_AddText_FontPtr(ImDrawList* self, - const ImFont* font, - float font_size, - const ImVec2 pos, - ImU32 col, - const char* text_begin, - const char* text_end, - float wrap_width, - const ImVec4* cpu_fine_clip_rect) { - return self->AddText(font, font_size, pos, col, text_begin, text_end, - wrap_width, cpu_fine_clip_rect); -} -CIMGUI_API void ImDrawList_AddBezierCubic(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - ImU32 col, - float thickness, - int num_segments) { - return self->AddBezierCubic(p1, p2, p3, p4, col, thickness, num_segments); -} -CIMGUI_API void ImDrawList_AddBezierQuadratic(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - ImU32 col, - float thickness, - int num_segments) { - return self->AddBezierQuadratic(p1, p2, p3, col, thickness, num_segments); -} -CIMGUI_API void ImDrawList_AddPolyline(ImDrawList* self, - const ImVec2* points, - int num_points, - ImU32 col, - ImDrawFlags flags, - float thickness) { - return self->AddPolyline(points, num_points, col, flags, thickness); -} -CIMGUI_API void ImDrawList_AddConvexPolyFilled(ImDrawList* self, - const ImVec2* points, - int num_points, - ImU32 col) { - return self->AddConvexPolyFilled(points, num_points, col); -} -CIMGUI_API void ImDrawList_AddConcavePolyFilled(ImDrawList* self, - const ImVec2* points, - int num_points, - ImU32 col) { - return self->AddConcavePolyFilled(points, num_points, col); -} -CIMGUI_API void ImDrawList_AddImage(ImDrawList* self, - ImTextureID user_texture_id, - const ImVec2 p_min, - const ImVec2 p_max, - const ImVec2 uv_min, - const ImVec2 uv_max, - ImU32 col) { - return self->AddImage(user_texture_id, p_min, p_max, uv_min, uv_max, col); -} -CIMGUI_API void ImDrawList_AddImageQuad(ImDrawList* self, - ImTextureID user_texture_id, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - const ImVec2 uv1, - const ImVec2 uv2, - const ImVec2 uv3, - const ImVec2 uv4, - ImU32 col) { - return self->AddImageQuad(user_texture_id, p1, p2, p3, p4, uv1, uv2, uv3, uv4, - col); -} -CIMGUI_API void ImDrawList_AddImageRounded(ImDrawList* self, - ImTextureID user_texture_id, - const ImVec2 p_min, - const ImVec2 p_max, - const ImVec2 uv_min, - const ImVec2 uv_max, - ImU32 col, - float rounding, - ImDrawFlags flags) { - return self->AddImageRounded(user_texture_id, p_min, p_max, uv_min, uv_max, - col, rounding, flags); -} -CIMGUI_API void ImDrawList_PathClear(ImDrawList* self) { - return self->PathClear(); -} -CIMGUI_API void ImDrawList_PathLineTo(ImDrawList* self, const ImVec2 pos) { - return self->PathLineTo(pos); -} -CIMGUI_API void ImDrawList_PathLineToMergeDuplicate(ImDrawList* self, - const ImVec2 pos) { - return self->PathLineToMergeDuplicate(pos); -} -CIMGUI_API void ImDrawList_PathFillConvex(ImDrawList* self, ImU32 col) { - return self->PathFillConvex(col); -} -CIMGUI_API void ImDrawList_PathFillConcave(ImDrawList* self, ImU32 col) { - return self->PathFillConcave(col); -} -CIMGUI_API void ImDrawList_PathStroke(ImDrawList* self, - ImU32 col, - ImDrawFlags flags, - float thickness) { - return self->PathStroke(col, flags, thickness); -} -CIMGUI_API void ImDrawList_PathArcTo(ImDrawList* self, - const ImVec2 center, - float radius, - float a_min, - float a_max, - int num_segments) { - return self->PathArcTo(center, radius, a_min, a_max, num_segments); -} -CIMGUI_API void ImDrawList_PathArcToFast(ImDrawList* self, - const ImVec2 center, - float radius, - int a_min_of_12, - int a_max_of_12) { - return self->PathArcToFast(center, radius, a_min_of_12, a_max_of_12); -} -CIMGUI_API void ImDrawList_PathEllipticalArcTo(ImDrawList* self, - const ImVec2 center, - const ImVec2 radius, - float rot, - float a_min, - float a_max, - int num_segments) { - return self->PathEllipticalArcTo(center, radius, rot, a_min, a_max, - num_segments); -} -CIMGUI_API void ImDrawList_PathBezierCubicCurveTo(ImDrawList* self, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - int num_segments) { - return self->PathBezierCubicCurveTo(p2, p3, p4, num_segments); -} -CIMGUI_API void ImDrawList_PathBezierQuadraticCurveTo(ImDrawList* self, - const ImVec2 p2, - const ImVec2 p3, - int num_segments) { - return self->PathBezierQuadraticCurveTo(p2, p3, num_segments); -} -CIMGUI_API void ImDrawList_PathRect(ImDrawList* self, - const ImVec2 rect_min, - const ImVec2 rect_max, - float rounding, - ImDrawFlags flags) { - return self->PathRect(rect_min, rect_max, rounding, flags); -} -CIMGUI_API void ImDrawList_AddCallback(ImDrawList* self, - ImDrawCallback callback, - void* callback_data) { - return self->AddCallback(callback, callback_data); -} -CIMGUI_API void ImDrawList_AddDrawCmd(ImDrawList* self) { - return self->AddDrawCmd(); -} -CIMGUI_API ImDrawList* ImDrawList_CloneOutput(ImDrawList* self) { - return self->CloneOutput(); -} -CIMGUI_API void ImDrawList_ChannelsSplit(ImDrawList* self, int count) { - return self->ChannelsSplit(count); -} -CIMGUI_API void ImDrawList_ChannelsMerge(ImDrawList* self) { - return self->ChannelsMerge(); -} -CIMGUI_API void ImDrawList_ChannelsSetCurrent(ImDrawList* self, int n) { - return self->ChannelsSetCurrent(n); -} -CIMGUI_API void ImDrawList_PrimReserve(ImDrawList* self, - int idx_count, - int vtx_count) { - return self->PrimReserve(idx_count, vtx_count); -} -CIMGUI_API void ImDrawList_PrimUnreserve(ImDrawList* self, - int idx_count, - int vtx_count) { - return self->PrimUnreserve(idx_count, vtx_count); -} -CIMGUI_API void ImDrawList_PrimRect(ImDrawList* self, - const ImVec2 a, - const ImVec2 b, - ImU32 col) { - return self->PrimRect(a, b, col); -} -CIMGUI_API void ImDrawList_PrimRectUV(ImDrawList* self, - const ImVec2 a, - const ImVec2 b, - const ImVec2 uv_a, - const ImVec2 uv_b, - ImU32 col) { - return self->PrimRectUV(a, b, uv_a, uv_b, col); -} -CIMGUI_API void ImDrawList_PrimQuadUV(ImDrawList* self, - const ImVec2 a, - const ImVec2 b, - const ImVec2 c, - const ImVec2 d, - const ImVec2 uv_a, - const ImVec2 uv_b, - const ImVec2 uv_c, - const ImVec2 uv_d, - ImU32 col) { - return self->PrimQuadUV(a, b, c, d, uv_a, uv_b, uv_c, uv_d, col); -} -CIMGUI_API void ImDrawList_PrimWriteVtx(ImDrawList* self, - const ImVec2 pos, - const ImVec2 uv, - ImU32 col) { - return self->PrimWriteVtx(pos, uv, col); -} -CIMGUI_API void ImDrawList_PrimWriteIdx(ImDrawList* self, ImDrawIdx idx) { - return self->PrimWriteIdx(idx); -} -CIMGUI_API void ImDrawList_PrimVtx(ImDrawList* self, - const ImVec2 pos, - const ImVec2 uv, - ImU32 col) { - return self->PrimVtx(pos, uv, col); -} -CIMGUI_API void ImDrawList__ResetForNewFrame(ImDrawList* self) { - return self->_ResetForNewFrame(); -} -CIMGUI_API void ImDrawList__ClearFreeMemory(ImDrawList* self) { - return self->_ClearFreeMemory(); -} -CIMGUI_API void ImDrawList__PopUnusedDrawCmd(ImDrawList* self) { - return self->_PopUnusedDrawCmd(); -} -CIMGUI_API void ImDrawList__TryMergeDrawCmds(ImDrawList* self) { - return self->_TryMergeDrawCmds(); -} -CIMGUI_API void ImDrawList__OnChangedClipRect(ImDrawList* self) { - return self->_OnChangedClipRect(); -} -CIMGUI_API void ImDrawList__OnChangedTextureID(ImDrawList* self) { - return self->_OnChangedTextureID(); -} -CIMGUI_API void ImDrawList__OnChangedVtxOffset(ImDrawList* self) { - return self->_OnChangedVtxOffset(); -} -CIMGUI_API int ImDrawList__CalcCircleAutoSegmentCount(ImDrawList* self, - float radius) { - return self->_CalcCircleAutoSegmentCount(radius); -} -CIMGUI_API void ImDrawList__PathArcToFastEx(ImDrawList* self, - const ImVec2 center, - float radius, - int a_min_sample, - int a_max_sample, - int a_step) { - return self->_PathArcToFastEx(center, radius, a_min_sample, a_max_sample, - a_step); -} -CIMGUI_API void ImDrawList__PathArcToN(ImDrawList* self, - const ImVec2 center, - float radius, - float a_min, - float a_max, - int num_segments) { - return self->_PathArcToN(center, radius, a_min, a_max, num_segments); -} -CIMGUI_API ImDrawData* ImDrawData_ImDrawData(void) { - return IM_NEW(ImDrawData)(); -} -CIMGUI_API void ImDrawData_destroy(ImDrawData* self) { - IM_DELETE(self); -} -CIMGUI_API void ImDrawData_Clear(ImDrawData* self) { - return self->Clear(); -} -CIMGUI_API void ImDrawData_AddDrawList(ImDrawData* self, - ImDrawList* draw_list) { - return self->AddDrawList(draw_list); -} -CIMGUI_API void ImDrawData_DeIndexAllBuffers(ImDrawData* self) { - return self->DeIndexAllBuffers(); -} -CIMGUI_API void ImDrawData_ScaleClipRects(ImDrawData* self, - const ImVec2 fb_scale) { - return self->ScaleClipRects(fb_scale); -} -CIMGUI_API ImFontConfig* ImFontConfig_ImFontConfig(void) { - return IM_NEW(ImFontConfig)(); -} -CIMGUI_API void ImFontConfig_destroy(ImFontConfig* self) { - IM_DELETE(self); -} -CIMGUI_API ImFontGlyphRangesBuilder* -ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder(void) { - return IM_NEW(ImFontGlyphRangesBuilder)(); -} -CIMGUI_API void ImFontGlyphRangesBuilder_destroy( - ImFontGlyphRangesBuilder* self) { - IM_DELETE(self); -} -CIMGUI_API void ImFontGlyphRangesBuilder_Clear(ImFontGlyphRangesBuilder* self) { - return self->Clear(); -} -CIMGUI_API bool ImFontGlyphRangesBuilder_GetBit(ImFontGlyphRangesBuilder* self, - size_t n) { - return self->GetBit(n); -} -CIMGUI_API void ImFontGlyphRangesBuilder_SetBit(ImFontGlyphRangesBuilder* self, - size_t n) { - return self->SetBit(n); -} -CIMGUI_API void ImFontGlyphRangesBuilder_AddChar(ImFontGlyphRangesBuilder* self, - ImWchar c) { - return self->AddChar(c); -} -CIMGUI_API void ImFontGlyphRangesBuilder_AddText(ImFontGlyphRangesBuilder* self, - const char* text, - const char* text_end) { - return self->AddText(text, text_end); -} -CIMGUI_API void ImFontGlyphRangesBuilder_AddRanges( - ImFontGlyphRangesBuilder* self, - const ImWchar* ranges) { - return self->AddRanges(ranges); -} -CIMGUI_API void ImFontGlyphRangesBuilder_BuildRanges( - ImFontGlyphRangesBuilder* self, - ImVector_ImWchar* out_ranges) { - return self->BuildRanges(out_ranges); -} -CIMGUI_API ImFontAtlasCustomRect* ImFontAtlasCustomRect_ImFontAtlasCustomRect( - void) { - return IM_NEW(ImFontAtlasCustomRect)(); -} -CIMGUI_API void ImFontAtlasCustomRect_destroy(ImFontAtlasCustomRect* self) { - IM_DELETE(self); -} -CIMGUI_API bool ImFontAtlasCustomRect_IsPacked(ImFontAtlasCustomRect* self) { - return self->IsPacked(); -} -CIMGUI_API ImFontAtlas* ImFontAtlas_ImFontAtlas(void) { - return IM_NEW(ImFontAtlas)(); -} -CIMGUI_API void ImFontAtlas_destroy(ImFontAtlas* self) { - IM_DELETE(self); -} -CIMGUI_API ImFont* ImFontAtlas_AddFont(ImFontAtlas* self, - const ImFontConfig* font_cfg) { - return self->AddFont(font_cfg); -} -CIMGUI_API ImFont* ImFontAtlas_AddFontDefault(ImFontAtlas* self, - const ImFontConfig* font_cfg) { - return self->AddFontDefault(font_cfg); -} -CIMGUI_API ImFont* ImFontAtlas_AddFontFromFileTTF(ImFontAtlas* self, - const char* filename, - float size_pixels, - const ImFontConfig* font_cfg, - const ImWchar* glyph_ranges) { - return self->AddFontFromFileTTF(filename, size_pixels, font_cfg, - glyph_ranges); -} -CIMGUI_API ImFont* ImFontAtlas_AddFontFromMemoryTTF( - ImFontAtlas* self, - void* font_data, - int font_data_size, - float size_pixels, - const ImFontConfig* font_cfg, - const ImWchar* glyph_ranges) { - return self->AddFontFromMemoryTTF(font_data, font_data_size, size_pixels, - font_cfg, glyph_ranges); -} -CIMGUI_API ImFont* ImFontAtlas_AddFontFromMemoryCompressedTTF( - ImFontAtlas* self, - const void* compressed_font_data, - int compressed_font_data_size, - float size_pixels, - const ImFontConfig* font_cfg, - const ImWchar* glyph_ranges) { - return self->AddFontFromMemoryCompressedTTF( - compressed_font_data, compressed_font_data_size, size_pixels, font_cfg, - glyph_ranges); -} -CIMGUI_API ImFont* ImFontAtlas_AddFontFromMemoryCompressedBase85TTF( - ImFontAtlas* self, - const char* compressed_font_data_base85, - float size_pixels, - const ImFontConfig* font_cfg, - const ImWchar* glyph_ranges) { - return self->AddFontFromMemoryCompressedBase85TTF( - compressed_font_data_base85, size_pixels, font_cfg, glyph_ranges); -} -CIMGUI_API void ImFontAtlas_ClearInputData(ImFontAtlas* self) { - return self->ClearInputData(); -} -CIMGUI_API void ImFontAtlas_ClearTexData(ImFontAtlas* self) { - return self->ClearTexData(); -} -CIMGUI_API void ImFontAtlas_ClearFonts(ImFontAtlas* self) { - return self->ClearFonts(); -} -CIMGUI_API void ImFontAtlas_Clear(ImFontAtlas* self) { - return self->Clear(); -} -CIMGUI_API bool ImFontAtlas_Build(ImFontAtlas* self) { - return self->Build(); -} -CIMGUI_API void ImFontAtlas_GetTexDataAsAlpha8(ImFontAtlas* self, - unsigned char** out_pixels, - int* out_width, - int* out_height, - int* out_bytes_per_pixel) { - return self->GetTexDataAsAlpha8(out_pixels, out_width, out_height, - out_bytes_per_pixel); -} -CIMGUI_API void ImFontAtlas_GetTexDataAsRGBA32(ImFontAtlas* self, - unsigned char** out_pixels, - int* out_width, - int* out_height, - int* out_bytes_per_pixel) { - return self->GetTexDataAsRGBA32(out_pixels, out_width, out_height, - out_bytes_per_pixel); -} -CIMGUI_API bool ImFontAtlas_IsBuilt(ImFontAtlas* self) { - return self->IsBuilt(); -} -CIMGUI_API void ImFontAtlas_SetTexID(ImFontAtlas* self, ImTextureID id) { - return self->SetTexID(id); -} -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesDefault(ImFontAtlas* self) { - return self->GetGlyphRangesDefault(); -} -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesGreek(ImFontAtlas* self) { - return self->GetGlyphRangesGreek(); -} -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesKorean(ImFontAtlas* self) { - return self->GetGlyphRangesKorean(); -} -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesJapanese( - ImFontAtlas* self) { - return self->GetGlyphRangesJapanese(); -} -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesChineseFull( - ImFontAtlas* self) { - return self->GetGlyphRangesChineseFull(); -} -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesChineseSimplifiedCommon( - ImFontAtlas* self) { - return self->GetGlyphRangesChineseSimplifiedCommon(); -} -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesCyrillic( - ImFontAtlas* self) { - return self->GetGlyphRangesCyrillic(); -} -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesThai(ImFontAtlas* self) { - return self->GetGlyphRangesThai(); -} -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesVietnamese( - ImFontAtlas* self) { - return self->GetGlyphRangesVietnamese(); -} -CIMGUI_API int ImFontAtlas_AddCustomRectRegular(ImFontAtlas* self, - int width, - int height) { - return self->AddCustomRectRegular(width, height); -} -CIMGUI_API int ImFontAtlas_AddCustomRectFontGlyph(ImFontAtlas* self, - ImFont* font, - ImWchar id, - int width, - int height, - float advance_x, - const ImVec2 offset) { - return self->AddCustomRectFontGlyph(font, id, width, height, advance_x, - offset); -} -CIMGUI_API ImFontAtlasCustomRect* ImFontAtlas_GetCustomRectByIndex( - ImFontAtlas* self, - int index) { - return self->GetCustomRectByIndex(index); -} -CIMGUI_API void ImFontAtlas_CalcCustomRectUV(ImFontAtlas* self, - const ImFontAtlasCustomRect* rect, - ImVec2* out_uv_min, - ImVec2* out_uv_max) { - return self->CalcCustomRectUV(rect, out_uv_min, out_uv_max); -} -CIMGUI_API bool ImFontAtlas_GetMouseCursorTexData(ImFontAtlas* self, - ImGuiMouseCursor cursor, - ImVec2* out_offset, - ImVec2* out_size, - ImVec2 out_uv_border[2], - ImVec2 out_uv_fill[2]) { - return self->GetMouseCursorTexData(cursor, out_offset, out_size, - out_uv_border, out_uv_fill); -} -CIMGUI_API ImFont* ImFont_ImFont(void) { - return IM_NEW(ImFont)(); -} -CIMGUI_API void ImFont_destroy(ImFont* self) { - IM_DELETE(self); -} -CIMGUI_API const ImFontGlyph* ImFont_FindGlyph(ImFont* self, ImWchar c) { - return self->FindGlyph(c); -} -CIMGUI_API const ImFontGlyph* ImFont_FindGlyphNoFallback(ImFont* self, - ImWchar c) { - return self->FindGlyphNoFallback(c); -} -CIMGUI_API float ImFont_GetCharAdvance(ImFont* self, ImWchar c) { - return self->GetCharAdvance(c); -} -CIMGUI_API bool ImFont_IsLoaded(ImFont* self) { - return self->IsLoaded(); -} -CIMGUI_API const char* ImFont_GetDebugName(ImFont* self) { - return self->GetDebugName(); -} -CIMGUI_API void ImFont_CalcTextSizeA(ImVec2* pOut, - ImFont* self, - float size, - float max_width, - float wrap_width, - const char* text_begin, - const char* text_end, - const char** remaining) { - *pOut = self->CalcTextSizeA(size, max_width, wrap_width, text_begin, text_end, - remaining); -} -CIMGUI_API const char* ImFont_CalcWordWrapPositionA(ImFont* self, - float scale, - const char* text, - const char* text_end, - float wrap_width) { - return self->CalcWordWrapPositionA(scale, text, text_end, wrap_width); -} -CIMGUI_API void ImFont_RenderChar(ImFont* self, - ImDrawList* draw_list, - float size, - const ImVec2 pos, - ImU32 col, - ImWchar c) { - return self->RenderChar(draw_list, size, pos, col, c); -} -CIMGUI_API void ImFont_RenderText(ImFont* self, - ImDrawList* draw_list, - float size, - const ImVec2 pos, - ImU32 col, - const ImVec4 clip_rect, - const char* text_begin, - const char* text_end, - float wrap_width, - bool cpu_fine_clip) { - return self->RenderText(draw_list, size, pos, col, clip_rect, text_begin, - text_end, wrap_width, cpu_fine_clip); -} -CIMGUI_API void ImFont_BuildLookupTable(ImFont* self) { - return self->BuildLookupTable(); -} -CIMGUI_API void ImFont_ClearOutputData(ImFont* self) { - return self->ClearOutputData(); -} -CIMGUI_API void ImFont_GrowIndex(ImFont* self, int new_size) { - return self->GrowIndex(new_size); -} -CIMGUI_API void ImFont_AddGlyph(ImFont* self, - const ImFontConfig* src_cfg, - ImWchar c, - float x0, - float y0, - float x1, - float y1, - float u0, - float v0, - float u1, - float v1, - float advance_x) { - return self->AddGlyph(src_cfg, c, x0, y0, x1, y1, u0, v0, u1, v1, advance_x); -} -CIMGUI_API void ImFont_AddRemapChar(ImFont* self, - ImWchar dst, - ImWchar src, - bool overwrite_dst) { - return self->AddRemapChar(dst, src, overwrite_dst); -} -CIMGUI_API void ImFont_SetGlyphVisible(ImFont* self, ImWchar c, bool visible) { - return self->SetGlyphVisible(c, visible); -} -CIMGUI_API bool ImFont_IsGlyphRangeUnused(ImFont* self, - unsigned int c_begin, - unsigned int c_last) { - return self->IsGlyphRangeUnused(c_begin, c_last); -} -CIMGUI_API ImGuiViewport* ImGuiViewport_ImGuiViewport(void) { - return IM_NEW(ImGuiViewport)(); -} -CIMGUI_API void ImGuiViewport_destroy(ImGuiViewport* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiViewport_GetCenter(ImVec2* pOut, ImGuiViewport* self) { - *pOut = self->GetCenter(); -} -CIMGUI_API void ImGuiViewport_GetWorkCenter(ImVec2* pOut, ImGuiViewport* self) { - *pOut = self->GetWorkCenter(); -} -CIMGUI_API ImGuiPlatformIO* ImGuiPlatformIO_ImGuiPlatformIO(void) { - return IM_NEW(ImGuiPlatformIO)(); -} -CIMGUI_API void ImGuiPlatformIO_destroy(ImGuiPlatformIO* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiPlatformMonitor* ImGuiPlatformMonitor_ImGuiPlatformMonitor( - void) { - return IM_NEW(ImGuiPlatformMonitor)(); -} -CIMGUI_API void ImGuiPlatformMonitor_destroy(ImGuiPlatformMonitor* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiPlatformImeData* ImGuiPlatformImeData_ImGuiPlatformImeData( - void) { - return IM_NEW(ImGuiPlatformImeData)(); -} -CIMGUI_API void ImGuiPlatformImeData_destroy(ImGuiPlatformImeData* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiID igImHashData(const void* data, - size_t data_size, - ImGuiID seed) { - return ImHashData(data, data_size, seed); -} -CIMGUI_API ImGuiID igImHashStr(const char* data, - size_t data_size, - ImGuiID seed) { - return ImHashStr(data, data_size, seed); -} -CIMGUI_API void igImQsort(void* base, - size_t count, - size_t size_of_element, - int (*compare_func)(void const*, void const*)) { - return ImQsort(base, count, size_of_element, compare_func); -} -CIMGUI_API ImU32 igImAlphaBlendColors(ImU32 col_a, ImU32 col_b) { - return ImAlphaBlendColors(col_a, col_b); -} -CIMGUI_API bool igImIsPowerOfTwo_Int(int v) { - return ImIsPowerOfTwo(v); -} -CIMGUI_API bool igImIsPowerOfTwo_U64(ImU64 v) { - return ImIsPowerOfTwo(v); -} -CIMGUI_API int igImUpperPowerOfTwo(int v) { - return ImUpperPowerOfTwo(v); -} -CIMGUI_API int igImStricmp(const char* str1, const char* str2) { - return ImStricmp(str1, str2); -} -CIMGUI_API int igImStrnicmp(const char* str1, const char* str2, size_t count) { - return ImStrnicmp(str1, str2, count); -} -CIMGUI_API void igImStrncpy(char* dst, const char* src, size_t count) { - return ImStrncpy(dst, src, count); -} -CIMGUI_API char* igImStrdup(const char* str) { - return ImStrdup(str); -} -CIMGUI_API char* igImStrdupcpy(char* dst, size_t* p_dst_size, const char* str) { - return ImStrdupcpy(dst, p_dst_size, str); -} -CIMGUI_API const char* igImStrchrRange(const char* str_begin, - const char* str_end, - char c) { - return ImStrchrRange(str_begin, str_end, c); -} -CIMGUI_API const char* igImStreolRange(const char* str, const char* str_end) { - return ImStreolRange(str, str_end); -} -CIMGUI_API const char* igImStristr(const char* haystack, - const char* haystack_end, - const char* needle, - const char* needle_end) { - return ImStristr(haystack, haystack_end, needle, needle_end); -} -CIMGUI_API void igImStrTrimBlanks(char* str) { - return ImStrTrimBlanks(str); -} -CIMGUI_API const char* igImStrSkipBlank(const char* str) { - return ImStrSkipBlank(str); -} -CIMGUI_API int igImStrlenW(const ImWchar* str) { - return ImStrlenW(str); -} -CIMGUI_API const ImWchar* igImStrbolW(const ImWchar* buf_mid_line, - const ImWchar* buf_begin) { - return ImStrbolW(buf_mid_line, buf_begin); -} -CIMGUI_API char igImToUpper(char c) { - return ImToUpper(c); -} -CIMGUI_API bool igImCharIsBlankA(char c) { - return ImCharIsBlankA(c); -} -CIMGUI_API bool igImCharIsBlankW(unsigned int c) { - return ImCharIsBlankW(c); -} -CIMGUI_API int igImFormatString(char* buf, - size_t buf_size, - const char* fmt, - ...) { - va_list args; - va_start(args, fmt); - int ret = ImFormatStringV(buf, buf_size, fmt, args); - va_end(args); - return ret; -} -CIMGUI_API int igImFormatStringV(char* buf, - size_t buf_size, - const char* fmt, - va_list args) { - return ImFormatStringV(buf, buf_size, fmt, args); -} -CIMGUI_API void igImFormatStringToTempBuffer(const char** out_buf, - const char** out_buf_end, - const char* fmt, - ...) { - va_list args; - va_start(args, fmt); - ImFormatStringToTempBufferV(out_buf, out_buf_end, fmt, args); - va_end(args); -} -CIMGUI_API void igImFormatStringToTempBufferV(const char** out_buf, - const char** out_buf_end, - const char* fmt, - va_list args) { - return ImFormatStringToTempBufferV(out_buf, out_buf_end, fmt, args); -} -CIMGUI_API const char* igImParseFormatFindStart(const char* format) { - return ImParseFormatFindStart(format); -} -CIMGUI_API const char* igImParseFormatFindEnd(const char* format) { - return ImParseFormatFindEnd(format); -} -CIMGUI_API const char* igImParseFormatTrimDecorations(const char* format, - char* buf, - size_t buf_size) { - return ImParseFormatTrimDecorations(format, buf, buf_size); -} -CIMGUI_API void igImParseFormatSanitizeForPrinting(const char* fmt_in, - char* fmt_out, - size_t fmt_out_size) { - return ImParseFormatSanitizeForPrinting(fmt_in, fmt_out, fmt_out_size); -} -CIMGUI_API const char* igImParseFormatSanitizeForScanning(const char* fmt_in, - char* fmt_out, - size_t fmt_out_size) { - return ImParseFormatSanitizeForScanning(fmt_in, fmt_out, fmt_out_size); -} -CIMGUI_API int igImParseFormatPrecision(const char* format, int default_value) { - return ImParseFormatPrecision(format, default_value); -} -CIMGUI_API const char* igImTextCharToUtf8(char out_buf[5], unsigned int c) { - return ImTextCharToUtf8(out_buf, c); -} -CIMGUI_API int igImTextStrToUtf8(char* out_buf, - int out_buf_size, - const ImWchar* in_text, - const ImWchar* in_text_end) { - return ImTextStrToUtf8(out_buf, out_buf_size, in_text, in_text_end); -} -CIMGUI_API int igImTextCharFromUtf8(unsigned int* out_char, - const char* in_text, - const char* in_text_end) { - return ImTextCharFromUtf8(out_char, in_text, in_text_end); -} -CIMGUI_API int igImTextStrFromUtf8(ImWchar* out_buf, - int out_buf_size, - const char* in_text, - const char* in_text_end, - const char** in_remaining) { - return ImTextStrFromUtf8(out_buf, out_buf_size, in_text, in_text_end, - in_remaining); -} -CIMGUI_API int igImTextCountCharsFromUtf8(const char* in_text, - const char* in_text_end) { - return ImTextCountCharsFromUtf8(in_text, in_text_end); -} -CIMGUI_API int igImTextCountUtf8BytesFromChar(const char* in_text, - const char* in_text_end) { - return ImTextCountUtf8BytesFromChar(in_text, in_text_end); -} -CIMGUI_API int igImTextCountUtf8BytesFromStr(const ImWchar* in_text, - const ImWchar* in_text_end) { - return ImTextCountUtf8BytesFromStr(in_text, in_text_end); -} -CIMGUI_API const char* igImTextFindPreviousUtf8Codepoint( - const char* in_text_start, - const char* in_text_curr) { - return ImTextFindPreviousUtf8Codepoint(in_text_start, in_text_curr); -} -CIMGUI_API int igImTextCountLines(const char* in_text, - const char* in_text_end) { - return ImTextCountLines(in_text, in_text_end); -} -CIMGUI_API ImFileHandle igImFileOpen(const char* filename, const char* mode) { - return ImFileOpen(filename, mode); -} -CIMGUI_API bool igImFileClose(ImFileHandle file) { - return ImFileClose(file); -} -CIMGUI_API ImU64 igImFileGetSize(ImFileHandle file) { - return ImFileGetSize(file); -} -CIMGUI_API ImU64 igImFileRead(void* data, - ImU64 size, - ImU64 count, - ImFileHandle file) { - return ImFileRead(data, size, count, file); -} -CIMGUI_API ImU64 igImFileWrite(const void* data, - ImU64 size, - ImU64 count, - ImFileHandle file) { - return ImFileWrite(data, size, count, file); -} -CIMGUI_API void* igImFileLoadToMemory(const char* filename, - const char* mode, - size_t* out_file_size, - int padding_bytes) { - return ImFileLoadToMemory(filename, mode, out_file_size, padding_bytes); -} -CIMGUI_API float igImPow_Float(float x, float y) { - return ImPow(x, y); -} -CIMGUI_API double igImPow_double(double x, double y) { - return ImPow(x, y); -} -CIMGUI_API float igImLog_Float(float x) { - return ImLog(x); -} -CIMGUI_API double igImLog_double(double x) { - return ImLog(x); -} -CIMGUI_API int igImAbs_Int(int x) { - return ImAbs(x); -} -CIMGUI_API float igImAbs_Float(float x) { - return ImAbs(x); -} -CIMGUI_API double igImAbs_double(double x) { - return ImAbs(x); -} -CIMGUI_API float igImSign_Float(float x) { - return ImSign(x); -} -CIMGUI_API double igImSign_double(double x) { - return ImSign(x); -} -CIMGUI_API float igImRsqrt_Float(float x) { - return ImRsqrt(x); -} -CIMGUI_API double igImRsqrt_double(double x) { - return ImRsqrt(x); -} -CIMGUI_API void igImMin(ImVec2* pOut, const ImVec2 lhs, const ImVec2 rhs) { - *pOut = ImMin(lhs, rhs); -} -CIMGUI_API void igImMax(ImVec2* pOut, const ImVec2 lhs, const ImVec2 rhs) { - *pOut = ImMax(lhs, rhs); -} -CIMGUI_API void igImClamp(ImVec2* pOut, - const ImVec2 v, - const ImVec2 mn, - ImVec2 mx) { - *pOut = ImClamp(v, mn, mx); -} -CIMGUI_API void igImLerp_Vec2Float(ImVec2* pOut, - const ImVec2 a, - const ImVec2 b, - float t) { - *pOut = ImLerp(a, b, t); -} -CIMGUI_API void igImLerp_Vec2Vec2(ImVec2* pOut, - const ImVec2 a, - const ImVec2 b, - const ImVec2 t) { - *pOut = ImLerp(a, b, t); -} -CIMGUI_API void igImLerp_Vec4(ImVec4* pOut, - const ImVec4 a, - const ImVec4 b, - float t) { - *pOut = ImLerp(a, b, t); -} -CIMGUI_API float igImSaturate(float f) { - return ImSaturate(f); -} -CIMGUI_API float igImLengthSqr_Vec2(const ImVec2 lhs) { - return ImLengthSqr(lhs); -} -CIMGUI_API float igImLengthSqr_Vec4(const ImVec4 lhs) { - return ImLengthSqr(lhs); -} -CIMGUI_API float igImInvLength(const ImVec2 lhs, float fail_value) { - return ImInvLength(lhs, fail_value); -} -CIMGUI_API float igImTrunc_Float(float f) { - return ImTrunc(f); -} -CIMGUI_API void igImTrunc_Vec2(ImVec2* pOut, const ImVec2 v) { - *pOut = ImTrunc(v); -} -CIMGUI_API float igImFloor_Float(float f) { - return ImFloor(f); -} -CIMGUI_API void igImFloor_Vec2(ImVec2* pOut, const ImVec2 v) { - *pOut = ImFloor(v); -} -CIMGUI_API int igImModPositive(int a, int b) { - return ImModPositive(a, b); -} -CIMGUI_API float igImDot(const ImVec2 a, const ImVec2 b) { - return ImDot(a, b); -} -CIMGUI_API void igImRotate(ImVec2* pOut, - const ImVec2 v, - float cos_a, - float sin_a) { - *pOut = ImRotate(v, cos_a, sin_a); -} -CIMGUI_API float igImLinearSweep(float current, float target, float speed) { - return ImLinearSweep(current, target, speed); -} -CIMGUI_API void igImMul(ImVec2* pOut, const ImVec2 lhs, const ImVec2 rhs) { - *pOut = ImMul(lhs, rhs); -} -CIMGUI_API bool igImIsFloatAboveGuaranteedIntegerPrecision(float f) { - return ImIsFloatAboveGuaranteedIntegerPrecision(f); -} -CIMGUI_API float igImExponentialMovingAverage(float avg, float sample, int n) { - return ImExponentialMovingAverage(avg, sample, n); -} -CIMGUI_API void igImBezierCubicCalc(ImVec2* pOut, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - float t) { - *pOut = ImBezierCubicCalc(p1, p2, p3, p4, t); -} -CIMGUI_API void igImBezierCubicClosestPoint(ImVec2* pOut, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - const ImVec2 p, - int num_segments) { - *pOut = ImBezierCubicClosestPoint(p1, p2, p3, p4, p, num_segments); -} -CIMGUI_API void igImBezierCubicClosestPointCasteljau(ImVec2* pOut, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - const ImVec2 p, - float tess_tol) { - *pOut = ImBezierCubicClosestPointCasteljau(p1, p2, p3, p4, p, tess_tol); -} -CIMGUI_API void igImBezierQuadraticCalc(ImVec2* pOut, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - float t) { - *pOut = ImBezierQuadraticCalc(p1, p2, p3, t); -} -CIMGUI_API void igImLineClosestPoint(ImVec2* pOut, - const ImVec2 a, - const ImVec2 b, - const ImVec2 p) { - *pOut = ImLineClosestPoint(a, b, p); -} -CIMGUI_API bool igImTriangleContainsPoint(const ImVec2 a, - const ImVec2 b, - const ImVec2 c, - const ImVec2 p) { - return ImTriangleContainsPoint(a, b, c, p); -} -CIMGUI_API void igImTriangleClosestPoint(ImVec2* pOut, - const ImVec2 a, - const ImVec2 b, - const ImVec2 c, - const ImVec2 p) { - *pOut = ImTriangleClosestPoint(a, b, c, p); -} -CIMGUI_API void igImTriangleBarycentricCoords(const ImVec2 a, - const ImVec2 b, - const ImVec2 c, - const ImVec2 p, - float* out_u, - float* out_v, - float* out_w) { - return ImTriangleBarycentricCoords(a, b, c, p, *out_u, *out_v, *out_w); -} -CIMGUI_API float igImTriangleArea(const ImVec2 a, - const ImVec2 b, - const ImVec2 c) { - return ImTriangleArea(a, b, c); -} -CIMGUI_API bool igImTriangleIsClockwise(const ImVec2 a, - const ImVec2 b, - const ImVec2 c) { - return ImTriangleIsClockwise(a, b, c); -} -CIMGUI_API ImVec1* ImVec1_ImVec1_Nil(void) { - return IM_NEW(ImVec1)(); -} -CIMGUI_API void ImVec1_destroy(ImVec1* self) { - IM_DELETE(self); -} -CIMGUI_API ImVec1* ImVec1_ImVec1_Float(float _x) { - return IM_NEW(ImVec1)(_x); -} -CIMGUI_API ImVec2ih* ImVec2ih_ImVec2ih_Nil(void) { - return IM_NEW(ImVec2ih)(); -} -CIMGUI_API void ImVec2ih_destroy(ImVec2ih* self) { - IM_DELETE(self); -} -CIMGUI_API ImVec2ih* ImVec2ih_ImVec2ih_short(short _x, short _y) { - return IM_NEW(ImVec2ih)(_x, _y); -} -CIMGUI_API ImVec2ih* ImVec2ih_ImVec2ih_Vec2(const ImVec2 rhs) { - return IM_NEW(ImVec2ih)(rhs); -} -CIMGUI_API ImRect* ImRect_ImRect_Nil(void) { - return IM_NEW(ImRect)(); -} -CIMGUI_API void ImRect_destroy(ImRect* self) { - IM_DELETE(self); -} -CIMGUI_API ImRect* ImRect_ImRect_Vec2(const ImVec2 min, const ImVec2 max) { - return IM_NEW(ImRect)(min, max); -} -CIMGUI_API ImRect* ImRect_ImRect_Vec4(const ImVec4 v) { - return IM_NEW(ImRect)(v); -} -CIMGUI_API ImRect* ImRect_ImRect_Float(float x1, float y1, float x2, float y2) { - return IM_NEW(ImRect)(x1, y1, x2, y2); -} -CIMGUI_API void ImRect_GetCenter(ImVec2* pOut, ImRect* self) { - *pOut = self->GetCenter(); -} -CIMGUI_API void ImRect_GetSize(ImVec2* pOut, ImRect* self) { - *pOut = self->GetSize(); -} -CIMGUI_API float ImRect_GetWidth(ImRect* self) { - return self->GetWidth(); -} -CIMGUI_API float ImRect_GetHeight(ImRect* self) { - return self->GetHeight(); -} -CIMGUI_API float ImRect_GetArea(ImRect* self) { - return self->GetArea(); -} -CIMGUI_API void ImRect_GetTL(ImVec2* pOut, ImRect* self) { - *pOut = self->GetTL(); -} -CIMGUI_API void ImRect_GetTR(ImVec2* pOut, ImRect* self) { - *pOut = self->GetTR(); -} -CIMGUI_API void ImRect_GetBL(ImVec2* pOut, ImRect* self) { - *pOut = self->GetBL(); -} -CIMGUI_API void ImRect_GetBR(ImVec2* pOut, ImRect* self) { - *pOut = self->GetBR(); -} -CIMGUI_API bool ImRect_Contains_Vec2(ImRect* self, const ImVec2 p) { - return self->Contains(p); -} -CIMGUI_API bool ImRect_Contains_Rect(ImRect* self, const ImRect r) { - return self->Contains(r); -} -CIMGUI_API bool ImRect_ContainsWithPad(ImRect* self, - const ImVec2 p, - const ImVec2 pad) { - return self->ContainsWithPad(p, pad); -} -CIMGUI_API bool ImRect_Overlaps(ImRect* self, const ImRect r) { - return self->Overlaps(r); -} -CIMGUI_API void ImRect_Add_Vec2(ImRect* self, const ImVec2 p) { - return self->Add(p); -} -CIMGUI_API void ImRect_Add_Rect(ImRect* self, const ImRect r) { - return self->Add(r); -} -CIMGUI_API void ImRect_Expand_Float(ImRect* self, const float amount) { - return self->Expand(amount); -} -CIMGUI_API void ImRect_Expand_Vec2(ImRect* self, const ImVec2 amount) { - return self->Expand(amount); -} -CIMGUI_API void ImRect_Translate(ImRect* self, const ImVec2 d) { - return self->Translate(d); -} -CIMGUI_API void ImRect_TranslateX(ImRect* self, float dx) { - return self->TranslateX(dx); -} -CIMGUI_API void ImRect_TranslateY(ImRect* self, float dy) { - return self->TranslateY(dy); -} -CIMGUI_API void ImRect_ClipWith(ImRect* self, const ImRect r) { - return self->ClipWith(r); -} -CIMGUI_API void ImRect_ClipWithFull(ImRect* self, const ImRect r) { - return self->ClipWithFull(r); -} -CIMGUI_API void ImRect_Floor(ImRect* self) { - return self->Floor(); -} -CIMGUI_API bool ImRect_IsInverted(ImRect* self) { - return self->IsInverted(); -} -CIMGUI_API void ImRect_ToVec4(ImVec4* pOut, ImRect* self) { - *pOut = self->ToVec4(); -} -CIMGUI_API size_t igImBitArrayGetStorageSizeInBytes(int bitcount) { - return ImBitArrayGetStorageSizeInBytes(bitcount); -} -CIMGUI_API void igImBitArrayClearAllBits(ImU32* arr, int bitcount) { - return ImBitArrayClearAllBits(arr, bitcount); -} -CIMGUI_API bool igImBitArrayTestBit(const ImU32* arr, int n) { - return ImBitArrayTestBit(arr, n); -} -CIMGUI_API void igImBitArrayClearBit(ImU32* arr, int n) { - return ImBitArrayClearBit(arr, n); -} -CIMGUI_API void igImBitArraySetBit(ImU32* arr, int n) { - return ImBitArraySetBit(arr, n); -} -CIMGUI_API void igImBitArraySetBitRange(ImU32* arr, int n, int n2) { - return ImBitArraySetBitRange(arr, n, n2); -} -CIMGUI_API void ImBitVector_Create(ImBitVector* self, int sz) { - return self->Create(sz); -} -CIMGUI_API void ImBitVector_Clear(ImBitVector* self) { - return self->Clear(); -} -CIMGUI_API bool ImBitVector_TestBit(ImBitVector* self, int n) { - return self->TestBit(n); -} -CIMGUI_API void ImBitVector_SetBit(ImBitVector* self, int n) { - return self->SetBit(n); -} -CIMGUI_API void ImBitVector_ClearBit(ImBitVector* self, int n) { - return self->ClearBit(n); -} -CIMGUI_API void ImGuiTextIndex_clear(ImGuiTextIndex* self) { - return self->clear(); -} -CIMGUI_API int ImGuiTextIndex_size(ImGuiTextIndex* self) { - return self->size(); -} -CIMGUI_API const char* ImGuiTextIndex_get_line_begin(ImGuiTextIndex* self, - const char* base, - int n) { - return self->get_line_begin(base, n); -} -CIMGUI_API const char* ImGuiTextIndex_get_line_end(ImGuiTextIndex* self, - const char* base, - int n) { - return self->get_line_end(base, n); -} -CIMGUI_API void ImGuiTextIndex_append(ImGuiTextIndex* self, - const char* base, - int old_size, - int new_size) { - return self->append(base, old_size, new_size); -} -CIMGUI_API ImDrawListSharedData* ImDrawListSharedData_ImDrawListSharedData( - void) { - return IM_NEW(ImDrawListSharedData)(); -} -CIMGUI_API void ImDrawListSharedData_destroy(ImDrawListSharedData* self) { - IM_DELETE(self); -} -CIMGUI_API void ImDrawListSharedData_SetCircleTessellationMaxError( - ImDrawListSharedData* self, - float max_error) { - return self->SetCircleTessellationMaxError(max_error); -} -CIMGUI_API ImDrawDataBuilder* ImDrawDataBuilder_ImDrawDataBuilder(void) { - return IM_NEW(ImDrawDataBuilder)(); -} -CIMGUI_API void ImDrawDataBuilder_destroy(ImDrawDataBuilder* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiStyleMod* ImGuiStyleMod_ImGuiStyleMod_Int(ImGuiStyleVar idx, - int v) { - return IM_NEW(ImGuiStyleMod)(idx, v); -} -CIMGUI_API void ImGuiStyleMod_destroy(ImGuiStyleMod* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiStyleMod* ImGuiStyleMod_ImGuiStyleMod_Float(ImGuiStyleVar idx, - float v) { - return IM_NEW(ImGuiStyleMod)(idx, v); -} -CIMGUI_API ImGuiStyleMod* ImGuiStyleMod_ImGuiStyleMod_Vec2(ImGuiStyleVar idx, - ImVec2 v) { - return IM_NEW(ImGuiStyleMod)(idx, v); -} -CIMGUI_API ImGuiComboPreviewData* ImGuiComboPreviewData_ImGuiComboPreviewData( - void) { - return IM_NEW(ImGuiComboPreviewData)(); -} -CIMGUI_API void ImGuiComboPreviewData_destroy(ImGuiComboPreviewData* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiMenuColumns* ImGuiMenuColumns_ImGuiMenuColumns(void) { - return IM_NEW(ImGuiMenuColumns)(); -} -CIMGUI_API void ImGuiMenuColumns_destroy(ImGuiMenuColumns* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiMenuColumns_Update(ImGuiMenuColumns* self, - float spacing, - bool window_reappearing) { - return self->Update(spacing, window_reappearing); -} -CIMGUI_API float ImGuiMenuColumns_DeclColumns(ImGuiMenuColumns* self, - float w_icon, - float w_label, - float w_shortcut, - float w_mark) { - return self->DeclColumns(w_icon, w_label, w_shortcut, w_mark); -} -CIMGUI_API void ImGuiMenuColumns_CalcNextTotalWidth(ImGuiMenuColumns* self, - bool update_offsets) { - return self->CalcNextTotalWidth(update_offsets); -} -CIMGUI_API ImGuiInputTextDeactivatedState* -ImGuiInputTextDeactivatedState_ImGuiInputTextDeactivatedState(void) { - return IM_NEW(ImGuiInputTextDeactivatedState)(); -} -CIMGUI_API void ImGuiInputTextDeactivatedState_destroy( - ImGuiInputTextDeactivatedState* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiInputTextDeactivatedState_ClearFreeMemory( - ImGuiInputTextDeactivatedState* self) { - return self->ClearFreeMemory(); -} -CIMGUI_API ImGuiInputTextState* ImGuiInputTextState_ImGuiInputTextState(void) { - return IM_NEW(ImGuiInputTextState)(); -} -CIMGUI_API void ImGuiInputTextState_destroy(ImGuiInputTextState* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiInputTextState_ClearText(ImGuiInputTextState* self) { - return self->ClearText(); -} -CIMGUI_API void ImGuiInputTextState_ClearFreeMemory(ImGuiInputTextState* self) { - return self->ClearFreeMemory(); -} -CIMGUI_API int ImGuiInputTextState_GetUndoAvailCount( - ImGuiInputTextState* self) { - return self->GetUndoAvailCount(); -} -CIMGUI_API int ImGuiInputTextState_GetRedoAvailCount( - ImGuiInputTextState* self) { - return self->GetRedoAvailCount(); -} -CIMGUI_API void ImGuiInputTextState_OnKeyPressed(ImGuiInputTextState* self, - int key) { - return self->OnKeyPressed(key); -} -CIMGUI_API void ImGuiInputTextState_CursorAnimReset(ImGuiInputTextState* self) { - return self->CursorAnimReset(); -} -CIMGUI_API void ImGuiInputTextState_CursorClamp(ImGuiInputTextState* self) { - return self->CursorClamp(); -} -CIMGUI_API bool ImGuiInputTextState_HasSelection(ImGuiInputTextState* self) { - return self->HasSelection(); -} -CIMGUI_API void ImGuiInputTextState_ClearSelection(ImGuiInputTextState* self) { - return self->ClearSelection(); -} -CIMGUI_API int ImGuiInputTextState_GetCursorPos(ImGuiInputTextState* self) { - return self->GetCursorPos(); -} -CIMGUI_API int ImGuiInputTextState_GetSelectionStart( - ImGuiInputTextState* self) { - return self->GetSelectionStart(); -} -CIMGUI_API int ImGuiInputTextState_GetSelectionEnd(ImGuiInputTextState* self) { - return self->GetSelectionEnd(); -} -CIMGUI_API void ImGuiInputTextState_SelectAll(ImGuiInputTextState* self) { - return self->SelectAll(); -} -CIMGUI_API void ImGuiInputTextState_ReloadUserBufAndSelectAll( - ImGuiInputTextState* self) { - return self->ReloadUserBufAndSelectAll(); -} -CIMGUI_API void ImGuiInputTextState_ReloadUserBufAndKeepSelection( - ImGuiInputTextState* self) { - return self->ReloadUserBufAndKeepSelection(); -} -CIMGUI_API void ImGuiInputTextState_ReloadUserBufAndMoveToEnd( - ImGuiInputTextState* self) { - return self->ReloadUserBufAndMoveToEnd(); -} -CIMGUI_API ImGuiNextWindowData* ImGuiNextWindowData_ImGuiNextWindowData(void) { - return IM_NEW(ImGuiNextWindowData)(); -} -CIMGUI_API void ImGuiNextWindowData_destroy(ImGuiNextWindowData* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiNextWindowData_ClearFlags(ImGuiNextWindowData* self) { - return self->ClearFlags(); -} -CIMGUI_API ImGuiNextItemData* ImGuiNextItemData_ImGuiNextItemData(void) { - return IM_NEW(ImGuiNextItemData)(); -} -CIMGUI_API void ImGuiNextItemData_destroy(ImGuiNextItemData* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiNextItemData_ClearFlags(ImGuiNextItemData* self) { - return self->ClearFlags(); -} -CIMGUI_API ImGuiLastItemData* ImGuiLastItemData_ImGuiLastItemData(void) { - return IM_NEW(ImGuiLastItemData)(); -} -CIMGUI_API void ImGuiLastItemData_destroy(ImGuiLastItemData* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiStackSizes* ImGuiStackSizes_ImGuiStackSizes(void) { - return IM_NEW(ImGuiStackSizes)(); -} -CIMGUI_API void ImGuiStackSizes_destroy(ImGuiStackSizes* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiStackSizes_SetToContextState(ImGuiStackSizes* self, - ImGuiContext* ctx) { - return self->SetToContextState(ctx); -} -CIMGUI_API void ImGuiStackSizes_CompareWithContextState(ImGuiStackSizes* self, - ImGuiContext* ctx) { - return self->CompareWithContextState(ctx); -} -CIMGUI_API ImGuiPtrOrIndex* ImGuiPtrOrIndex_ImGuiPtrOrIndex_Ptr(void* ptr) { - return IM_NEW(ImGuiPtrOrIndex)(ptr); -} -CIMGUI_API void ImGuiPtrOrIndex_destroy(ImGuiPtrOrIndex* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiPtrOrIndex* ImGuiPtrOrIndex_ImGuiPtrOrIndex_Int(int index) { - return IM_NEW(ImGuiPtrOrIndex)(index); -} -CIMGUI_API void* ImGuiDataVarInfo_GetVarPtr(ImGuiDataVarInfo* self, - void* parent) { - return self->GetVarPtr(parent); -} -CIMGUI_API ImGuiPopupData* ImGuiPopupData_ImGuiPopupData(void) { - return IM_NEW(ImGuiPopupData)(); -} -CIMGUI_API void ImGuiPopupData_destroy(ImGuiPopupData* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiInputEvent* ImGuiInputEvent_ImGuiInputEvent(void) { - return IM_NEW(ImGuiInputEvent)(); -} -CIMGUI_API void ImGuiInputEvent_destroy(ImGuiInputEvent* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiKeyRoutingData* ImGuiKeyRoutingData_ImGuiKeyRoutingData(void) { - return IM_NEW(ImGuiKeyRoutingData)(); -} -CIMGUI_API void ImGuiKeyRoutingData_destroy(ImGuiKeyRoutingData* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiKeyRoutingTable* ImGuiKeyRoutingTable_ImGuiKeyRoutingTable( - void) { - return IM_NEW(ImGuiKeyRoutingTable)(); -} -CIMGUI_API void ImGuiKeyRoutingTable_destroy(ImGuiKeyRoutingTable* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiKeyRoutingTable_Clear(ImGuiKeyRoutingTable* self) { - return self->Clear(); -} -CIMGUI_API ImGuiKeyOwnerData* ImGuiKeyOwnerData_ImGuiKeyOwnerData(void) { - return IM_NEW(ImGuiKeyOwnerData)(); -} -CIMGUI_API void ImGuiKeyOwnerData_destroy(ImGuiKeyOwnerData* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiListClipperRange ImGuiListClipperRange_FromIndices(int min, - int max) { - return ImGuiListClipperRange::FromIndices(min, max); -} -CIMGUI_API ImGuiListClipperRange -ImGuiListClipperRange_FromPositions(float y1, - float y2, - int off_min, - int off_max) { - return ImGuiListClipperRange::FromPositions(y1, y2, off_min, off_max); -} -CIMGUI_API ImGuiListClipperData* ImGuiListClipperData_ImGuiListClipperData( - void) { - return IM_NEW(ImGuiListClipperData)(); -} -CIMGUI_API void ImGuiListClipperData_destroy(ImGuiListClipperData* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiListClipperData_Reset(ImGuiListClipperData* self, - ImGuiListClipper* clipper) { - return self->Reset(clipper); -} -CIMGUI_API ImGuiNavItemData* ImGuiNavItemData_ImGuiNavItemData(void) { - return IM_NEW(ImGuiNavItemData)(); -} -CIMGUI_API void ImGuiNavItemData_destroy(ImGuiNavItemData* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiNavItemData_Clear(ImGuiNavItemData* self) { - return self->Clear(); -} -CIMGUI_API ImGuiTypingSelectState* -ImGuiTypingSelectState_ImGuiTypingSelectState(void) { - return IM_NEW(ImGuiTypingSelectState)(); -} -CIMGUI_API void ImGuiTypingSelectState_destroy(ImGuiTypingSelectState* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiTypingSelectState_Clear(ImGuiTypingSelectState* self) { - return self->Clear(); -} -CIMGUI_API ImGuiOldColumnData* ImGuiOldColumnData_ImGuiOldColumnData(void) { - return IM_NEW(ImGuiOldColumnData)(); -} -CIMGUI_API void ImGuiOldColumnData_destroy(ImGuiOldColumnData* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiOldColumns* ImGuiOldColumns_ImGuiOldColumns(void) { - return IM_NEW(ImGuiOldColumns)(); -} -CIMGUI_API void ImGuiOldColumns_destroy(ImGuiOldColumns* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiDockNode* ImGuiDockNode_ImGuiDockNode(ImGuiID id) { - return IM_NEW(ImGuiDockNode)(id); -} -CIMGUI_API void ImGuiDockNode_destroy(ImGuiDockNode* self) { - IM_DELETE(self); -} -CIMGUI_API bool ImGuiDockNode_IsRootNode(ImGuiDockNode* self) { - return self->IsRootNode(); -} -CIMGUI_API bool ImGuiDockNode_IsDockSpace(ImGuiDockNode* self) { - return self->IsDockSpace(); -} -CIMGUI_API bool ImGuiDockNode_IsFloatingNode(ImGuiDockNode* self) { - return self->IsFloatingNode(); -} -CIMGUI_API bool ImGuiDockNode_IsCentralNode(ImGuiDockNode* self) { - return self->IsCentralNode(); -} -CIMGUI_API bool ImGuiDockNode_IsHiddenTabBar(ImGuiDockNode* self) { - return self->IsHiddenTabBar(); -} -CIMGUI_API bool ImGuiDockNode_IsNoTabBar(ImGuiDockNode* self) { - return self->IsNoTabBar(); -} -CIMGUI_API bool ImGuiDockNode_IsSplitNode(ImGuiDockNode* self) { - return self->IsSplitNode(); -} -CIMGUI_API bool ImGuiDockNode_IsLeafNode(ImGuiDockNode* self) { - return self->IsLeafNode(); -} -CIMGUI_API bool ImGuiDockNode_IsEmpty(ImGuiDockNode* self) { - return self->IsEmpty(); -} -CIMGUI_API void ImGuiDockNode_Rect(ImRect* pOut, ImGuiDockNode* self) { - *pOut = self->Rect(); -} -CIMGUI_API void ImGuiDockNode_SetLocalFlags(ImGuiDockNode* self, - ImGuiDockNodeFlags flags) { - return self->SetLocalFlags(flags); -} -CIMGUI_API void ImGuiDockNode_UpdateMergedFlags(ImGuiDockNode* self) { - return self->UpdateMergedFlags(); -} -CIMGUI_API ImGuiDockContext* ImGuiDockContext_ImGuiDockContext(void) { - return IM_NEW(ImGuiDockContext)(); -} -CIMGUI_API void ImGuiDockContext_destroy(ImGuiDockContext* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiViewportP* ImGuiViewportP_ImGuiViewportP(void) { - return IM_NEW(ImGuiViewportP)(); -} -CIMGUI_API void ImGuiViewportP_destroy(ImGuiViewportP* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiViewportP_ClearRequestFlags(ImGuiViewportP* self) { - return self->ClearRequestFlags(); -} -CIMGUI_API void ImGuiViewportP_CalcWorkRectPos(ImVec2* pOut, - ImGuiViewportP* self, - const ImVec2 off_min) { - *pOut = self->CalcWorkRectPos(off_min); -} -CIMGUI_API void ImGuiViewportP_CalcWorkRectSize(ImVec2* pOut, - ImGuiViewportP* self, - const ImVec2 off_min, - const ImVec2 off_max) { - *pOut = self->CalcWorkRectSize(off_min, off_max); -} -CIMGUI_API void ImGuiViewportP_UpdateWorkRect(ImGuiViewportP* self) { - return self->UpdateWorkRect(); -} -CIMGUI_API void ImGuiViewportP_GetMainRect(ImRect* pOut, ImGuiViewportP* self) { - *pOut = self->GetMainRect(); -} -CIMGUI_API void ImGuiViewportP_GetWorkRect(ImRect* pOut, ImGuiViewportP* self) { - *pOut = self->GetWorkRect(); -} -CIMGUI_API void ImGuiViewportP_GetBuildWorkRect(ImRect* pOut, - ImGuiViewportP* self) { - *pOut = self->GetBuildWorkRect(); -} -CIMGUI_API ImGuiWindowSettings* ImGuiWindowSettings_ImGuiWindowSettings(void) { - return IM_NEW(ImGuiWindowSettings)(); -} -CIMGUI_API void ImGuiWindowSettings_destroy(ImGuiWindowSettings* self) { - IM_DELETE(self); -} -CIMGUI_API char* ImGuiWindowSettings_GetName(ImGuiWindowSettings* self) { - return self->GetName(); -} -CIMGUI_API ImGuiSettingsHandler* ImGuiSettingsHandler_ImGuiSettingsHandler( - void) { - return IM_NEW(ImGuiSettingsHandler)(); -} -CIMGUI_API void ImGuiSettingsHandler_destroy(ImGuiSettingsHandler* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiDebugAllocInfo* ImGuiDebugAllocInfo_ImGuiDebugAllocInfo(void) { - return IM_NEW(ImGuiDebugAllocInfo)(); -} -CIMGUI_API void ImGuiDebugAllocInfo_destroy(ImGuiDebugAllocInfo* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiStackLevelInfo* ImGuiStackLevelInfo_ImGuiStackLevelInfo(void) { - return IM_NEW(ImGuiStackLevelInfo)(); -} -CIMGUI_API void ImGuiStackLevelInfo_destroy(ImGuiStackLevelInfo* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiIDStackTool* ImGuiIDStackTool_ImGuiIDStackTool(void) { - return IM_NEW(ImGuiIDStackTool)(); -} -CIMGUI_API void ImGuiIDStackTool_destroy(ImGuiIDStackTool* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiContextHook* ImGuiContextHook_ImGuiContextHook(void) { - return IM_NEW(ImGuiContextHook)(); -} -CIMGUI_API void ImGuiContextHook_destroy(ImGuiContextHook* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiContext* ImGuiContext_ImGuiContext( - ImFontAtlas* shared_font_atlas) { - return IM_NEW(ImGuiContext)(shared_font_atlas); -} -CIMGUI_API void ImGuiContext_destroy(ImGuiContext* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiWindow* ImGuiWindow_ImGuiWindow(ImGuiContext* context, - const char* name) { - return IM_NEW(ImGuiWindow)(context, name); -} -CIMGUI_API void ImGuiWindow_destroy(ImGuiWindow* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiID ImGuiWindow_GetID_Str(ImGuiWindow* self, - const char* str, - const char* str_end) { - return self->GetID(str, str_end); -} -CIMGUI_API ImGuiID ImGuiWindow_GetID_Ptr(ImGuiWindow* self, const void* ptr) { - return self->GetID(ptr); -} -CIMGUI_API ImGuiID ImGuiWindow_GetID_Int(ImGuiWindow* self, int n) { - return self->GetID(n); -} -CIMGUI_API ImGuiID ImGuiWindow_GetIDFromRectangle(ImGuiWindow* self, - const ImRect r_abs) { - return self->GetIDFromRectangle(r_abs); -} -CIMGUI_API void ImGuiWindow_Rect(ImRect* pOut, ImGuiWindow* self) { - *pOut = self->Rect(); -} -CIMGUI_API float ImGuiWindow_CalcFontSize(ImGuiWindow* self) { - return self->CalcFontSize(); -} -CIMGUI_API float ImGuiWindow_TitleBarHeight(ImGuiWindow* self) { - return self->TitleBarHeight(); -} -CIMGUI_API void ImGuiWindow_TitleBarRect(ImRect* pOut, ImGuiWindow* self) { - *pOut = self->TitleBarRect(); -} -CIMGUI_API float ImGuiWindow_MenuBarHeight(ImGuiWindow* self) { - return self->MenuBarHeight(); -} -CIMGUI_API void ImGuiWindow_MenuBarRect(ImRect* pOut, ImGuiWindow* self) { - *pOut = self->MenuBarRect(); -} -CIMGUI_API ImGuiTabItem* ImGuiTabItem_ImGuiTabItem(void) { - return IM_NEW(ImGuiTabItem)(); -} -CIMGUI_API void ImGuiTabItem_destroy(ImGuiTabItem* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTabBar* ImGuiTabBar_ImGuiTabBar(void) { - return IM_NEW(ImGuiTabBar)(); -} -CIMGUI_API void ImGuiTabBar_destroy(ImGuiTabBar* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTableColumn* ImGuiTableColumn_ImGuiTableColumn(void) { - return IM_NEW(ImGuiTableColumn)(); -} -CIMGUI_API void ImGuiTableColumn_destroy(ImGuiTableColumn* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTableInstanceData* -ImGuiTableInstanceData_ImGuiTableInstanceData(void) { - return IM_NEW(ImGuiTableInstanceData)(); -} -CIMGUI_API void ImGuiTableInstanceData_destroy(ImGuiTableInstanceData* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTable* ImGuiTable_ImGuiTable(void) { - return IM_NEW(ImGuiTable)(); -} -CIMGUI_API void ImGuiTable_destroy(ImGuiTable* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTableTempData* ImGuiTableTempData_ImGuiTableTempData(void) { - return IM_NEW(ImGuiTableTempData)(); -} -CIMGUI_API void ImGuiTableTempData_destroy(ImGuiTableTempData* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTableColumnSettings* -ImGuiTableColumnSettings_ImGuiTableColumnSettings(void) { - return IM_NEW(ImGuiTableColumnSettings)(); -} -CIMGUI_API void ImGuiTableColumnSettings_destroy( - ImGuiTableColumnSettings* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTableSettings* ImGuiTableSettings_ImGuiTableSettings(void) { - return IM_NEW(ImGuiTableSettings)(); -} -CIMGUI_API void ImGuiTableSettings_destroy(ImGuiTableSettings* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTableColumnSettings* ImGuiTableSettings_GetColumnSettings( - ImGuiTableSettings* self) { - return self->GetColumnSettings(); -} -CIMGUI_API ImGuiWindow* igGetCurrentWindowRead() { - return ImGui::GetCurrentWindowRead(); -} -CIMGUI_API ImGuiWindow* igGetCurrentWindow() { - return ImGui::GetCurrentWindow(); -} -CIMGUI_API ImGuiWindow* igFindWindowByID(ImGuiID id) { - return ImGui::FindWindowByID(id); -} -CIMGUI_API ImGuiWindow* igFindWindowByName(const char* name) { - return ImGui::FindWindowByName(name); -} -CIMGUI_API void igUpdateWindowParentAndRootLinks(ImGuiWindow* window, - ImGuiWindowFlags flags, - ImGuiWindow* parent_window) { - return ImGui::UpdateWindowParentAndRootLinks(window, flags, parent_window); -} -CIMGUI_API void igUpdateWindowSkipRefresh(ImGuiWindow* window) { - return ImGui::UpdateWindowSkipRefresh(window); -} -CIMGUI_API void igCalcWindowNextAutoFitSize(ImVec2* pOut, ImGuiWindow* window) { - *pOut = ImGui::CalcWindowNextAutoFitSize(window); -} -CIMGUI_API bool igIsWindowChildOf(ImGuiWindow* window, - ImGuiWindow* potential_parent, - bool popup_hierarchy, - bool dock_hierarchy) { - return ImGui::IsWindowChildOf(window, potential_parent, popup_hierarchy, - dock_hierarchy); -} -CIMGUI_API bool igIsWindowWithinBeginStackOf(ImGuiWindow* window, - ImGuiWindow* potential_parent) { - return ImGui::IsWindowWithinBeginStackOf(window, potential_parent); -} -CIMGUI_API bool igIsWindowAbove(ImGuiWindow* potential_above, - ImGuiWindow* potential_below) { - return ImGui::IsWindowAbove(potential_above, potential_below); -} -CIMGUI_API bool igIsWindowNavFocusable(ImGuiWindow* window) { - return ImGui::IsWindowNavFocusable(window); -} -CIMGUI_API void igSetWindowPos_WindowPtr(ImGuiWindow* window, - const ImVec2 pos, - ImGuiCond cond) { - return ImGui::SetWindowPos(window, pos, cond); -} -CIMGUI_API void igSetWindowSize_WindowPtr(ImGuiWindow* window, - const ImVec2 size, - ImGuiCond cond) { - return ImGui::SetWindowSize(window, size, cond); -} -CIMGUI_API void igSetWindowCollapsed_WindowPtr(ImGuiWindow* window, - bool collapsed, - ImGuiCond cond) { - return ImGui::SetWindowCollapsed(window, collapsed, cond); -} -CIMGUI_API void igSetWindowHitTestHole(ImGuiWindow* window, - const ImVec2 pos, - const ImVec2 size) { - return ImGui::SetWindowHitTestHole(window, pos, size); -} -CIMGUI_API void igSetWindowHiddenAndSkipItemsForCurrentFrame( - ImGuiWindow* window) { - return ImGui::SetWindowHiddenAndSkipItemsForCurrentFrame(window); -} -CIMGUI_API void igSetWindowParentWindowForFocusRoute( - ImGuiWindow* window, - ImGuiWindow* parent_window) { - return ImGui::SetWindowParentWindowForFocusRoute(window, parent_window); -} -CIMGUI_API void igWindowRectAbsToRel(ImRect* pOut, - ImGuiWindow* window, - const ImRect r) { - *pOut = ImGui::WindowRectAbsToRel(window, r); -} -CIMGUI_API void igWindowRectRelToAbs(ImRect* pOut, - ImGuiWindow* window, - const ImRect r) { - *pOut = ImGui::WindowRectRelToAbs(window, r); -} -CIMGUI_API void igWindowPosRelToAbs(ImVec2* pOut, - ImGuiWindow* window, - const ImVec2 p) { - *pOut = ImGui::WindowPosRelToAbs(window, p); -} -CIMGUI_API void igFocusWindow(ImGuiWindow* window, - ImGuiFocusRequestFlags flags) { - return ImGui::FocusWindow(window, flags); -} -CIMGUI_API void igFocusTopMostWindowUnderOne(ImGuiWindow* under_this_window, - ImGuiWindow* ignore_window, - ImGuiViewport* filter_viewport, - ImGuiFocusRequestFlags flags) { - return ImGui::FocusTopMostWindowUnderOne(under_this_window, ignore_window, - filter_viewport, flags); -} -CIMGUI_API void igBringWindowToFocusFront(ImGuiWindow* window) { - return ImGui::BringWindowToFocusFront(window); -} -CIMGUI_API void igBringWindowToDisplayFront(ImGuiWindow* window) { - return ImGui::BringWindowToDisplayFront(window); -} -CIMGUI_API void igBringWindowToDisplayBack(ImGuiWindow* window) { - return ImGui::BringWindowToDisplayBack(window); -} -CIMGUI_API void igBringWindowToDisplayBehind(ImGuiWindow* window, - ImGuiWindow* above_window) { - return ImGui::BringWindowToDisplayBehind(window, above_window); -} -CIMGUI_API int igFindWindowDisplayIndex(ImGuiWindow* window) { - return ImGui::FindWindowDisplayIndex(window); -} -CIMGUI_API ImGuiWindow* igFindBottomMostVisibleWindowWithinBeginStack( - ImGuiWindow* window) { - return ImGui::FindBottomMostVisibleWindowWithinBeginStack(window); -} -CIMGUI_API void igSetNextWindowRefreshPolicy(ImGuiWindowRefreshFlags flags) { - return ImGui::SetNextWindowRefreshPolicy(flags); -} -CIMGUI_API void igSetCurrentFont(ImFont* font) { - return ImGui::SetCurrentFont(font); -} -CIMGUI_API ImFont* igGetDefaultFont() { - return ImGui::GetDefaultFont(); -} -CIMGUI_API ImDrawList* igGetForegroundDrawList_WindowPtr(ImGuiWindow* window) { - return ImGui::GetForegroundDrawList(window); -} -CIMGUI_API void igAddDrawListToDrawDataEx(ImDrawData* draw_data, - ImVector_ImDrawListPtr* out_list, - ImDrawList* draw_list) { - return ImGui::AddDrawListToDrawDataEx(draw_data, out_list, draw_list); -} -CIMGUI_API void igInitialize() { - return ImGui::Initialize(); -} -CIMGUI_API void igShutdown() { - return ImGui::Shutdown(); -} -CIMGUI_API void igUpdateInputEvents(bool trickle_fast_inputs) { - return ImGui::UpdateInputEvents(trickle_fast_inputs); -} -CIMGUI_API void igUpdateHoveredWindowAndCaptureFlags() { - return ImGui::UpdateHoveredWindowAndCaptureFlags(); -} -CIMGUI_API void igStartMouseMovingWindow(ImGuiWindow* window) { - return ImGui::StartMouseMovingWindow(window); -} -CIMGUI_API void igStartMouseMovingWindowOrNode(ImGuiWindow* window, - ImGuiDockNode* node, - bool undock) { - return ImGui::StartMouseMovingWindowOrNode(window, node, undock); -} -CIMGUI_API void igUpdateMouseMovingWindowNewFrame() { - return ImGui::UpdateMouseMovingWindowNewFrame(); -} -CIMGUI_API void igUpdateMouseMovingWindowEndFrame() { - return ImGui::UpdateMouseMovingWindowEndFrame(); -} -CIMGUI_API ImGuiID igAddContextHook(ImGuiContext* context, - const ImGuiContextHook* hook) { - return ImGui::AddContextHook(context, hook); -} -CIMGUI_API void igRemoveContextHook(ImGuiContext* context, - ImGuiID hook_to_remove) { - return ImGui::RemoveContextHook(context, hook_to_remove); -} -CIMGUI_API void igCallContextHooks(ImGuiContext* context, - ImGuiContextHookType type) { - return ImGui::CallContextHooks(context, type); -} -CIMGUI_API void igTranslateWindowsInViewport(ImGuiViewportP* viewport, - const ImVec2 old_pos, - const ImVec2 new_pos) { - return ImGui::TranslateWindowsInViewport(viewport, old_pos, new_pos); -} -CIMGUI_API void igScaleWindowsInViewport(ImGuiViewportP* viewport, - float scale) { - return ImGui::ScaleWindowsInViewport(viewport, scale); -} -CIMGUI_API void igDestroyPlatformWindow(ImGuiViewportP* viewport) { - return ImGui::DestroyPlatformWindow(viewport); -} -CIMGUI_API void igSetWindowViewport(ImGuiWindow* window, - ImGuiViewportP* viewport) { - return ImGui::SetWindowViewport(window, viewport); -} -CIMGUI_API void igSetCurrentViewport(ImGuiWindow* window, - ImGuiViewportP* viewport) { - return ImGui::SetCurrentViewport(window, viewport); -} -CIMGUI_API const ImGuiPlatformMonitor* igGetViewportPlatformMonitor( - ImGuiViewport* viewport) { - return ImGui::GetViewportPlatformMonitor(viewport); -} -CIMGUI_API ImGuiViewportP* igFindHoveredViewportFromPlatformWindowStack( - const ImVec2 mouse_platform_pos) { - return ImGui::FindHoveredViewportFromPlatformWindowStack(mouse_platform_pos); -} -CIMGUI_API void igMarkIniSettingsDirty_Nil() { - return ImGui::MarkIniSettingsDirty(); -} -CIMGUI_API void igMarkIniSettingsDirty_WindowPtr(ImGuiWindow* window) { - return ImGui::MarkIniSettingsDirty(window); -} -CIMGUI_API void igClearIniSettings() { - return ImGui::ClearIniSettings(); -} -CIMGUI_API void igAddSettingsHandler(const ImGuiSettingsHandler* handler) { - return ImGui::AddSettingsHandler(handler); -} -CIMGUI_API void igRemoveSettingsHandler(const char* type_name) { - return ImGui::RemoveSettingsHandler(type_name); -} -CIMGUI_API ImGuiSettingsHandler* igFindSettingsHandler(const char* type_name) { - return ImGui::FindSettingsHandler(type_name); -} -CIMGUI_API ImGuiWindowSettings* igCreateNewWindowSettings(const char* name) { - return ImGui::CreateNewWindowSettings(name); -} -CIMGUI_API ImGuiWindowSettings* igFindWindowSettingsByID(ImGuiID id) { - return ImGui::FindWindowSettingsByID(id); -} -CIMGUI_API ImGuiWindowSettings* igFindWindowSettingsByWindow( - ImGuiWindow* window) { - return ImGui::FindWindowSettingsByWindow(window); -} -CIMGUI_API void igClearWindowSettings(const char* name) { - return ImGui::ClearWindowSettings(name); -} -CIMGUI_API void igLocalizeRegisterEntries(const ImGuiLocEntry* entries, - int count) { - return ImGui::LocalizeRegisterEntries(entries, count); -} -CIMGUI_API const char* igLocalizeGetMsg(ImGuiLocKey key) { - return ImGui::LocalizeGetMsg(key); -} -CIMGUI_API void igSetScrollX_WindowPtr(ImGuiWindow* window, float scroll_x) { - return ImGui::SetScrollX(window, scroll_x); -} -CIMGUI_API void igSetScrollY_WindowPtr(ImGuiWindow* window, float scroll_y) { - return ImGui::SetScrollY(window, scroll_y); -} -CIMGUI_API void igSetScrollFromPosX_WindowPtr(ImGuiWindow* window, - float local_x, - float center_x_ratio) { - return ImGui::SetScrollFromPosX(window, local_x, center_x_ratio); -} -CIMGUI_API void igSetScrollFromPosY_WindowPtr(ImGuiWindow* window, - float local_y, - float center_y_ratio) { - return ImGui::SetScrollFromPosY(window, local_y, center_y_ratio); -} -CIMGUI_API void igScrollToItem(ImGuiScrollFlags flags) { - return ImGui::ScrollToItem(flags); -} -CIMGUI_API void igScrollToRect(ImGuiWindow* window, - const ImRect rect, - ImGuiScrollFlags flags) { - return ImGui::ScrollToRect(window, rect, flags); -} -CIMGUI_API void igScrollToRectEx(ImVec2* pOut, - ImGuiWindow* window, - const ImRect rect, - ImGuiScrollFlags flags) { - *pOut = ImGui::ScrollToRectEx(window, rect, flags); -} -CIMGUI_API void igScrollToBringRectIntoView(ImGuiWindow* window, - const ImRect rect) { - return ImGui::ScrollToBringRectIntoView(window, rect); -} -CIMGUI_API ImGuiItemStatusFlags igGetItemStatusFlags() { - return ImGui::GetItemStatusFlags(); -} -CIMGUI_API ImGuiItemFlags igGetItemFlags() { - return ImGui::GetItemFlags(); -} -CIMGUI_API ImGuiID igGetActiveID() { - return ImGui::GetActiveID(); -} -CIMGUI_API ImGuiID igGetFocusID() { - return ImGui::GetFocusID(); -} -CIMGUI_API void igSetActiveID(ImGuiID id, ImGuiWindow* window) { - return ImGui::SetActiveID(id, window); -} -CIMGUI_API void igSetFocusID(ImGuiID id, ImGuiWindow* window) { - return ImGui::SetFocusID(id, window); -} -CIMGUI_API void igClearActiveID() { - return ImGui::ClearActiveID(); -} -CIMGUI_API ImGuiID igGetHoveredID() { - return ImGui::GetHoveredID(); -} -CIMGUI_API void igSetHoveredID(ImGuiID id) { - return ImGui::SetHoveredID(id); -} -CIMGUI_API void igKeepAliveID(ImGuiID id) { - return ImGui::KeepAliveID(id); -} -CIMGUI_API void igMarkItemEdited(ImGuiID id) { - return ImGui::MarkItemEdited(id); -} -CIMGUI_API void igPushOverrideID(ImGuiID id) { - return ImGui::PushOverrideID(id); -} -CIMGUI_API ImGuiID igGetIDWithSeed_Str(const char* str_id_begin, - const char* str_id_end, - ImGuiID seed) { - return ImGui::GetIDWithSeed(str_id_begin, str_id_end, seed); -} -CIMGUI_API ImGuiID igGetIDWithSeed_Int(int n, ImGuiID seed) { - return ImGui::GetIDWithSeed(n, seed); -} -CIMGUI_API void igItemSize_Vec2(const ImVec2 size, float text_baseline_y) { - return ImGui::ItemSize(size, text_baseline_y); -} -CIMGUI_API void igItemSize_Rect(const ImRect bb, float text_baseline_y) { - return ImGui::ItemSize(bb, text_baseline_y); -} -CIMGUI_API bool igItemAdd(const ImRect bb, - ImGuiID id, - const ImRect* nav_bb, - ImGuiItemFlags extra_flags) { - return ImGui::ItemAdd(bb, id, nav_bb, extra_flags); -} -CIMGUI_API bool igItemHoverable(const ImRect bb, - ImGuiID id, - ImGuiItemFlags item_flags) { - return ImGui::ItemHoverable(bb, id, item_flags); -} -CIMGUI_API bool igIsWindowContentHoverable(ImGuiWindow* window, - ImGuiHoveredFlags flags) { - return ImGui::IsWindowContentHoverable(window, flags); -} -CIMGUI_API bool igIsClippedEx(const ImRect bb, ImGuiID id) { - return ImGui::IsClippedEx(bb, id); -} -CIMGUI_API void igSetLastItemData(ImGuiID item_id, - ImGuiItemFlags in_flags, - ImGuiItemStatusFlags status_flags, - const ImRect item_rect) { - return ImGui::SetLastItemData(item_id, in_flags, status_flags, item_rect); -} -CIMGUI_API void igCalcItemSize(ImVec2* pOut, - ImVec2 size, - float default_w, - float default_h) { - *pOut = ImGui::CalcItemSize(size, default_w, default_h); -} -CIMGUI_API float igCalcWrapWidthForPos(const ImVec2 pos, float wrap_pos_x) { - return ImGui::CalcWrapWidthForPos(pos, wrap_pos_x); -} -CIMGUI_API void igPushMultiItemsWidths(int components, float width_full) { - return ImGui::PushMultiItemsWidths(components, width_full); -} -CIMGUI_API bool igIsItemToggledSelection() { - return ImGui::IsItemToggledSelection(); -} -CIMGUI_API void igGetContentRegionMaxAbs(ImVec2* pOut) { - *pOut = ImGui::GetContentRegionMaxAbs(); -} -CIMGUI_API void igShrinkWidths(ImGuiShrinkWidthItem* items, - int count, - float width_excess) { - return ImGui::ShrinkWidths(items, count, width_excess); -} -CIMGUI_API void igPushItemFlag(ImGuiItemFlags option, bool enabled) { - return ImGui::PushItemFlag(option, enabled); -} -CIMGUI_API void igPopItemFlag() { - return ImGui::PopItemFlag(); -} -CIMGUI_API const ImGuiDataVarInfo* igGetStyleVarInfo(ImGuiStyleVar idx) { - return ImGui::GetStyleVarInfo(idx); -} -CIMGUI_API void igLogBegin(ImGuiLogType type, int auto_open_depth) { - return ImGui::LogBegin(type, auto_open_depth); -} -CIMGUI_API void igLogToBuffer(int auto_open_depth) { - return ImGui::LogToBuffer(auto_open_depth); -} -CIMGUI_API void igLogRenderedText(const ImVec2* ref_pos, - const char* text, - const char* text_end) { - return ImGui::LogRenderedText(ref_pos, text, text_end); -} -CIMGUI_API void igLogSetNextTextDecoration(const char* prefix, - const char* suffix) { - return ImGui::LogSetNextTextDecoration(prefix, suffix); -} -CIMGUI_API bool igBeginChildEx(const char* name, - ImGuiID id, - const ImVec2 size_arg, - ImGuiChildFlags child_flags, - ImGuiWindowFlags window_flags) { - return ImGui::BeginChildEx(name, id, size_arg, child_flags, window_flags); -} -CIMGUI_API void igOpenPopupEx(ImGuiID id, ImGuiPopupFlags popup_flags) { - return ImGui::OpenPopupEx(id, popup_flags); -} -CIMGUI_API void igClosePopupToLevel(int remaining, - bool restore_focus_to_window_under_popup) { - return ImGui::ClosePopupToLevel(remaining, - restore_focus_to_window_under_popup); -} -CIMGUI_API void igClosePopupsOverWindow( - ImGuiWindow* ref_window, - bool restore_focus_to_window_under_popup) { - return ImGui::ClosePopupsOverWindow(ref_window, - restore_focus_to_window_under_popup); -} -CIMGUI_API void igClosePopupsExceptModals() { - return ImGui::ClosePopupsExceptModals(); -} -CIMGUI_API bool igIsPopupOpen_ID(ImGuiID id, ImGuiPopupFlags popup_flags) { - return ImGui::IsPopupOpen(id, popup_flags); -} -CIMGUI_API bool igBeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_flags) { - return ImGui::BeginPopupEx(id, extra_flags); -} -CIMGUI_API bool igBeginTooltipEx(ImGuiTooltipFlags tooltip_flags, - ImGuiWindowFlags extra_window_flags) { - return ImGui::BeginTooltipEx(tooltip_flags, extra_window_flags); -} -CIMGUI_API bool igBeginTooltipHidden() { - return ImGui::BeginTooltipHidden(); -} -CIMGUI_API void igGetPopupAllowedExtentRect(ImRect* pOut, ImGuiWindow* window) { - *pOut = ImGui::GetPopupAllowedExtentRect(window); -} -CIMGUI_API ImGuiWindow* igGetTopMostPopupModal() { - return ImGui::GetTopMostPopupModal(); -} -CIMGUI_API ImGuiWindow* igGetTopMostAndVisiblePopupModal() { - return ImGui::GetTopMostAndVisiblePopupModal(); -} -CIMGUI_API ImGuiWindow* igFindBlockingModal(ImGuiWindow* window) { - return ImGui::FindBlockingModal(window); -} -CIMGUI_API void igFindBestWindowPosForPopup(ImVec2* pOut, ImGuiWindow* window) { - *pOut = ImGui::FindBestWindowPosForPopup(window); -} -CIMGUI_API void igFindBestWindowPosForPopupEx(ImVec2* pOut, - const ImVec2 ref_pos, - const ImVec2 size, - ImGuiDir* last_dir, - const ImRect r_outer, - const ImRect r_avoid, - ImGuiPopupPositionPolicy policy) { - *pOut = ImGui::FindBestWindowPosForPopupEx(ref_pos, size, last_dir, r_outer, - r_avoid, policy); -} -CIMGUI_API bool igBeginViewportSideBar(const char* name, - ImGuiViewport* viewport, - ImGuiDir dir, - float size, - ImGuiWindowFlags window_flags) { - return ImGui::BeginViewportSideBar(name, viewport, dir, size, window_flags); -} -CIMGUI_API bool igBeginMenuEx(const char* label, - const char* icon, - bool enabled) { - return ImGui::BeginMenuEx(label, icon, enabled); -} -CIMGUI_API bool igMenuItemEx(const char* label, - const char* icon, - const char* shortcut, - bool selected, - bool enabled) { - return ImGui::MenuItemEx(label, icon, shortcut, selected, enabled); -} -CIMGUI_API bool igBeginComboPopup(ImGuiID popup_id, - const ImRect bb, - ImGuiComboFlags flags) { - return ImGui::BeginComboPopup(popup_id, bb, flags); -} -CIMGUI_API bool igBeginComboPreview() { - return ImGui::BeginComboPreview(); -} -CIMGUI_API void igEndComboPreview() { - return ImGui::EndComboPreview(); -} -CIMGUI_API void igNavInitWindow(ImGuiWindow* window, bool force_reinit) { - return ImGui::NavInitWindow(window, force_reinit); -} -CIMGUI_API void igNavInitRequestApplyResult() { - return ImGui::NavInitRequestApplyResult(); -} -CIMGUI_API bool igNavMoveRequestButNoResultYet() { - return ImGui::NavMoveRequestButNoResultYet(); -} -CIMGUI_API void igNavMoveRequestSubmit(ImGuiDir move_dir, - ImGuiDir clip_dir, - ImGuiNavMoveFlags move_flags, - ImGuiScrollFlags scroll_flags) { - return ImGui::NavMoveRequestSubmit(move_dir, clip_dir, move_flags, - scroll_flags); -} -CIMGUI_API void igNavMoveRequestForward(ImGuiDir move_dir, - ImGuiDir clip_dir, - ImGuiNavMoveFlags move_flags, - ImGuiScrollFlags scroll_flags) { - return ImGui::NavMoveRequestForward(move_dir, clip_dir, move_flags, - scroll_flags); -} -CIMGUI_API void igNavMoveRequestResolveWithLastItem(ImGuiNavItemData* result) { - return ImGui::NavMoveRequestResolveWithLastItem(result); -} -CIMGUI_API void igNavMoveRequestResolveWithPastTreeNode( - ImGuiNavItemData* result, - ImGuiNavTreeNodeData* tree_node_data) { - return ImGui::NavMoveRequestResolveWithPastTreeNode(result, tree_node_data); -} -CIMGUI_API void igNavMoveRequestCancel() { - return ImGui::NavMoveRequestCancel(); -} -CIMGUI_API void igNavMoveRequestApplyResult() { - return ImGui::NavMoveRequestApplyResult(); -} -CIMGUI_API void igNavMoveRequestTryWrapping(ImGuiWindow* window, - ImGuiNavMoveFlags move_flags) { - return ImGui::NavMoveRequestTryWrapping(window, move_flags); -} -CIMGUI_API void igNavHighlightActivated(ImGuiID id) { - return ImGui::NavHighlightActivated(id); -} -CIMGUI_API void igNavClearPreferredPosForAxis(ImGuiAxis axis) { - return ImGui::NavClearPreferredPosForAxis(axis); -} -CIMGUI_API void igNavRestoreHighlightAfterMove() { - return ImGui::NavRestoreHighlightAfterMove(); -} -CIMGUI_API void igNavUpdateCurrentWindowIsScrollPushableX() { - return ImGui::NavUpdateCurrentWindowIsScrollPushableX(); -} -CIMGUI_API void igSetNavWindow(ImGuiWindow* window) { - return ImGui::SetNavWindow(window); -} -CIMGUI_API void igSetNavID(ImGuiID id, - ImGuiNavLayer nav_layer, - ImGuiID focus_scope_id, - const ImRect rect_rel) { - return ImGui::SetNavID(id, nav_layer, focus_scope_id, rect_rel); -} -CIMGUI_API void igSetNavFocusScope(ImGuiID focus_scope_id) { - return ImGui::SetNavFocusScope(focus_scope_id); -} -CIMGUI_API void igFocusItem() { - return ImGui::FocusItem(); -} -CIMGUI_API void igActivateItemByID(ImGuiID id) { - return ImGui::ActivateItemByID(id); -} -CIMGUI_API bool igIsNamedKey(ImGuiKey key) { - return ImGui::IsNamedKey(key); -} -CIMGUI_API bool igIsNamedKeyOrModKey(ImGuiKey key) { - return ImGui::IsNamedKeyOrModKey(key); -} -CIMGUI_API bool igIsLegacyKey(ImGuiKey key) { - return ImGui::IsLegacyKey(key); -} -CIMGUI_API bool igIsKeyboardKey(ImGuiKey key) { - return ImGui::IsKeyboardKey(key); -} -CIMGUI_API bool igIsGamepadKey(ImGuiKey key) { - return ImGui::IsGamepadKey(key); -} -CIMGUI_API bool igIsMouseKey(ImGuiKey key) { - return ImGui::IsMouseKey(key); -} -CIMGUI_API bool igIsAliasKey(ImGuiKey key) { - return ImGui::IsAliasKey(key); -} -CIMGUI_API bool igIsModKey(ImGuiKey key) { - return ImGui::IsModKey(key); -} -CIMGUI_API ImGuiKeyChord igFixupKeyChord(ImGuiContext* ctx, - ImGuiKeyChord key_chord) { - return ImGui::FixupKeyChord(ctx, key_chord); -} -CIMGUI_API ImGuiKey igConvertSingleModFlagToKey(ImGuiContext* ctx, - ImGuiKey key) { - return ImGui::ConvertSingleModFlagToKey(ctx, key); -} -CIMGUI_API ImGuiKeyData* igGetKeyData_ContextPtr(ImGuiContext* ctx, - ImGuiKey key) { - return ImGui::GetKeyData(ctx, key); -} -CIMGUI_API ImGuiKeyData* igGetKeyData_Key(ImGuiKey key) { - return ImGui::GetKeyData(key); -} -CIMGUI_API const char* igGetKeyChordName(ImGuiKeyChord key_chord) { - return ImGui::GetKeyChordName(key_chord); -} -CIMGUI_API ImGuiKey igMouseButtonToKey(ImGuiMouseButton button) { - return ImGui::MouseButtonToKey(button); -} -CIMGUI_API bool igIsMouseDragPastThreshold(ImGuiMouseButton button, - float lock_threshold) { - return ImGui::IsMouseDragPastThreshold(button, lock_threshold); -} -CIMGUI_API void igGetKeyMagnitude2d(ImVec2* pOut, - ImGuiKey key_left, - ImGuiKey key_right, - ImGuiKey key_up, - ImGuiKey key_down) { - *pOut = ImGui::GetKeyMagnitude2d(key_left, key_right, key_up, key_down); -} -CIMGUI_API float igGetNavTweakPressedAmount(ImGuiAxis axis) { - return ImGui::GetNavTweakPressedAmount(axis); -} -CIMGUI_API int igCalcTypematicRepeatAmount(float t0, - float t1, - float repeat_delay, - float repeat_rate) { - return ImGui::CalcTypematicRepeatAmount(t0, t1, repeat_delay, repeat_rate); -} -CIMGUI_API void igGetTypematicRepeatRate(ImGuiInputFlags flags, - float* repeat_delay, - float* repeat_rate) { - return ImGui::GetTypematicRepeatRate(flags, repeat_delay, repeat_rate); -} -CIMGUI_API void igTeleportMousePos(const ImVec2 pos) { - return ImGui::TeleportMousePos(pos); -} -CIMGUI_API void igSetActiveIdUsingAllKeyboardKeys() { - return ImGui::SetActiveIdUsingAllKeyboardKeys(); -} -CIMGUI_API bool igIsActiveIdUsingNavDir(ImGuiDir dir) { - return ImGui::IsActiveIdUsingNavDir(dir); -} -CIMGUI_API ImGuiID igGetKeyOwner(ImGuiKey key) { - return ImGui::GetKeyOwner(key); -} -CIMGUI_API void igSetKeyOwner(ImGuiKey key, - ImGuiID owner_id, - ImGuiInputFlags flags) { - return ImGui::SetKeyOwner(key, owner_id, flags); -} -CIMGUI_API void igSetKeyOwnersForKeyChord(ImGuiKeyChord key, - ImGuiID owner_id, - ImGuiInputFlags flags) { - return ImGui::SetKeyOwnersForKeyChord(key, owner_id, flags); -} -CIMGUI_API void igSetItemKeyOwner(ImGuiKey key, ImGuiInputFlags flags) { - return ImGui::SetItemKeyOwner(key, flags); -} -CIMGUI_API bool igTestKeyOwner(ImGuiKey key, ImGuiID owner_id) { - return ImGui::TestKeyOwner(key, owner_id); -} -CIMGUI_API ImGuiKeyOwnerData* igGetKeyOwnerData(ImGuiContext* ctx, - ImGuiKey key) { - return ImGui::GetKeyOwnerData(ctx, key); -} -CIMGUI_API bool igIsKeyDown_ID(ImGuiKey key, ImGuiID owner_id) { - return ImGui::IsKeyDown(key, owner_id); -} -CIMGUI_API bool igIsKeyPressed_ID(ImGuiKey key, - ImGuiID owner_id, - ImGuiInputFlags flags) { - return ImGui::IsKeyPressed(key, owner_id, flags); -} -CIMGUI_API bool igIsKeyReleased_ID(ImGuiKey key, ImGuiID owner_id) { - return ImGui::IsKeyReleased(key, owner_id); -} -CIMGUI_API bool igIsMouseDown_ID(ImGuiMouseButton button, ImGuiID owner_id) { - return ImGui::IsMouseDown(button, owner_id); -} -CIMGUI_API bool igIsMouseClicked_ID(ImGuiMouseButton button, - ImGuiID owner_id, - ImGuiInputFlags flags) { - return ImGui::IsMouseClicked(button, owner_id, flags); -} -CIMGUI_API bool igIsMouseReleased_ID(ImGuiMouseButton button, - ImGuiID owner_id) { - return ImGui::IsMouseReleased(button, owner_id); -} -CIMGUI_API bool igIsMouseDoubleClicked_ID(ImGuiMouseButton button, - ImGuiID owner_id) { - return ImGui::IsMouseDoubleClicked(button, owner_id); -} -CIMGUI_API bool igIsKeyChordPressed_ID(ImGuiKeyChord key_chord, - ImGuiID owner_id, - ImGuiInputFlags flags) { - return ImGui::IsKeyChordPressed(key_chord, owner_id, flags); -} -CIMGUI_API void igSetNextItemShortcut(ImGuiKeyChord key_chord) { - return ImGui::SetNextItemShortcut(key_chord); -} -CIMGUI_API bool igShortcut(ImGuiKeyChord key_chord, - ImGuiID owner_id, - ImGuiInputFlags flags) { - return ImGui::Shortcut(key_chord, owner_id, flags); -} -CIMGUI_API bool igSetShortcutRouting(ImGuiKeyChord key_chord, - ImGuiID owner_id, - ImGuiInputFlags flags) { - return ImGui::SetShortcutRouting(key_chord, owner_id, flags); -} -CIMGUI_API bool igTestShortcutRouting(ImGuiKeyChord key_chord, - ImGuiID owner_id) { - return ImGui::TestShortcutRouting(key_chord, owner_id); -} -CIMGUI_API ImGuiKeyRoutingData* igGetShortcutRoutingData( - ImGuiKeyChord key_chord) { - return ImGui::GetShortcutRoutingData(key_chord); -} -CIMGUI_API void igDockContextInitialize(ImGuiContext* ctx) { - return ImGui::DockContextInitialize(ctx); -} -CIMGUI_API void igDockContextShutdown(ImGuiContext* ctx) { - return ImGui::DockContextShutdown(ctx); -} -CIMGUI_API void igDockContextClearNodes(ImGuiContext* ctx, - ImGuiID root_id, - bool clear_settings_refs) { - return ImGui::DockContextClearNodes(ctx, root_id, clear_settings_refs); -} -CIMGUI_API void igDockContextRebuildNodes(ImGuiContext* ctx) { - return ImGui::DockContextRebuildNodes(ctx); -} -CIMGUI_API void igDockContextNewFrameUpdateUndocking(ImGuiContext* ctx) { - return ImGui::DockContextNewFrameUpdateUndocking(ctx); -} -CIMGUI_API void igDockContextNewFrameUpdateDocking(ImGuiContext* ctx) { - return ImGui::DockContextNewFrameUpdateDocking(ctx); -} -CIMGUI_API void igDockContextEndFrame(ImGuiContext* ctx) { - return ImGui::DockContextEndFrame(ctx); -} -CIMGUI_API ImGuiID igDockContextGenNodeID(ImGuiContext* ctx) { - return ImGui::DockContextGenNodeID(ctx); -} -CIMGUI_API void igDockContextQueueDock(ImGuiContext* ctx, - ImGuiWindow* target, - ImGuiDockNode* target_node, - ImGuiWindow* payload, - ImGuiDir split_dir, - float split_ratio, - bool split_outer) { - return ImGui::DockContextQueueDock(ctx, target, target_node, payload, - split_dir, split_ratio, split_outer); -} -CIMGUI_API void igDockContextQueueUndockWindow(ImGuiContext* ctx, - ImGuiWindow* window) { - return ImGui::DockContextQueueUndockWindow(ctx, window); -} -CIMGUI_API void igDockContextQueueUndockNode(ImGuiContext* ctx, - ImGuiDockNode* node) { - return ImGui::DockContextQueueUndockNode(ctx, node); -} -CIMGUI_API void igDockContextProcessUndockWindow( - ImGuiContext* ctx, - ImGuiWindow* window, - bool clear_persistent_docking_ref) { - return ImGui::DockContextProcessUndockWindow(ctx, window, - clear_persistent_docking_ref); -} -CIMGUI_API void igDockContextProcessUndockNode(ImGuiContext* ctx, - ImGuiDockNode* node) { - return ImGui::DockContextProcessUndockNode(ctx, node); -} -CIMGUI_API bool igDockContextCalcDropPosForDocking(ImGuiWindow* target, - ImGuiDockNode* target_node, - ImGuiWindow* payload_window, - ImGuiDockNode* payload_node, - ImGuiDir split_dir, - bool split_outer, - ImVec2* out_pos) { - return ImGui::DockContextCalcDropPosForDocking( - target, target_node, payload_window, payload_node, split_dir, split_outer, - out_pos); -} -CIMGUI_API ImGuiDockNode* igDockContextFindNodeByID(ImGuiContext* ctx, - ImGuiID id) { - return ImGui::DockContextFindNodeByID(ctx, id); -} -CIMGUI_API void igDockNodeWindowMenuHandler_Default(ImGuiContext* ctx, - ImGuiDockNode* node, - ImGuiTabBar* tab_bar) { - return ImGui::DockNodeWindowMenuHandler_Default(ctx, node, tab_bar); -} -CIMGUI_API bool igDockNodeBeginAmendTabBar(ImGuiDockNode* node) { - return ImGui::DockNodeBeginAmendTabBar(node); -} -CIMGUI_API void igDockNodeEndAmendTabBar() { - return ImGui::DockNodeEndAmendTabBar(); -} -CIMGUI_API ImGuiDockNode* igDockNodeGetRootNode(ImGuiDockNode* node) { - return ImGui::DockNodeGetRootNode(node); -} -CIMGUI_API bool igDockNodeIsInHierarchyOf(ImGuiDockNode* node, - ImGuiDockNode* parent) { - return ImGui::DockNodeIsInHierarchyOf(node, parent); -} -CIMGUI_API int igDockNodeGetDepth(const ImGuiDockNode* node) { - return ImGui::DockNodeGetDepth(node); -} -CIMGUI_API ImGuiID igDockNodeGetWindowMenuButtonId(const ImGuiDockNode* node) { - return ImGui::DockNodeGetWindowMenuButtonId(node); -} -CIMGUI_API ImGuiDockNode* igGetWindowDockNode() { - return ImGui::GetWindowDockNode(); -} -CIMGUI_API bool igGetWindowAlwaysWantOwnTabBar(ImGuiWindow* window) { - return ImGui::GetWindowAlwaysWantOwnTabBar(window); -} -CIMGUI_API void igBeginDocked(ImGuiWindow* window, bool* p_open) { - return ImGui::BeginDocked(window, p_open); -} -CIMGUI_API void igBeginDockableDragDropSource(ImGuiWindow* window) { - return ImGui::BeginDockableDragDropSource(window); -} -CIMGUI_API void igBeginDockableDragDropTarget(ImGuiWindow* window) { - return ImGui::BeginDockableDragDropTarget(window); -} -CIMGUI_API void igSetWindowDock(ImGuiWindow* window, - ImGuiID dock_id, - ImGuiCond cond) { - return ImGui::SetWindowDock(window, dock_id, cond); -} -CIMGUI_API void igDockBuilderDockWindow(const char* window_name, - ImGuiID node_id) { - return ImGui::DockBuilderDockWindow(window_name, node_id); -} -CIMGUI_API ImGuiDockNode* igDockBuilderGetNode(ImGuiID node_id) { - return ImGui::DockBuilderGetNode(node_id); -} -CIMGUI_API ImGuiDockNode* igDockBuilderGetCentralNode(ImGuiID node_id) { - return ImGui::DockBuilderGetCentralNode(node_id); -} -CIMGUI_API ImGuiID igDockBuilderAddNode(ImGuiID node_id, - ImGuiDockNodeFlags flags) { - return ImGui::DockBuilderAddNode(node_id, flags); -} -CIMGUI_API void igDockBuilderRemoveNode(ImGuiID node_id) { - return ImGui::DockBuilderRemoveNode(node_id); -} -CIMGUI_API void igDockBuilderRemoveNodeDockedWindows(ImGuiID node_id, - bool clear_settings_refs) { - return ImGui::DockBuilderRemoveNodeDockedWindows(node_id, - clear_settings_refs); -} -CIMGUI_API void igDockBuilderRemoveNodeChildNodes(ImGuiID node_id) { - return ImGui::DockBuilderRemoveNodeChildNodes(node_id); -} -CIMGUI_API void igDockBuilderSetNodePos(ImGuiID node_id, ImVec2 pos) { - return ImGui::DockBuilderSetNodePos(node_id, pos); -} -CIMGUI_API void igDockBuilderSetNodeSize(ImGuiID node_id, ImVec2 size) { - return ImGui::DockBuilderSetNodeSize(node_id, size); -} -CIMGUI_API ImGuiID igDockBuilderSplitNode(ImGuiID node_id, - ImGuiDir split_dir, - float size_ratio_for_node_at_dir, - ImGuiID* out_id_at_dir, - ImGuiID* out_id_at_opposite_dir) { - return ImGui::DockBuilderSplitNode(node_id, split_dir, - size_ratio_for_node_at_dir, out_id_at_dir, - out_id_at_opposite_dir); -} -CIMGUI_API void igDockBuilderCopyDockSpace( - ImGuiID src_dockspace_id, - ImGuiID dst_dockspace_id, - ImVector_const_charPtr* in_window_remap_pairs) { - return ImGui::DockBuilderCopyDockSpace(src_dockspace_id, dst_dockspace_id, - in_window_remap_pairs); -} -CIMGUI_API void igDockBuilderCopyNode(ImGuiID src_node_id, - ImGuiID dst_node_id, - ImVector_ImGuiID* out_node_remap_pairs) { - return ImGui::DockBuilderCopyNode(src_node_id, dst_node_id, - out_node_remap_pairs); -} -CIMGUI_API void igDockBuilderCopyWindowSettings(const char* src_name, - const char* dst_name) { - return ImGui::DockBuilderCopyWindowSettings(src_name, dst_name); -} -CIMGUI_API void igDockBuilderFinish(ImGuiID node_id) { - return ImGui::DockBuilderFinish(node_id); -} -CIMGUI_API void igPushFocusScope(ImGuiID id) { - return ImGui::PushFocusScope(id); -} -CIMGUI_API void igPopFocusScope() { - return ImGui::PopFocusScope(); -} -CIMGUI_API ImGuiID igGetCurrentFocusScope() { - return ImGui::GetCurrentFocusScope(); -} -CIMGUI_API bool igIsDragDropActive() { - return ImGui::IsDragDropActive(); -} -CIMGUI_API bool igBeginDragDropTargetCustom(const ImRect bb, ImGuiID id) { - return ImGui::BeginDragDropTargetCustom(bb, id); -} -CIMGUI_API void igClearDragDrop() { - return ImGui::ClearDragDrop(); -} -CIMGUI_API bool igIsDragDropPayloadBeingAccepted() { - return ImGui::IsDragDropPayloadBeingAccepted(); -} -CIMGUI_API void igRenderDragDropTargetRect(const ImRect bb, - const ImRect item_clip_rect) { - return ImGui::RenderDragDropTargetRect(bb, item_clip_rect); -} -CIMGUI_API ImGuiTypingSelectRequest* igGetTypingSelectRequest( - ImGuiTypingSelectFlags flags) { - return ImGui::GetTypingSelectRequest(flags); -} -CIMGUI_API int igTypingSelectFindMatch(ImGuiTypingSelectRequest* req, - int items_count, - const char* (*get_item_name_func)(void*, - int), - void* user_data, - int nav_item_idx) { - return ImGui::TypingSelectFindMatch(req, items_count, get_item_name_func, - user_data, nav_item_idx); -} -CIMGUI_API int igTypingSelectFindNextSingleCharMatch( - ImGuiTypingSelectRequest* req, - int items_count, - const char* (*get_item_name_func)(void*, int), - void* user_data, - int nav_item_idx) { - return ImGui::TypingSelectFindNextSingleCharMatch( - req, items_count, get_item_name_func, user_data, nav_item_idx); -} -CIMGUI_API int igTypingSelectFindBestLeadingMatch( - ImGuiTypingSelectRequest* req, - int items_count, - const char* (*get_item_name_func)(void*, int), - void* user_data) { - return ImGui::TypingSelectFindBestLeadingMatch(req, items_count, - get_item_name_func, user_data); -} -CIMGUI_API void igSetWindowClipRectBeforeSetChannel(ImGuiWindow* window, - const ImRect clip_rect) { - return ImGui::SetWindowClipRectBeforeSetChannel(window, clip_rect); -} -CIMGUI_API void igBeginColumns(const char* str_id, - int count, - ImGuiOldColumnFlags flags) { - return ImGui::BeginColumns(str_id, count, flags); -} -CIMGUI_API void igEndColumns() { - return ImGui::EndColumns(); -} -CIMGUI_API void igPushColumnClipRect(int column_index) { - return ImGui::PushColumnClipRect(column_index); -} -CIMGUI_API void igPushColumnsBackground() { - return ImGui::PushColumnsBackground(); -} -CIMGUI_API void igPopColumnsBackground() { - return ImGui::PopColumnsBackground(); -} -CIMGUI_API ImGuiID igGetColumnsID(const char* str_id, int count) { - return ImGui::GetColumnsID(str_id, count); -} -CIMGUI_API ImGuiOldColumns* igFindOrCreateColumns(ImGuiWindow* window, - ImGuiID id) { - return ImGui::FindOrCreateColumns(window, id); -} -CIMGUI_API float igGetColumnOffsetFromNorm(const ImGuiOldColumns* columns, - float offset_norm) { - return ImGui::GetColumnOffsetFromNorm(columns, offset_norm); -} -CIMGUI_API float igGetColumnNormFromOffset(const ImGuiOldColumns* columns, - float offset) { - return ImGui::GetColumnNormFromOffset(columns, offset); -} -CIMGUI_API void igTableOpenContextMenu(int column_n) { - return ImGui::TableOpenContextMenu(column_n); -} -CIMGUI_API void igTableSetColumnWidth(int column_n, float width) { - return ImGui::TableSetColumnWidth(column_n, width); -} -CIMGUI_API void igTableSetColumnSortDirection(int column_n, - ImGuiSortDirection sort_direction, - bool append_to_sort_specs) { - return ImGui::TableSetColumnSortDirection(column_n, sort_direction, - append_to_sort_specs); -} -CIMGUI_API int igTableGetHoveredColumn() { - return ImGui::TableGetHoveredColumn(); -} -CIMGUI_API int igTableGetHoveredRow() { - return ImGui::TableGetHoveredRow(); -} -CIMGUI_API float igTableGetHeaderRowHeight() { - return ImGui::TableGetHeaderRowHeight(); -} -CIMGUI_API float igTableGetHeaderAngledMaxLabelWidth() { - return ImGui::TableGetHeaderAngledMaxLabelWidth(); -} -CIMGUI_API void igTablePushBackgroundChannel() { - return ImGui::TablePushBackgroundChannel(); -} -CIMGUI_API void igTablePopBackgroundChannel() { - return ImGui::TablePopBackgroundChannel(); -} -CIMGUI_API void igTableAngledHeadersRowEx(ImGuiID row_id, - float angle, - float max_label_width, - const ImGuiTableHeaderData* data, - int data_count) { - return ImGui::TableAngledHeadersRowEx(row_id, angle, max_label_width, data, - data_count); -} -CIMGUI_API ImGuiTable* igGetCurrentTable() { - return ImGui::GetCurrentTable(); -} -CIMGUI_API ImGuiTable* igTableFindByID(ImGuiID id) { - return ImGui::TableFindByID(id); -} -CIMGUI_API bool igBeginTableEx(const char* name, - ImGuiID id, - int columns_count, - ImGuiTableFlags flags, - const ImVec2 outer_size, - float inner_width) { - return ImGui::BeginTableEx(name, id, columns_count, flags, outer_size, - inner_width); -} -CIMGUI_API void igTableBeginInitMemory(ImGuiTable* table, int columns_count) { - return ImGui::TableBeginInitMemory(table, columns_count); -} -CIMGUI_API void igTableBeginApplyRequests(ImGuiTable* table) { - return ImGui::TableBeginApplyRequests(table); -} -CIMGUI_API void igTableSetupDrawChannels(ImGuiTable* table) { - return ImGui::TableSetupDrawChannels(table); -} -CIMGUI_API void igTableUpdateLayout(ImGuiTable* table) { - return ImGui::TableUpdateLayout(table); -} -CIMGUI_API void igTableUpdateBorders(ImGuiTable* table) { - return ImGui::TableUpdateBorders(table); -} -CIMGUI_API void igTableUpdateColumnsWeightFromWidth(ImGuiTable* table) { - return ImGui::TableUpdateColumnsWeightFromWidth(table); -} -CIMGUI_API void igTableDrawBorders(ImGuiTable* table) { - return ImGui::TableDrawBorders(table); -} -CIMGUI_API void igTableDrawDefaultContextMenu( - ImGuiTable* table, - ImGuiTableFlags flags_for_section_to_display) { - return ImGui::TableDrawDefaultContextMenu(table, - flags_for_section_to_display); -} -CIMGUI_API bool igTableBeginContextMenuPopup(ImGuiTable* table) { - return ImGui::TableBeginContextMenuPopup(table); -} -CIMGUI_API void igTableMergeDrawChannels(ImGuiTable* table) { - return ImGui::TableMergeDrawChannels(table); -} -CIMGUI_API ImGuiTableInstanceData* igTableGetInstanceData(ImGuiTable* table, - int instance_no) { - return ImGui::TableGetInstanceData(table, instance_no); -} -CIMGUI_API ImGuiID igTableGetInstanceID(ImGuiTable* table, int instance_no) { - return ImGui::TableGetInstanceID(table, instance_no); -} -CIMGUI_API void igTableSortSpecsSanitize(ImGuiTable* table) { - return ImGui::TableSortSpecsSanitize(table); -} -CIMGUI_API void igTableSortSpecsBuild(ImGuiTable* table) { - return ImGui::TableSortSpecsBuild(table); -} -CIMGUI_API ImGuiSortDirection -igTableGetColumnNextSortDirection(ImGuiTableColumn* column) { - return ImGui::TableGetColumnNextSortDirection(column); -} -CIMGUI_API void igTableFixColumnSortDirection(ImGuiTable* table, - ImGuiTableColumn* column) { - return ImGui::TableFixColumnSortDirection(table, column); -} -CIMGUI_API float igTableGetColumnWidthAuto(ImGuiTable* table, - ImGuiTableColumn* column) { - return ImGui::TableGetColumnWidthAuto(table, column); -} -CIMGUI_API void igTableBeginRow(ImGuiTable* table) { - return ImGui::TableBeginRow(table); -} -CIMGUI_API void igTableEndRow(ImGuiTable* table) { - return ImGui::TableEndRow(table); -} -CIMGUI_API void igTableBeginCell(ImGuiTable* table, int column_n) { - return ImGui::TableBeginCell(table, column_n); -} -CIMGUI_API void igTableEndCell(ImGuiTable* table) { - return ImGui::TableEndCell(table); -} -CIMGUI_API void igTableGetCellBgRect(ImRect* pOut, - const ImGuiTable* table, - int column_n) { - *pOut = ImGui::TableGetCellBgRect(table, column_n); -} -CIMGUI_API const char* igTableGetColumnName_TablePtr(const ImGuiTable* table, - int column_n) { - return ImGui::TableGetColumnName(table, column_n); -} -CIMGUI_API ImGuiID igTableGetColumnResizeID(ImGuiTable* table, - int column_n, - int instance_no) { - return ImGui::TableGetColumnResizeID(table, column_n, instance_no); -} -CIMGUI_API float igTableGetMaxColumnWidth(const ImGuiTable* table, - int column_n) { - return ImGui::TableGetMaxColumnWidth(table, column_n); -} -CIMGUI_API void igTableSetColumnWidthAutoSingle(ImGuiTable* table, - int column_n) { - return ImGui::TableSetColumnWidthAutoSingle(table, column_n); -} -CIMGUI_API void igTableSetColumnWidthAutoAll(ImGuiTable* table) { - return ImGui::TableSetColumnWidthAutoAll(table); -} -CIMGUI_API void igTableRemove(ImGuiTable* table) { - return ImGui::TableRemove(table); -} -CIMGUI_API void igTableGcCompactTransientBuffers_TablePtr(ImGuiTable* table) { - return ImGui::TableGcCompactTransientBuffers(table); -} -CIMGUI_API void igTableGcCompactTransientBuffers_TableTempDataPtr( - ImGuiTableTempData* table) { - return ImGui::TableGcCompactTransientBuffers(table); -} -CIMGUI_API void igTableGcCompactSettings() { - return ImGui::TableGcCompactSettings(); -} -CIMGUI_API void igTableLoadSettings(ImGuiTable* table) { - return ImGui::TableLoadSettings(table); -} -CIMGUI_API void igTableSaveSettings(ImGuiTable* table) { - return ImGui::TableSaveSettings(table); -} -CIMGUI_API void igTableResetSettings(ImGuiTable* table) { - return ImGui::TableResetSettings(table); -} -CIMGUI_API ImGuiTableSettings* igTableGetBoundSettings(ImGuiTable* table) { - return ImGui::TableGetBoundSettings(table); -} -CIMGUI_API void igTableSettingsAddSettingsHandler() { - return ImGui::TableSettingsAddSettingsHandler(); -} -CIMGUI_API ImGuiTableSettings* igTableSettingsCreate(ImGuiID id, - int columns_count) { - return ImGui::TableSettingsCreate(id, columns_count); -} -CIMGUI_API ImGuiTableSettings* igTableSettingsFindByID(ImGuiID id) { - return ImGui::TableSettingsFindByID(id); -} -CIMGUI_API ImGuiTabBar* igGetCurrentTabBar() { - return ImGui::GetCurrentTabBar(); -} -CIMGUI_API bool igBeginTabBarEx(ImGuiTabBar* tab_bar, - const ImRect bb, - ImGuiTabBarFlags flags) { - return ImGui::BeginTabBarEx(tab_bar, bb, flags); -} -CIMGUI_API ImGuiTabItem* igTabBarFindTabByID(ImGuiTabBar* tab_bar, - ImGuiID tab_id) { - return ImGui::TabBarFindTabByID(tab_bar, tab_id); -} -CIMGUI_API ImGuiTabItem* igTabBarFindTabByOrder(ImGuiTabBar* tab_bar, - int order) { - return ImGui::TabBarFindTabByOrder(tab_bar, order); -} -CIMGUI_API ImGuiTabItem* igTabBarFindMostRecentlySelectedTabForActiveWindow( - ImGuiTabBar* tab_bar) { - return ImGui::TabBarFindMostRecentlySelectedTabForActiveWindow(tab_bar); -} -CIMGUI_API ImGuiTabItem* igTabBarGetCurrentTab(ImGuiTabBar* tab_bar) { - return ImGui::TabBarGetCurrentTab(tab_bar); -} -CIMGUI_API int igTabBarGetTabOrder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) { - return ImGui::TabBarGetTabOrder(tab_bar, tab); -} -CIMGUI_API const char* igTabBarGetTabName(ImGuiTabBar* tab_bar, - ImGuiTabItem* tab) { - return ImGui::TabBarGetTabName(tab_bar, tab); -} -CIMGUI_API void igTabBarAddTab(ImGuiTabBar* tab_bar, - ImGuiTabItemFlags tab_flags, - ImGuiWindow* window) { - return ImGui::TabBarAddTab(tab_bar, tab_flags, window); -} -CIMGUI_API void igTabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id) { - return ImGui::TabBarRemoveTab(tab_bar, tab_id); -} -CIMGUI_API void igTabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) { - return ImGui::TabBarCloseTab(tab_bar, tab); -} -CIMGUI_API void igTabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) { - return ImGui::TabBarQueueFocus(tab_bar, tab); -} -CIMGUI_API void igTabBarQueueReorder(ImGuiTabBar* tab_bar, - ImGuiTabItem* tab, - int offset) { - return ImGui::TabBarQueueReorder(tab_bar, tab, offset); -} -CIMGUI_API void igTabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, - ImGuiTabItem* tab, - ImVec2 mouse_pos) { - return ImGui::TabBarQueueReorderFromMousePos(tab_bar, tab, mouse_pos); -} -CIMGUI_API bool igTabBarProcessReorder(ImGuiTabBar* tab_bar) { - return ImGui::TabBarProcessReorder(tab_bar); -} -CIMGUI_API bool igTabItemEx(ImGuiTabBar* tab_bar, - const char* label, - bool* p_open, - ImGuiTabItemFlags flags, - ImGuiWindow* docked_window) { - return ImGui::TabItemEx(tab_bar, label, p_open, flags, docked_window); -} -CIMGUI_API void igTabItemCalcSize_Str(ImVec2* pOut, - const char* label, - bool has_close_button_or_unsaved_marker) { - *pOut = ImGui::TabItemCalcSize(label, has_close_button_or_unsaved_marker); -} -CIMGUI_API void igTabItemCalcSize_WindowPtr(ImVec2* pOut, ImGuiWindow* window) { - *pOut = ImGui::TabItemCalcSize(window); -} -CIMGUI_API void igTabItemBackground(ImDrawList* draw_list, - const ImRect bb, - ImGuiTabItemFlags flags, - ImU32 col) { - return ImGui::TabItemBackground(draw_list, bb, flags, col); -} -CIMGUI_API void igTabItemLabelAndCloseButton(ImDrawList* draw_list, - const ImRect bb, - ImGuiTabItemFlags flags, - ImVec2 frame_padding, - const char* label, - ImGuiID tab_id, - ImGuiID close_button_id, - bool is_contents_visible, - bool* out_just_closed, - bool* out_text_clipped) { - return ImGui::TabItemLabelAndCloseButton( - draw_list, bb, flags, frame_padding, label, tab_id, close_button_id, - is_contents_visible, out_just_closed, out_text_clipped); -} -CIMGUI_API void igRenderText(ImVec2 pos, - const char* text, - const char* text_end, - bool hide_text_after_hash) { - return ImGui::RenderText(pos, text, text_end, hide_text_after_hash); -} -CIMGUI_API void igRenderTextWrapped(ImVec2 pos, - const char* text, - const char* text_end, - float wrap_width) { - return ImGui::RenderTextWrapped(pos, text, text_end, wrap_width); -} -CIMGUI_API void igRenderTextClipped(const ImVec2 pos_min, - const ImVec2 pos_max, - const char* text, - const char* text_end, - const ImVec2* text_size_if_known, - const ImVec2 align, - const ImRect* clip_rect) { - return ImGui::RenderTextClipped(pos_min, pos_max, text, text_end, - text_size_if_known, align, clip_rect); -} -CIMGUI_API void igRenderTextClippedEx(ImDrawList* draw_list, - const ImVec2 pos_min, - const ImVec2 pos_max, - const char* text, - const char* text_end, - const ImVec2* text_size_if_known, - const ImVec2 align, - const ImRect* clip_rect) { - return ImGui::RenderTextClippedEx(draw_list, pos_min, pos_max, text, text_end, - text_size_if_known, align, clip_rect); -} -CIMGUI_API void igRenderTextEllipsis(ImDrawList* draw_list, - const ImVec2 pos_min, - const ImVec2 pos_max, - float clip_max_x, - float ellipsis_max_x, - const char* text, - const char* text_end, - const ImVec2* text_size_if_known) { - return ImGui::RenderTextEllipsis(draw_list, pos_min, pos_max, clip_max_x, - ellipsis_max_x, text, text_end, - text_size_if_known); -} -CIMGUI_API void igRenderFrame(ImVec2 p_min, - ImVec2 p_max, - ImU32 fill_col, - bool border, - float rounding) { - return ImGui::RenderFrame(p_min, p_max, fill_col, border, rounding); -} -CIMGUI_API void igRenderFrameBorder(ImVec2 p_min, - ImVec2 p_max, - float rounding) { - return ImGui::RenderFrameBorder(p_min, p_max, rounding); -} -CIMGUI_API void igRenderColorRectWithAlphaCheckerboard(ImDrawList* draw_list, - ImVec2 p_min, - ImVec2 p_max, - ImU32 fill_col, - float grid_step, - ImVec2 grid_off, - float rounding, - ImDrawFlags flags) { - return ImGui::RenderColorRectWithAlphaCheckerboard( - draw_list, p_min, p_max, fill_col, grid_step, grid_off, rounding, flags); -} -CIMGUI_API void igRenderNavHighlight(const ImRect bb, - ImGuiID id, - ImGuiNavHighlightFlags flags) { - return ImGui::RenderNavHighlight(bb, id, flags); -} -CIMGUI_API const char* igFindRenderedTextEnd(const char* text, - const char* text_end) { - return ImGui::FindRenderedTextEnd(text, text_end); -} -CIMGUI_API void igRenderMouseCursor(ImVec2 pos, - float scale, - ImGuiMouseCursor mouse_cursor, - ImU32 col_fill, - ImU32 col_border, - ImU32 col_shadow) { - return ImGui::RenderMouseCursor(pos, scale, mouse_cursor, col_fill, - col_border, col_shadow); -} -CIMGUI_API void igRenderArrow(ImDrawList* draw_list, - ImVec2 pos, - ImU32 col, - ImGuiDir dir, - float scale) { - return ImGui::RenderArrow(draw_list, pos, col, dir, scale); -} -CIMGUI_API void igRenderBullet(ImDrawList* draw_list, ImVec2 pos, ImU32 col) { - return ImGui::RenderBullet(draw_list, pos, col); -} -CIMGUI_API void igRenderCheckMark(ImDrawList* draw_list, - ImVec2 pos, - ImU32 col, - float sz) { - return ImGui::RenderCheckMark(draw_list, pos, col, sz); -} -CIMGUI_API void igRenderArrowPointingAt(ImDrawList* draw_list, - ImVec2 pos, - ImVec2 half_sz, - ImGuiDir direction, - ImU32 col) { - return ImGui::RenderArrowPointingAt(draw_list, pos, half_sz, direction, col); -} -CIMGUI_API void igRenderArrowDockMenu(ImDrawList* draw_list, - ImVec2 p_min, - float sz, - ImU32 col) { - return ImGui::RenderArrowDockMenu(draw_list, p_min, sz, col); -} -CIMGUI_API void igRenderRectFilledRangeH(ImDrawList* draw_list, - const ImRect rect, - ImU32 col, - float x_start_norm, - float x_end_norm, - float rounding) { - return ImGui::RenderRectFilledRangeH(draw_list, rect, col, x_start_norm, - x_end_norm, rounding); -} -CIMGUI_API void igRenderRectFilledWithHole(ImDrawList* draw_list, - const ImRect outer, - const ImRect inner, - ImU32 col, - float rounding) { - return ImGui::RenderRectFilledWithHole(draw_list, outer, inner, col, - rounding); -} -CIMGUI_API ImDrawFlags igCalcRoundingFlagsForRectInRect(const ImRect r_in, - const ImRect r_outer, - float threshold) { - return ImGui::CalcRoundingFlagsForRectInRect(r_in, r_outer, threshold); -} -CIMGUI_API void igTextEx(const char* text, - const char* text_end, - ImGuiTextFlags flags) { - return ImGui::TextEx(text, text_end, flags); -} -CIMGUI_API bool igButtonEx(const char* label, - const ImVec2 size_arg, - ImGuiButtonFlags flags) { - return ImGui::ButtonEx(label, size_arg, flags); -} -CIMGUI_API bool igArrowButtonEx(const char* str_id, - ImGuiDir dir, - ImVec2 size_arg, - ImGuiButtonFlags flags) { - return ImGui::ArrowButtonEx(str_id, dir, size_arg, flags); -} -CIMGUI_API bool igImageButtonEx(ImGuiID id, - ImTextureID texture_id, - const ImVec2 image_size, - const ImVec2 uv0, - const ImVec2 uv1, - const ImVec4 bg_col, - const ImVec4 tint_col, - ImGuiButtonFlags flags) { - return ImGui::ImageButtonEx(id, texture_id, image_size, uv0, uv1, bg_col, - tint_col, flags); -} -CIMGUI_API void igSeparatorEx(ImGuiSeparatorFlags flags, float thickness) { - return ImGui::SeparatorEx(flags, thickness); -} -CIMGUI_API void igSeparatorTextEx(ImGuiID id, - const char* label, - const char* label_end, - float extra_width) { - return ImGui::SeparatorTextEx(id, label, label_end, extra_width); -} -CIMGUI_API bool igCheckboxFlags_S64Ptr(const char* label, - ImS64* flags, - ImS64 flags_value) { - return ImGui::CheckboxFlags(label, flags, flags_value); -} -CIMGUI_API bool igCheckboxFlags_U64Ptr(const char* label, - ImU64* flags, - ImU64 flags_value) { - return ImGui::CheckboxFlags(label, flags, flags_value); -} -CIMGUI_API bool igCloseButton(ImGuiID id, const ImVec2 pos) { - return ImGui::CloseButton(id, pos); -} -CIMGUI_API bool igCollapseButton(ImGuiID id, - const ImVec2 pos, - ImGuiDockNode* dock_node) { - return ImGui::CollapseButton(id, pos, dock_node); -} -CIMGUI_API void igScrollbar(ImGuiAxis axis) { - return ImGui::Scrollbar(axis); -} -CIMGUI_API bool igScrollbarEx(const ImRect bb, - ImGuiID id, - ImGuiAxis axis, - ImS64* p_scroll_v, - ImS64 avail_v, - ImS64 contents_v, - ImDrawFlags flags) { - return ImGui::ScrollbarEx(bb, id, axis, p_scroll_v, avail_v, contents_v, - flags); -} -CIMGUI_API void igGetWindowScrollbarRect(ImRect* pOut, - ImGuiWindow* window, - ImGuiAxis axis) { - *pOut = ImGui::GetWindowScrollbarRect(window, axis); -} -CIMGUI_API ImGuiID igGetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis) { - return ImGui::GetWindowScrollbarID(window, axis); -} -CIMGUI_API ImGuiID igGetWindowResizeCornerID(ImGuiWindow* window, int n) { - return ImGui::GetWindowResizeCornerID(window, n); -} -CIMGUI_API ImGuiID igGetWindowResizeBorderID(ImGuiWindow* window, - ImGuiDir dir) { - return ImGui::GetWindowResizeBorderID(window, dir); -} -CIMGUI_API bool igButtonBehavior(const ImRect bb, - ImGuiID id, - bool* out_hovered, - bool* out_held, - ImGuiButtonFlags flags) { - return ImGui::ButtonBehavior(bb, id, out_hovered, out_held, flags); -} -CIMGUI_API bool igDragBehavior(ImGuiID id, - ImGuiDataType data_type, - void* p_v, - float v_speed, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragBehavior(id, data_type, p_v, v_speed, p_min, p_max, format, - flags); -} -CIMGUI_API bool igSliderBehavior(const ImRect bb, - ImGuiID id, - ImGuiDataType data_type, - void* p_v, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags, - ImRect* out_grab_bb) { - return ImGui::SliderBehavior(bb, id, data_type, p_v, p_min, p_max, format, - flags, out_grab_bb); -} -CIMGUI_API bool igSplitterBehavior(const ImRect bb, - ImGuiID id, - ImGuiAxis axis, - float* size1, - float* size2, - float min_size1, - float min_size2, - float hover_extend, - float hover_visibility_delay, - ImU32 bg_col) { - return ImGui::SplitterBehavior(bb, id, axis, size1, size2, min_size1, - min_size2, hover_extend, - hover_visibility_delay, bg_col); -} -CIMGUI_API bool igTreeNodeBehavior(ImGuiID id, - ImGuiTreeNodeFlags flags, - const char* label, - const char* label_end) { - return ImGui::TreeNodeBehavior(id, flags, label, label_end); -} -CIMGUI_API void igTreePushOverrideID(ImGuiID id) { - return ImGui::TreePushOverrideID(id); -} -CIMGUI_API void igTreeNodeSetOpen(ImGuiID id, bool open) { - return ImGui::TreeNodeSetOpen(id, open); -} -CIMGUI_API bool igTreeNodeUpdateNextOpen(ImGuiID id, ImGuiTreeNodeFlags flags) { - return ImGui::TreeNodeUpdateNextOpen(id, flags); -} -CIMGUI_API void igSetNextItemSelectionUserData( - ImGuiSelectionUserData selection_user_data) { - return ImGui::SetNextItemSelectionUserData(selection_user_data); -} -CIMGUI_API const ImGuiDataTypeInfo* igDataTypeGetInfo(ImGuiDataType data_type) { - return ImGui::DataTypeGetInfo(data_type); -} -CIMGUI_API int igDataTypeFormatString(char* buf, - int buf_size, - ImGuiDataType data_type, - const void* p_data, - const char* format) { - return ImGui::DataTypeFormatString(buf, buf_size, data_type, p_data, format); -} -CIMGUI_API void igDataTypeApplyOp(ImGuiDataType data_type, - int op, - void* output, - const void* arg_1, - const void* arg_2) { - return ImGui::DataTypeApplyOp(data_type, op, output, arg_1, arg_2); -} -CIMGUI_API bool igDataTypeApplyFromText(const char* buf, - ImGuiDataType data_type, - void* p_data, - const char* format) { - return ImGui::DataTypeApplyFromText(buf, data_type, p_data, format); -} -CIMGUI_API int igDataTypeCompare(ImGuiDataType data_type, - const void* arg_1, - const void* arg_2) { - return ImGui::DataTypeCompare(data_type, arg_1, arg_2); -} -CIMGUI_API bool igDataTypeClamp(ImGuiDataType data_type, - void* p_data, - const void* p_min, - const void* p_max) { - return ImGui::DataTypeClamp(data_type, p_data, p_min, p_max); -} -CIMGUI_API bool igInputTextEx(const char* label, - const char* hint, - char* buf, - int buf_size, - const ImVec2 size_arg, - ImGuiInputTextFlags flags, - ImGuiInputTextCallback callback, - void* user_data) { - return ImGui::InputTextEx(label, hint, buf, buf_size, size_arg, flags, - callback, user_data); -} -CIMGUI_API void igInputTextDeactivateHook(ImGuiID id) { - return ImGui::InputTextDeactivateHook(id); -} -CIMGUI_API bool igTempInputText(const ImRect bb, - ImGuiID id, - const char* label, - char* buf, - int buf_size, - ImGuiInputTextFlags flags) { - return ImGui::TempInputText(bb, id, label, buf, buf_size, flags); -} -CIMGUI_API bool igTempInputScalar(const ImRect bb, - ImGuiID id, - const char* label, - ImGuiDataType data_type, - void* p_data, - const char* format, - const void* p_clamp_min, - const void* p_clamp_max) { - return ImGui::TempInputScalar(bb, id, label, data_type, p_data, format, - p_clamp_min, p_clamp_max); -} -CIMGUI_API bool igTempInputIsActive(ImGuiID id) { - return ImGui::TempInputIsActive(id); -} -CIMGUI_API ImGuiInputTextState* igGetInputTextState(ImGuiID id) { - return ImGui::GetInputTextState(id); -} -CIMGUI_API void igColorTooltip(const char* text, - const float* col, - ImGuiColorEditFlags flags) { - return ImGui::ColorTooltip(text, col, flags); -} -CIMGUI_API void igColorEditOptionsPopup(const float* col, - ImGuiColorEditFlags flags) { - return ImGui::ColorEditOptionsPopup(col, flags); -} -CIMGUI_API void igColorPickerOptionsPopup(const float* ref_col, - ImGuiColorEditFlags flags) { - return ImGui::ColorPickerOptionsPopup(ref_col, flags); -} -CIMGUI_API int igPlotEx(ImGuiPlotType plot_type, - const char* label, - float (*values_getter)(void* data, int idx), - void* data, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - const ImVec2 size_arg) { - return ImGui::PlotEx(plot_type, label, values_getter, data, values_count, - values_offset, overlay_text, scale_min, scale_max, - size_arg); -} -CIMGUI_API void igShadeVertsLinearColorGradientKeepAlpha(ImDrawList* draw_list, - int vert_start_idx, - int vert_end_idx, - ImVec2 gradient_p0, - ImVec2 gradient_p1, - ImU32 col0, - ImU32 col1) { - return ImGui::ShadeVertsLinearColorGradientKeepAlpha( - draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col0, - col1); -} -CIMGUI_API void igShadeVertsLinearUV(ImDrawList* draw_list, - int vert_start_idx, - int vert_end_idx, - const ImVec2 a, - const ImVec2 b, - const ImVec2 uv_a, - const ImVec2 uv_b, - bool clamp) { - return ImGui::ShadeVertsLinearUV(draw_list, vert_start_idx, vert_end_idx, a, - b, uv_a, uv_b, clamp); -} -CIMGUI_API void igShadeVertsTransformPos(ImDrawList* draw_list, - int vert_start_idx, - int vert_end_idx, - const ImVec2 pivot_in, - float cos_a, - float sin_a, - const ImVec2 pivot_out) { - return ImGui::ShadeVertsTransformPos(draw_list, vert_start_idx, vert_end_idx, - pivot_in, cos_a, sin_a, pivot_out); -} -CIMGUI_API void igGcCompactTransientMiscBuffers() { - return ImGui::GcCompactTransientMiscBuffers(); -} -CIMGUI_API void igGcCompactTransientWindowBuffers(ImGuiWindow* window) { - return ImGui::GcCompactTransientWindowBuffers(window); -} -CIMGUI_API void igGcAwakeTransientWindowBuffers(ImGuiWindow* window) { - return ImGui::GcAwakeTransientWindowBuffers(window); -} -CIMGUI_API void igDebugLog(const char* fmt, ...) { - va_list args; - va_start(args, fmt); - ImGui::DebugLogV(fmt, args); - va_end(args); -} -CIMGUI_API void igDebugLogV(const char* fmt, va_list args) { - return ImGui::DebugLogV(fmt, args); -} -CIMGUI_API void igDebugAllocHook(ImGuiDebugAllocInfo* info, - int frame_count, - void* ptr, - size_t size) { - return ImGui::DebugAllocHook(info, frame_count, ptr, size); -} -CIMGUI_API void igErrorCheckEndFrameRecover(ImGuiErrorLogCallback log_callback, - void* user_data) { - return ImGui::ErrorCheckEndFrameRecover(log_callback, user_data); -} -CIMGUI_API void igErrorCheckEndWindowRecover(ImGuiErrorLogCallback log_callback, - void* user_data) { - return ImGui::ErrorCheckEndWindowRecover(log_callback, user_data); -} -CIMGUI_API void igErrorCheckUsingSetCursorPosToExtendParentBoundaries() { - return ImGui::ErrorCheckUsingSetCursorPosToExtendParentBoundaries(); -} -CIMGUI_API void igDebugDrawCursorPos(ImU32 col) { - return ImGui::DebugDrawCursorPos(col); -} -CIMGUI_API void igDebugDrawLineExtents(ImU32 col) { - return ImGui::DebugDrawLineExtents(col); -} -CIMGUI_API void igDebugDrawItemRect(ImU32 col) { - return ImGui::DebugDrawItemRect(col); -} -CIMGUI_API void igDebugLocateItem(ImGuiID target_id) { - return ImGui::DebugLocateItem(target_id); -} -CIMGUI_API void igDebugLocateItemOnHover(ImGuiID target_id) { - return ImGui::DebugLocateItemOnHover(target_id); -} -CIMGUI_API void igDebugLocateItemResolveWithLastItem() { - return ImGui::DebugLocateItemResolveWithLastItem(); -} -CIMGUI_API void igDebugBreakClearData() { - return ImGui::DebugBreakClearData(); -} -CIMGUI_API bool igDebugBreakButton(const char* label, - const char* description_of_location) { - return ImGui::DebugBreakButton(label, description_of_location); -} -CIMGUI_API void igDebugBreakButtonTooltip(bool keyboard_only, - const char* description_of_location) { - return ImGui::DebugBreakButtonTooltip(keyboard_only, description_of_location); -} -CIMGUI_API void igShowFontAtlas(ImFontAtlas* atlas) { - return ImGui::ShowFontAtlas(atlas); -} -CIMGUI_API void igDebugHookIdInfo(ImGuiID id, - ImGuiDataType data_type, - const void* data_id, - const void* data_id_end) { - return ImGui::DebugHookIdInfo(id, data_type, data_id, data_id_end); -} -CIMGUI_API void igDebugNodeColumns(ImGuiOldColumns* columns) { - return ImGui::DebugNodeColumns(columns); -} -CIMGUI_API void igDebugNodeDockNode(ImGuiDockNode* node, const char* label) { - return ImGui::DebugNodeDockNode(node, label); -} -CIMGUI_API void igDebugNodeDrawList(ImGuiWindow* window, - ImGuiViewportP* viewport, - const ImDrawList* draw_list, - const char* label) { - return ImGui::DebugNodeDrawList(window, viewport, draw_list, label); -} -CIMGUI_API void igDebugNodeDrawCmdShowMeshAndBoundingBox( - ImDrawList* out_draw_list, - const ImDrawList* draw_list, - const ImDrawCmd* draw_cmd, - bool show_mesh, - bool show_aabb) { - return ImGui::DebugNodeDrawCmdShowMeshAndBoundingBox( - out_draw_list, draw_list, draw_cmd, show_mesh, show_aabb); -} -CIMGUI_API void igDebugNodeFont(ImFont* font) { - return ImGui::DebugNodeFont(font); -} -CIMGUI_API void igDebugNodeFontGlyph(ImFont* font, const ImFontGlyph* glyph) { - return ImGui::DebugNodeFontGlyph(font, glyph); -} -CIMGUI_API void igDebugNodeStorage(ImGuiStorage* storage, const char* label) { - return ImGui::DebugNodeStorage(storage, label); -} -CIMGUI_API void igDebugNodeTabBar(ImGuiTabBar* tab_bar, const char* label) { - return ImGui::DebugNodeTabBar(tab_bar, label); -} -CIMGUI_API void igDebugNodeTable(ImGuiTable* table) { - return ImGui::DebugNodeTable(table); -} -CIMGUI_API void igDebugNodeTableSettings(ImGuiTableSettings* settings) { - return ImGui::DebugNodeTableSettings(settings); -} -CIMGUI_API void igDebugNodeInputTextState(ImGuiInputTextState* state) { - return ImGui::DebugNodeInputTextState(state); -} -CIMGUI_API void igDebugNodeTypingSelectState(ImGuiTypingSelectState* state) { - return ImGui::DebugNodeTypingSelectState(state); -} -CIMGUI_API void igDebugNodeWindow(ImGuiWindow* window, const char* label) { - return ImGui::DebugNodeWindow(window, label); -} -CIMGUI_API void igDebugNodeWindowSettings(ImGuiWindowSettings* settings) { - return ImGui::DebugNodeWindowSettings(settings); -} -CIMGUI_API void igDebugNodeWindowsList(ImVector_ImGuiWindowPtr* windows, - const char* label) { - return ImGui::DebugNodeWindowsList(windows, label); -} -CIMGUI_API void igDebugNodeWindowsListByBeginStackParent( - ImGuiWindow** windows, - int windows_size, - ImGuiWindow* parent_in_begin_stack) { - return ImGui::DebugNodeWindowsListByBeginStackParent(windows, windows_size, - parent_in_begin_stack); -} -CIMGUI_API void igDebugNodeViewport(ImGuiViewportP* viewport) { - return ImGui::DebugNodeViewport(viewport); -} -CIMGUI_API void igDebugRenderKeyboardPreview(ImDrawList* draw_list) { - return ImGui::DebugRenderKeyboardPreview(draw_list); -} -CIMGUI_API void igDebugRenderViewportThumbnail(ImDrawList* draw_list, - ImGuiViewportP* viewport, - const ImRect bb) { - return ImGui::DebugRenderViewportThumbnail(draw_list, viewport, bb); -} - -CIMGUI_API void igImFontAtlasUpdateConfigDataPointers(ImFontAtlas* atlas) { - return ImFontAtlasUpdateConfigDataPointers(atlas); -} -CIMGUI_API void igImFontAtlasBuildInit(ImFontAtlas* atlas) { - return ImFontAtlasBuildInit(atlas); -} -CIMGUI_API void igImFontAtlasBuildSetupFont(ImFontAtlas* atlas, - ImFont* font, - ImFontConfig* font_config, - float ascent, - float descent) { - return ImFontAtlasBuildSetupFont(atlas, font, font_config, ascent, descent); -} -CIMGUI_API void igImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, - void* stbrp_context_opaque) { - return ImFontAtlasBuildPackCustomRects(atlas, stbrp_context_opaque); -} -CIMGUI_API void igImFontAtlasBuildFinish(ImFontAtlas* atlas) { - return ImFontAtlasBuildFinish(atlas); -} -CIMGUI_API void igImFontAtlasBuildRender8bppRectFromString( - ImFontAtlas* atlas, - int x, - int y, - int w, - int h, - const char* in_str, - char in_marker_char, - unsigned char in_marker_pixel_value) { - return ImFontAtlasBuildRender8bppRectFromString( - atlas, x, y, w, h, in_str, in_marker_char, in_marker_pixel_value); -} -CIMGUI_API void igImFontAtlasBuildRender32bppRectFromString( - ImFontAtlas* atlas, - int x, - int y, - int w, - int h, - const char* in_str, - char in_marker_char, - unsigned int in_marker_pixel_value) { - return ImFontAtlasBuildRender32bppRectFromString( - atlas, x, y, w, h, in_str, in_marker_char, in_marker_pixel_value); -} -CIMGUI_API void igImFontAtlasBuildMultiplyCalcLookupTable( - unsigned char out_table[256], - float in_multiply_factor) { - return ImFontAtlasBuildMultiplyCalcLookupTable(out_table, in_multiply_factor); -} -CIMGUI_API void igImFontAtlasBuildMultiplyRectAlpha8( - const unsigned char table[256], - unsigned char* pixels, - int x, - int y, - int w, - int h, - int stride) { - return ImFontAtlasBuildMultiplyRectAlpha8(table, pixels, x, y, w, h, stride); -} - -/////////////////////////////manual written functions -CIMGUI_API void igLogText(CONST char* fmt, ...) { - char buffer[256]; - va_list args; - va_start(args, fmt); - vsnprintf(buffer, 256, fmt, args); - va_end(args); - - ImGui::LogText("%s", buffer); -} -CIMGUI_API void ImGuiTextBuffer_appendf(struct ImGuiTextBuffer* buffer, - const char* fmt, - ...) { - va_list args; - va_start(args, fmt); - buffer->appendfv(fmt, args); - va_end(args); -} - -CIMGUI_API float igGET_FLT_MAX() { - return FLT_MAX; -} - -CIMGUI_API float igGET_FLT_MIN() { - return FLT_MIN; -} - -CIMGUI_API ImVector_ImWchar* ImVector_ImWchar_create() { - return IM_NEW(ImVector)(); -} - -CIMGUI_API void ImVector_ImWchar_destroy(ImVector_ImWchar* self) { - IM_DELETE(self); -} - -CIMGUI_API void ImVector_ImWchar_Init(ImVector_ImWchar* p) { - IM_PLACEMENT_NEW(p) ImVector(); -} -CIMGUI_API void ImVector_ImWchar_UnInit(ImVector_ImWchar* p) { - p->~ImVector(); -} - -#ifdef IMGUI_HAS_DOCK - -// NOTE: Some function pointers in the ImGuiPlatformIO structure are not -// C-compatible because of their use of a complex return type. To work around -// this, we store a custom CimguiStorage object inside -// ImGuiIO::BackendLanguageUserData, which contains C-compatible function -// pointer variants for these functions. When a user function pointer is -// provided, we hook up the underlying ImGuiPlatformIO function pointer to a -// thunk which accesses the user function pointer through CimguiStorage. - -struct CimguiStorage { - void (*Platform_GetWindowPos)(ImGuiViewport* vp, ImVec2* out_pos); - void (*Platform_GetWindowSize)(ImGuiViewport* vp, ImVec2* out_pos); -}; - -// Gets a reference to the CimguiStorage object stored in the current ImGui -// context's BackendLanguageUserData. -CimguiStorage& GetCimguiStorage() { - ImGuiIO& io = ImGui::GetIO(); - if (io.BackendLanguageUserData == NULL) { - io.BackendLanguageUserData = new CimguiStorage(); - } - - return *(CimguiStorage*)io.BackendLanguageUserData; -} - -// Thunk satisfying the signature of ImGuiPlatformIO::Platform_GetWindowPos. -ImVec2 Platform_GetWindowPos_hook(ImGuiViewport* vp) { - ImVec2 pos; - GetCimguiStorage().Platform_GetWindowPos(vp, &pos); - return pos; -}; - -// Fully C-compatible function pointer setter for -// ImGuiPlatformIO::Platform_GetWindowPos. -CIMGUI_API void ImGuiPlatformIO_Set_Platform_GetWindowPos( - ImGuiPlatformIO* platform_io, - void (*user_callback)(ImGuiViewport* vp, ImVec2* out_pos)) { - CimguiStorage& storage = GetCimguiStorage(); - storage.Platform_GetWindowPos = user_callback; - platform_io->Platform_GetWindowPos = &Platform_GetWindowPos_hook; -} - -// Thunk satisfying the signature of ImGuiPlatformIO::Platform_GetWindowSize. -ImVec2 Platform_GetWindowSize_hook(ImGuiViewport* vp) { - ImVec2 size; - GetCimguiStorage().Platform_GetWindowSize(vp, &size); - return size; -}; - -// Fully C-compatible function pointer setter for -// ImGuiPlatformIO::Platform_GetWindowSize. -CIMGUI_API void ImGuiPlatformIO_Set_Platform_GetWindowSize( - ImGuiPlatformIO* platform_io, - void (*user_callback)(ImGuiViewport* vp, ImVec2* out_size)) { - CimguiStorage& storage = GetCimguiStorage(); - storage.Platform_GetWindowSize = user_callback; - platform_io->Platform_GetWindowSize = &Platform_GetWindowSize_hook; -} - -#endif diff --git a/pkg/cimgui/vendor/cimgui.h b/pkg/cimgui/vendor/cimgui.h deleted file mode 100644 index f00b4d9b7..000000000 --- a/pkg/cimgui/vendor/cimgui.h +++ /dev/null @@ -1,6554 +0,0 @@ -// This file is automatically generated by generator.lua from -// https://github.com/cimgui/cimgui based on imgui.h file version "1.90.6" 19060 -// from Dear ImGui https://github.com/ocornut/imgui with imgui_internal.h api -// docking branch -#ifndef CIMGUI_INCLUDED -#define CIMGUI_INCLUDED -#include -#include -#if defined _WIN32 || defined __CYGWIN__ -#ifdef CIMGUI_NO_EXPORT -#define API -#else -#define API __declspec(dllexport) -#endif -#else -#ifdef __GNUC__ -#define API __attribute__((__visibility__("default"))) -#else -#define API -#endif -#endif - -#if defined __cplusplus -#define EXTERN extern "C" -#else -#include -#include -#define EXTERN extern -#endif - -#define CIMGUI_API EXTERN API -#define CONST const - -#ifdef _MSC_VER -typedef unsigned __int64 ImU64; -#else -// typedef unsigned long long ImU64; -#endif - -#ifdef CIMGUI_DEFINE_ENUMS_AND_STRUCTS - -typedef struct ImDrawChannel ImDrawChannel; -typedef struct ImDrawCmd ImDrawCmd; -typedef struct ImDrawData ImDrawData; -typedef struct ImDrawList ImDrawList; -typedef struct ImDrawListSharedData ImDrawListSharedData; -typedef struct ImDrawListSplitter ImDrawListSplitter; -typedef struct ImDrawVert ImDrawVert; -typedef struct ImFont ImFont; -typedef struct ImFontAtlas ImFontAtlas; -typedef struct ImFontBuilderIO ImFontBuilderIO; -typedef struct ImFontConfig ImFontConfig; -typedef struct ImFontGlyph ImFontGlyph; -typedef struct ImFontGlyphRangesBuilder ImFontGlyphRangesBuilder; -typedef struct ImColor ImColor; -typedef struct ImGuiContext ImGuiContext; -typedef struct ImGuiIO ImGuiIO; -typedef struct ImGuiInputTextCallbackData ImGuiInputTextCallbackData; -typedef struct ImGuiKeyData ImGuiKeyData; -typedef struct ImGuiListClipper ImGuiListClipper; -typedef struct ImGuiOnceUponAFrame ImGuiOnceUponAFrame; -typedef struct ImGuiPayload ImGuiPayload; -typedef struct ImGuiPlatformIO ImGuiPlatformIO; -typedef struct ImGuiPlatformMonitor ImGuiPlatformMonitor; -typedef struct ImGuiPlatformImeData ImGuiPlatformImeData; -typedef struct ImGuiSizeCallbackData ImGuiSizeCallbackData; -typedef struct ImGuiStorage ImGuiStorage; -typedef struct ImGuiStyle ImGuiStyle; -typedef struct ImGuiTableSortSpecs ImGuiTableSortSpecs; -typedef struct ImGuiTableColumnSortSpecs ImGuiTableColumnSortSpecs; -typedef struct ImGuiTextBuffer ImGuiTextBuffer; -typedef struct ImGuiTextFilter ImGuiTextFilter; -typedef struct ImGuiViewport ImGuiViewport; -typedef struct ImGuiWindowClass ImGuiWindowClass; -typedef struct ImBitVector ImBitVector; -typedef struct ImRect ImRect; -typedef struct ImDrawDataBuilder ImDrawDataBuilder; -typedef struct ImGuiColorMod ImGuiColorMod; -typedef struct ImGuiContextHook ImGuiContextHook; -typedef struct ImGuiDataVarInfo ImGuiDataVarInfo; -typedef struct ImGuiDataTypeInfo ImGuiDataTypeInfo; -typedef struct ImGuiDockContext ImGuiDockContext; -typedef struct ImGuiDockRequest ImGuiDockRequest; -typedef struct ImGuiDockNode ImGuiDockNode; -typedef struct ImGuiDockNodeSettings ImGuiDockNodeSettings; -typedef struct ImGuiGroupData ImGuiGroupData; -typedef struct ImGuiInputTextState ImGuiInputTextState; -typedef struct ImGuiInputTextDeactivateData ImGuiInputTextDeactivateData; -typedef struct ImGuiLastItemData ImGuiLastItemData; -typedef struct ImGuiLocEntry ImGuiLocEntry; -typedef struct ImGuiMenuColumns ImGuiMenuColumns; -typedef struct ImGuiNavItemData ImGuiNavItemData; -typedef struct ImGuiNavTreeNodeData ImGuiNavTreeNodeData; -typedef struct ImGuiMetricsConfig ImGuiMetricsConfig; -typedef struct ImGuiNextWindowData ImGuiNextWindowData; -typedef struct ImGuiNextItemData ImGuiNextItemData; -typedef struct ImGuiOldColumnData ImGuiOldColumnData; -typedef struct ImGuiOldColumns ImGuiOldColumns; -typedef struct ImGuiPopupData ImGuiPopupData; -typedef struct ImGuiSettingsHandler ImGuiSettingsHandler; -typedef struct ImGuiStackSizes ImGuiStackSizes; -typedef struct ImGuiStyleMod ImGuiStyleMod; -typedef struct ImGuiTabBar ImGuiTabBar; -typedef struct ImGuiTabItem ImGuiTabItem; -typedef struct ImGuiTable ImGuiTable; -typedef struct ImGuiTableHeaderData ImGuiTableHeaderData; -typedef struct ImGuiTableColumn ImGuiTableColumn; -typedef struct ImGuiTableInstanceData ImGuiTableInstanceData; -typedef struct ImGuiTableTempData ImGuiTableTempData; -typedef struct ImGuiTableSettings ImGuiTableSettings; -typedef struct ImGuiTableColumnsSettings ImGuiTableColumnsSettings; -typedef struct ImGuiTypingSelectState ImGuiTypingSelectState; -typedef struct ImGuiTypingSelectRequest ImGuiTypingSelectRequest; -typedef struct ImGuiWindow ImGuiWindow; -typedef struct ImGuiWindowDockStyle ImGuiWindowDockStyle; -typedef struct ImGuiWindowTempData ImGuiWindowTempData; -typedef struct ImGuiWindowSettings ImGuiWindowSettings; -typedef struct ImVector_const_charPtr { - int Size; - int Capacity; - const char** Data; -} ImVector_const_charPtr; - -struct ImDrawChannel; -struct ImDrawCmd; -struct ImDrawData; -struct ImDrawList; -struct ImDrawListSharedData; -struct ImDrawListSplitter; -struct ImDrawVert; -struct ImFont; -struct ImFontAtlas; -struct ImFontBuilderIO; -struct ImFontConfig; -struct ImFontGlyph; -struct ImFontGlyphRangesBuilder; -struct ImColor; -struct ImGuiContext; -struct ImGuiIO; -struct ImGuiInputTextCallbackData; -struct ImGuiKeyData; -struct ImGuiListClipper; -struct ImGuiOnceUponAFrame; -struct ImGuiPayload; -struct ImGuiPlatformIO; -struct ImGuiPlatformMonitor; -struct ImGuiPlatformImeData; -struct ImGuiSizeCallbackData; -struct ImGuiStorage; -struct ImGuiStyle; -struct ImGuiTableSortSpecs; -struct ImGuiTableColumnSortSpecs; -struct ImGuiTextBuffer; -struct ImGuiTextFilter; -struct ImGuiViewport; -struct ImGuiWindowClass; -typedef int ImGuiCol; -typedef int ImGuiCond; -typedef int ImGuiDataType; -typedef int ImGuiDir; -typedef int ImGuiMouseButton; -typedef int ImGuiMouseCursor; -typedef int ImGuiSortDirection; -typedef int ImGuiStyleVar; -typedef int ImGuiTableBgTarget; -typedef int ImDrawFlags; -typedef int ImDrawListFlags; -typedef int ImFontAtlasFlags; -typedef int ImGuiBackendFlags; -typedef int ImGuiButtonFlags; -typedef int ImGuiChildFlags; -typedef int ImGuiColorEditFlags; -typedef int ImGuiConfigFlags; -typedef int ImGuiComboFlags; -typedef int ImGuiDockNodeFlags; -typedef int ImGuiDragDropFlags; -typedef int ImGuiFocusedFlags; -typedef int ImGuiHoveredFlags; -typedef int ImGuiInputTextFlags; -typedef int ImGuiKeyChord; -typedef int ImGuiPopupFlags; -typedef int ImGuiSelectableFlags; -typedef int ImGuiSliderFlags; -typedef int ImGuiTabBarFlags; -typedef int ImGuiTabItemFlags; -typedef int ImGuiTableFlags; -typedef int ImGuiTableColumnFlags; -typedef int ImGuiTableRowFlags; -typedef int ImGuiTreeNodeFlags; -typedef int ImGuiViewportFlags; -typedef int ImGuiWindowFlags; -typedef void* ImTextureID; -typedef unsigned short ImDrawIdx; -typedef unsigned int ImGuiID; -typedef signed char ImS8; -typedef unsigned char ImU8; -typedef signed short ImS16; -typedef unsigned short ImU16; -typedef signed int ImS32; -typedef unsigned int ImU32; -typedef signed long long ImS64; -typedef unsigned long long ImU64; -typedef unsigned int ImWchar32; -typedef unsigned short ImWchar16; -typedef ImWchar16 ImWchar; -typedef int (*ImGuiInputTextCallback)(ImGuiInputTextCallbackData* data); -typedef void (*ImGuiSizeCallback)(ImGuiSizeCallbackData* data); -typedef void* (*ImGuiMemAllocFunc)(size_t sz, void* user_data); -typedef void (*ImGuiMemFreeFunc)(void* ptr, void* user_data); -typedef struct ImVec2 ImVec2; -struct ImVec2 { - float x, y; -}; -typedef struct ImVec4 ImVec4; -struct ImVec4 { - float x, y, z, w; -}; -typedef enum { - ImGuiWindowFlags_None = 0, - ImGuiWindowFlags_NoTitleBar = 1 << 0, - ImGuiWindowFlags_NoResize = 1 << 1, - ImGuiWindowFlags_NoMove = 1 << 2, - ImGuiWindowFlags_NoScrollbar = 1 << 3, - ImGuiWindowFlags_NoScrollWithMouse = 1 << 4, - ImGuiWindowFlags_NoCollapse = 1 << 5, - ImGuiWindowFlags_AlwaysAutoResize = 1 << 6, - ImGuiWindowFlags_NoBackground = 1 << 7, - ImGuiWindowFlags_NoSavedSettings = 1 << 8, - ImGuiWindowFlags_NoMouseInputs = 1 << 9, - ImGuiWindowFlags_MenuBar = 1 << 10, - ImGuiWindowFlags_HorizontalScrollbar = 1 << 11, - ImGuiWindowFlags_NoFocusOnAppearing = 1 << 12, - ImGuiWindowFlags_NoBringToFrontOnFocus = 1 << 13, - ImGuiWindowFlags_AlwaysVerticalScrollbar = 1 << 14, - ImGuiWindowFlags_AlwaysHorizontalScrollbar = 1 << 15, - ImGuiWindowFlags_NoNavInputs = 1 << 16, - ImGuiWindowFlags_NoNavFocus = 1 << 17, - ImGuiWindowFlags_UnsavedDocument = 1 << 18, - ImGuiWindowFlags_NoDocking = 1 << 19, - ImGuiWindowFlags_NoNav = - ImGuiWindowFlags_NoNavInputs | ImGuiWindowFlags_NoNavFocus, - ImGuiWindowFlags_NoDecoration = - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse, - ImGuiWindowFlags_NoInputs = ImGuiWindowFlags_NoMouseInputs | - ImGuiWindowFlags_NoNavInputs | - ImGuiWindowFlags_NoNavFocus, - ImGuiWindowFlags_NavFlattened = 1 << 23, - ImGuiWindowFlags_ChildWindow = 1 << 24, - ImGuiWindowFlags_Tooltip = 1 << 25, - ImGuiWindowFlags_Popup = 1 << 26, - ImGuiWindowFlags_Modal = 1 << 27, - ImGuiWindowFlags_ChildMenu = 1 << 28, - ImGuiWindowFlags_DockNodeHost = 1 << 29, -} ImGuiWindowFlags_; -typedef enum { - ImGuiChildFlags_None = 0, - ImGuiChildFlags_Border = 1 << 0, - ImGuiChildFlags_AlwaysUseWindowPadding = 1 << 1, - ImGuiChildFlags_ResizeX = 1 << 2, - ImGuiChildFlags_ResizeY = 1 << 3, - ImGuiChildFlags_AutoResizeX = 1 << 4, - ImGuiChildFlags_AutoResizeY = 1 << 5, - ImGuiChildFlags_AlwaysAutoResize = 1 << 6, - ImGuiChildFlags_FrameStyle = 1 << 7, -} ImGuiChildFlags_; -typedef enum { - ImGuiInputTextFlags_None = 0, - ImGuiInputTextFlags_CharsDecimal = 1 << 0, - ImGuiInputTextFlags_CharsHexadecimal = 1 << 1, - ImGuiInputTextFlags_CharsUppercase = 1 << 2, - ImGuiInputTextFlags_CharsNoBlank = 1 << 3, - ImGuiInputTextFlags_AutoSelectAll = 1 << 4, - ImGuiInputTextFlags_EnterReturnsTrue = 1 << 5, - ImGuiInputTextFlags_CallbackCompletion = 1 << 6, - ImGuiInputTextFlags_CallbackHistory = 1 << 7, - ImGuiInputTextFlags_CallbackAlways = 1 << 8, - ImGuiInputTextFlags_CallbackCharFilter = 1 << 9, - ImGuiInputTextFlags_AllowTabInput = 1 << 10, - ImGuiInputTextFlags_CtrlEnterForNewLine = 1 << 11, - ImGuiInputTextFlags_NoHorizontalScroll = 1 << 12, - ImGuiInputTextFlags_AlwaysOverwrite = 1 << 13, - ImGuiInputTextFlags_ReadOnly = 1 << 14, - ImGuiInputTextFlags_Password = 1 << 15, - ImGuiInputTextFlags_NoUndoRedo = 1 << 16, - ImGuiInputTextFlags_CharsScientific = 1 << 17, - ImGuiInputTextFlags_CallbackResize = 1 << 18, - ImGuiInputTextFlags_CallbackEdit = 1 << 19, - ImGuiInputTextFlags_EscapeClearsAll = 1 << 20, -} ImGuiInputTextFlags_; -typedef enum { - ImGuiTreeNodeFlags_None = 0, - ImGuiTreeNodeFlags_Selected = 1 << 0, - ImGuiTreeNodeFlags_Framed = 1 << 1, - ImGuiTreeNodeFlags_AllowOverlap = 1 << 2, - ImGuiTreeNodeFlags_NoTreePushOnOpen = 1 << 3, - ImGuiTreeNodeFlags_NoAutoOpenOnLog = 1 << 4, - ImGuiTreeNodeFlags_DefaultOpen = 1 << 5, - ImGuiTreeNodeFlags_OpenOnDoubleClick = 1 << 6, - ImGuiTreeNodeFlags_OpenOnArrow = 1 << 7, - ImGuiTreeNodeFlags_Leaf = 1 << 8, - ImGuiTreeNodeFlags_Bullet = 1 << 9, - ImGuiTreeNodeFlags_FramePadding = 1 << 10, - ImGuiTreeNodeFlags_SpanAvailWidth = 1 << 11, - ImGuiTreeNodeFlags_SpanFullWidth = 1 << 12, - ImGuiTreeNodeFlags_SpanTextWidth = 1 << 13, - ImGuiTreeNodeFlags_SpanAllColumns = 1 << 14, - ImGuiTreeNodeFlags_NavLeftJumpsBackHere = 1 << 15, - ImGuiTreeNodeFlags_CollapsingHeader = ImGuiTreeNodeFlags_Framed | - ImGuiTreeNodeFlags_NoTreePushOnOpen | - ImGuiTreeNodeFlags_NoAutoOpenOnLog, -} ImGuiTreeNodeFlags_; -typedef enum { - ImGuiPopupFlags_None = 0, - ImGuiPopupFlags_MouseButtonLeft = 0, - ImGuiPopupFlags_MouseButtonRight = 1, - ImGuiPopupFlags_MouseButtonMiddle = 2, - ImGuiPopupFlags_MouseButtonMask_ = 0x1F, - ImGuiPopupFlags_MouseButtonDefault_ = 1, - ImGuiPopupFlags_NoReopen = 1 << 5, - ImGuiPopupFlags_NoOpenOverExistingPopup = 1 << 7, - ImGuiPopupFlags_NoOpenOverItems = 1 << 8, - ImGuiPopupFlags_AnyPopupId = 1 << 10, - ImGuiPopupFlags_AnyPopupLevel = 1 << 11, - ImGuiPopupFlags_AnyPopup = - ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel, -} ImGuiPopupFlags_; -typedef enum { - ImGuiSelectableFlags_None = 0, - ImGuiSelectableFlags_DontClosePopups = 1 << 0, - ImGuiSelectableFlags_SpanAllColumns = 1 << 1, - ImGuiSelectableFlags_AllowDoubleClick = 1 << 2, - ImGuiSelectableFlags_Disabled = 1 << 3, - ImGuiSelectableFlags_AllowOverlap = 1 << 4, -} ImGuiSelectableFlags_; -typedef enum { - ImGuiComboFlags_None = 0, - ImGuiComboFlags_PopupAlignLeft = 1 << 0, - ImGuiComboFlags_HeightSmall = 1 << 1, - ImGuiComboFlags_HeightRegular = 1 << 2, - ImGuiComboFlags_HeightLarge = 1 << 3, - ImGuiComboFlags_HeightLargest = 1 << 4, - ImGuiComboFlags_NoArrowButton = 1 << 5, - ImGuiComboFlags_NoPreview = 1 << 6, - ImGuiComboFlags_WidthFitPreview = 1 << 7, - ImGuiComboFlags_HeightMask_ = - ImGuiComboFlags_HeightSmall | ImGuiComboFlags_HeightRegular | - ImGuiComboFlags_HeightLarge | ImGuiComboFlags_HeightLargest, -} ImGuiComboFlags_; -typedef enum { - ImGuiTabBarFlags_None = 0, - ImGuiTabBarFlags_Reorderable = 1 << 0, - ImGuiTabBarFlags_AutoSelectNewTabs = 1 << 1, - ImGuiTabBarFlags_TabListPopupButton = 1 << 2, - ImGuiTabBarFlags_NoCloseWithMiddleMouseButton = 1 << 3, - ImGuiTabBarFlags_NoTabListScrollingButtons = 1 << 4, - ImGuiTabBarFlags_NoTooltip = 1 << 5, - ImGuiTabBarFlags_FittingPolicyResizeDown = 1 << 6, - ImGuiTabBarFlags_FittingPolicyScroll = 1 << 7, - ImGuiTabBarFlags_FittingPolicyMask_ = - ImGuiTabBarFlags_FittingPolicyResizeDown | - ImGuiTabBarFlags_FittingPolicyScroll, - ImGuiTabBarFlags_FittingPolicyDefault_ = - ImGuiTabBarFlags_FittingPolicyResizeDown, -} ImGuiTabBarFlags_; -typedef enum { - ImGuiTabItemFlags_None = 0, - ImGuiTabItemFlags_UnsavedDocument = 1 << 0, - ImGuiTabItemFlags_SetSelected = 1 << 1, - ImGuiTabItemFlags_NoCloseWithMiddleMouseButton = 1 << 2, - ImGuiTabItemFlags_NoPushId = 1 << 3, - ImGuiTabItemFlags_NoTooltip = 1 << 4, - ImGuiTabItemFlags_NoReorder = 1 << 5, - ImGuiTabItemFlags_Leading = 1 << 6, - ImGuiTabItemFlags_Trailing = 1 << 7, - ImGuiTabItemFlags_NoAssumedClosure = 1 << 8, -} ImGuiTabItemFlags_; -typedef enum { - ImGuiFocusedFlags_None = 0, - ImGuiFocusedFlags_ChildWindows = 1 << 0, - ImGuiFocusedFlags_RootWindow = 1 << 1, - ImGuiFocusedFlags_AnyWindow = 1 << 2, - ImGuiFocusedFlags_NoPopupHierarchy = 1 << 3, - ImGuiFocusedFlags_DockHierarchy = 1 << 4, - ImGuiFocusedFlags_RootAndChildWindows = - ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_ChildWindows, -} ImGuiFocusedFlags_; -typedef enum { - ImGuiHoveredFlags_None = 0, - ImGuiHoveredFlags_ChildWindows = 1 << 0, - ImGuiHoveredFlags_RootWindow = 1 << 1, - ImGuiHoveredFlags_AnyWindow = 1 << 2, - ImGuiHoveredFlags_NoPopupHierarchy = 1 << 3, - ImGuiHoveredFlags_DockHierarchy = 1 << 4, - ImGuiHoveredFlags_AllowWhenBlockedByPopup = 1 << 5, - ImGuiHoveredFlags_AllowWhenBlockedByActiveItem = 1 << 7, - ImGuiHoveredFlags_AllowWhenOverlappedByItem = 1 << 8, - ImGuiHoveredFlags_AllowWhenOverlappedByWindow = 1 << 9, - ImGuiHoveredFlags_AllowWhenDisabled = 1 << 10, - ImGuiHoveredFlags_NoNavOverride = 1 << 11, - ImGuiHoveredFlags_AllowWhenOverlapped = - ImGuiHoveredFlags_AllowWhenOverlappedByItem | - ImGuiHoveredFlags_AllowWhenOverlappedByWindow, - ImGuiHoveredFlags_RectOnly = ImGuiHoveredFlags_AllowWhenBlockedByPopup | - ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | - ImGuiHoveredFlags_AllowWhenOverlapped, - ImGuiHoveredFlags_RootAndChildWindows = - ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows, - ImGuiHoveredFlags_ForTooltip = 1 << 12, - ImGuiHoveredFlags_Stationary = 1 << 13, - ImGuiHoveredFlags_DelayNone = 1 << 14, - ImGuiHoveredFlags_DelayShort = 1 << 15, - ImGuiHoveredFlags_DelayNormal = 1 << 16, - ImGuiHoveredFlags_NoSharedDelay = 1 << 17, -} ImGuiHoveredFlags_; -typedef enum { - ImGuiDockNodeFlags_None = 0, - ImGuiDockNodeFlags_KeepAliveOnly = 1 << 0, - ImGuiDockNodeFlags_NoDockingOverCentralNode = 1 << 2, - ImGuiDockNodeFlags_PassthruCentralNode = 1 << 3, - ImGuiDockNodeFlags_NoDockingSplit = 1 << 4, - ImGuiDockNodeFlags_NoResize = 1 << 5, - ImGuiDockNodeFlags_AutoHideTabBar = 1 << 6, - ImGuiDockNodeFlags_NoUndocking = 1 << 7, -} ImGuiDockNodeFlags_; -typedef enum { - ImGuiDragDropFlags_None = 0, - ImGuiDragDropFlags_SourceNoPreviewTooltip = 1 << 0, - ImGuiDragDropFlags_SourceNoDisableHover = 1 << 1, - ImGuiDragDropFlags_SourceNoHoldToOpenOthers = 1 << 2, - ImGuiDragDropFlags_SourceAllowNullID = 1 << 3, - ImGuiDragDropFlags_SourceExtern = 1 << 4, - ImGuiDragDropFlags_SourceAutoExpirePayload = 1 << 5, - ImGuiDragDropFlags_AcceptBeforeDelivery = 1 << 10, - ImGuiDragDropFlags_AcceptNoDrawDefaultRect = 1 << 11, - ImGuiDragDropFlags_AcceptNoPreviewTooltip = 1 << 12, - ImGuiDragDropFlags_AcceptPeekOnly = - ImGuiDragDropFlags_AcceptBeforeDelivery | - ImGuiDragDropFlags_AcceptNoDrawDefaultRect, -} ImGuiDragDropFlags_; -typedef enum { - ImGuiDataType_S8, - ImGuiDataType_U8, - ImGuiDataType_S16, - ImGuiDataType_U16, - ImGuiDataType_S32, - ImGuiDataType_U32, - ImGuiDataType_S64, - ImGuiDataType_U64, - ImGuiDataType_Float, - ImGuiDataType_Double, - ImGuiDataType_COUNT -} ImGuiDataType_; -typedef enum { - ImGuiDir_None = -1, - ImGuiDir_Left = 0, - ImGuiDir_Right = 1, - ImGuiDir_Up = 2, - ImGuiDir_Down = 3, - ImGuiDir_COUNT -} ImGuiDir_; -typedef enum { - ImGuiSortDirection_None = 0, - ImGuiSortDirection_Ascending = 1, - ImGuiSortDirection_Descending = 2 -} ImGuiSortDirection_; -typedef enum { - ImGuiKey_None = 0, - ImGuiKey_Tab = 512, - ImGuiKey_LeftArrow = 513, - ImGuiKey_RightArrow = 514, - ImGuiKey_UpArrow = 515, - ImGuiKey_DownArrow = 516, - ImGuiKey_PageUp = 517, - ImGuiKey_PageDown = 518, - ImGuiKey_Home = 519, - ImGuiKey_End = 520, - ImGuiKey_Insert = 521, - ImGuiKey_Delete = 522, - ImGuiKey_Backspace = 523, - ImGuiKey_Space = 524, - ImGuiKey_Enter = 525, - ImGuiKey_Escape = 526, - ImGuiKey_LeftCtrl = 527, - ImGuiKey_LeftShift = 528, - ImGuiKey_LeftAlt = 529, - ImGuiKey_LeftSuper = 530, - ImGuiKey_RightCtrl = 531, - ImGuiKey_RightShift = 532, - ImGuiKey_RightAlt = 533, - ImGuiKey_RightSuper = 534, - ImGuiKey_Menu = 535, - ImGuiKey_0 = 536, - ImGuiKey_1 = 537, - ImGuiKey_2 = 538, - ImGuiKey_3 = 539, - ImGuiKey_4 = 540, - ImGuiKey_5 = 541, - ImGuiKey_6 = 542, - ImGuiKey_7 = 543, - ImGuiKey_8 = 544, - ImGuiKey_9 = 545, - ImGuiKey_A = 546, - ImGuiKey_B = 547, - ImGuiKey_C = 548, - ImGuiKey_D = 549, - ImGuiKey_E = 550, - ImGuiKey_F = 551, - ImGuiKey_G = 552, - ImGuiKey_H = 553, - ImGuiKey_I = 554, - ImGuiKey_J = 555, - ImGuiKey_K = 556, - ImGuiKey_L = 557, - ImGuiKey_M = 558, - ImGuiKey_N = 559, - ImGuiKey_O = 560, - ImGuiKey_P = 561, - ImGuiKey_Q = 562, - ImGuiKey_R = 563, - ImGuiKey_S = 564, - ImGuiKey_T = 565, - ImGuiKey_U = 566, - ImGuiKey_V = 567, - ImGuiKey_W = 568, - ImGuiKey_X = 569, - ImGuiKey_Y = 570, - ImGuiKey_Z = 571, - ImGuiKey_F1 = 572, - ImGuiKey_F2 = 573, - ImGuiKey_F3 = 574, - ImGuiKey_F4 = 575, - ImGuiKey_F5 = 576, - ImGuiKey_F6 = 577, - ImGuiKey_F7 = 578, - ImGuiKey_F8 = 579, - ImGuiKey_F9 = 580, - ImGuiKey_F10 = 581, - ImGuiKey_F11 = 582, - ImGuiKey_F12 = 583, - ImGuiKey_F13 = 584, - ImGuiKey_F14 = 585, - ImGuiKey_F15 = 586, - ImGuiKey_F16 = 587, - ImGuiKey_F17 = 588, - ImGuiKey_F18 = 589, - ImGuiKey_F19 = 590, - ImGuiKey_F20 = 591, - ImGuiKey_F21 = 592, - ImGuiKey_F22 = 593, - ImGuiKey_F23 = 594, - ImGuiKey_F24 = 595, - ImGuiKey_Apostrophe = 596, - ImGuiKey_Comma = 597, - ImGuiKey_Minus = 598, - ImGuiKey_Period = 599, - ImGuiKey_Slash = 600, - ImGuiKey_Semicolon = 601, - ImGuiKey_Equal = 602, - ImGuiKey_LeftBracket = 603, - ImGuiKey_Backslash = 604, - ImGuiKey_RightBracket = 605, - ImGuiKey_GraveAccent = 606, - ImGuiKey_CapsLock = 607, - ImGuiKey_ScrollLock = 608, - ImGuiKey_NumLock = 609, - ImGuiKey_PrintScreen = 610, - ImGuiKey_Pause = 611, - ImGuiKey_Keypad0 = 612, - ImGuiKey_Keypad1 = 613, - ImGuiKey_Keypad2 = 614, - ImGuiKey_Keypad3 = 615, - ImGuiKey_Keypad4 = 616, - ImGuiKey_Keypad5 = 617, - ImGuiKey_Keypad6 = 618, - ImGuiKey_Keypad7 = 619, - ImGuiKey_Keypad8 = 620, - ImGuiKey_Keypad9 = 621, - ImGuiKey_KeypadDecimal = 622, - ImGuiKey_KeypadDivide = 623, - ImGuiKey_KeypadMultiply = 624, - ImGuiKey_KeypadSubtract = 625, - ImGuiKey_KeypadAdd = 626, - ImGuiKey_KeypadEnter = 627, - ImGuiKey_KeypadEqual = 628, - ImGuiKey_AppBack = 629, - ImGuiKey_AppForward = 630, - ImGuiKey_GamepadStart = 631, - ImGuiKey_GamepadBack = 632, - ImGuiKey_GamepadFaceLeft = 633, - ImGuiKey_GamepadFaceRight = 634, - ImGuiKey_GamepadFaceUp = 635, - ImGuiKey_GamepadFaceDown = 636, - ImGuiKey_GamepadDpadLeft = 637, - ImGuiKey_GamepadDpadRight = 638, - ImGuiKey_GamepadDpadUp = 639, - ImGuiKey_GamepadDpadDown = 640, - ImGuiKey_GamepadL1 = 641, - ImGuiKey_GamepadR1 = 642, - ImGuiKey_GamepadL2 = 643, - ImGuiKey_GamepadR2 = 644, - ImGuiKey_GamepadL3 = 645, - ImGuiKey_GamepadR3 = 646, - ImGuiKey_GamepadLStickLeft = 647, - ImGuiKey_GamepadLStickRight = 648, - ImGuiKey_GamepadLStickUp = 649, - ImGuiKey_GamepadLStickDown = 650, - ImGuiKey_GamepadRStickLeft = 651, - ImGuiKey_GamepadRStickRight = 652, - ImGuiKey_GamepadRStickUp = 653, - ImGuiKey_GamepadRStickDown = 654, - ImGuiKey_MouseLeft = 655, - ImGuiKey_MouseRight = 656, - ImGuiKey_MouseMiddle = 657, - ImGuiKey_MouseX1 = 658, - ImGuiKey_MouseX2 = 659, - ImGuiKey_MouseWheelX = 660, - ImGuiKey_MouseWheelY = 661, - ImGuiKey_ReservedForModCtrl = 662, - ImGuiKey_ReservedForModShift = 663, - ImGuiKey_ReservedForModAlt = 664, - ImGuiKey_ReservedForModSuper = 665, - ImGuiKey_COUNT = 666, - ImGuiMod_None = 0, - ImGuiMod_Ctrl = 1 << 12, - ImGuiMod_Shift = 1 << 13, - ImGuiMod_Alt = 1 << 14, - ImGuiMod_Super = 1 << 15, - ImGuiMod_Shortcut = 1 << 11, - ImGuiMod_Mask_ = 0xF800, - ImGuiKey_NamedKey_BEGIN = 512, - ImGuiKey_NamedKey_END = ImGuiKey_COUNT, - ImGuiKey_NamedKey_COUNT = ImGuiKey_NamedKey_END - ImGuiKey_NamedKey_BEGIN, - ImGuiKey_KeysData_SIZE = ImGuiKey_NamedKey_COUNT, - ImGuiKey_KeysData_OFFSET = ImGuiKey_NamedKey_BEGIN, -} ImGuiKey; -typedef enum { - ImGuiConfigFlags_None = 0, - ImGuiConfigFlags_NavEnableKeyboard = 1 << 0, - ImGuiConfigFlags_NavEnableGamepad = 1 << 1, - ImGuiConfigFlags_NavEnableSetMousePos = 1 << 2, - ImGuiConfigFlags_NavNoCaptureKeyboard = 1 << 3, - ImGuiConfigFlags_NoMouse = 1 << 4, - ImGuiConfigFlags_NoMouseCursorChange = 1 << 5, - ImGuiConfigFlags_DockingEnable = 1 << 6, - ImGuiConfigFlags_ViewportsEnable = 1 << 10, - ImGuiConfigFlags_DpiEnableScaleViewports = 1 << 14, - ImGuiConfigFlags_DpiEnableScaleFonts = 1 << 15, - ImGuiConfigFlags_IsSRGB = 1 << 20, - ImGuiConfigFlags_IsTouchScreen = 1 << 21, -} ImGuiConfigFlags_; -typedef enum { - ImGuiBackendFlags_None = 0, - ImGuiBackendFlags_HasGamepad = 1 << 0, - ImGuiBackendFlags_HasMouseCursors = 1 << 1, - ImGuiBackendFlags_HasSetMousePos = 1 << 2, - ImGuiBackendFlags_RendererHasVtxOffset = 1 << 3, - ImGuiBackendFlags_PlatformHasViewports = 1 << 10, - ImGuiBackendFlags_HasMouseHoveredViewport = 1 << 11, - ImGuiBackendFlags_RendererHasViewports = 1 << 12, -} ImGuiBackendFlags_; -typedef enum { - ImGuiCol_Text, - ImGuiCol_TextDisabled, - ImGuiCol_WindowBg, - ImGuiCol_ChildBg, - ImGuiCol_PopupBg, - ImGuiCol_Border, - ImGuiCol_BorderShadow, - ImGuiCol_FrameBg, - ImGuiCol_FrameBgHovered, - ImGuiCol_FrameBgActive, - ImGuiCol_TitleBg, - ImGuiCol_TitleBgActive, - ImGuiCol_TitleBgCollapsed, - ImGuiCol_MenuBarBg, - ImGuiCol_ScrollbarBg, - ImGuiCol_ScrollbarGrab, - ImGuiCol_ScrollbarGrabHovered, - ImGuiCol_ScrollbarGrabActive, - ImGuiCol_CheckMark, - ImGuiCol_SliderGrab, - ImGuiCol_SliderGrabActive, - ImGuiCol_Button, - ImGuiCol_ButtonHovered, - ImGuiCol_ButtonActive, - ImGuiCol_Header, - ImGuiCol_HeaderHovered, - ImGuiCol_HeaderActive, - ImGuiCol_Separator, - ImGuiCol_SeparatorHovered, - ImGuiCol_SeparatorActive, - ImGuiCol_ResizeGrip, - ImGuiCol_ResizeGripHovered, - ImGuiCol_ResizeGripActive, - ImGuiCol_Tab, - ImGuiCol_TabHovered, - ImGuiCol_TabActive, - ImGuiCol_TabUnfocused, - ImGuiCol_TabUnfocusedActive, - ImGuiCol_DockingPreview, - ImGuiCol_DockingEmptyBg, - ImGuiCol_PlotLines, - ImGuiCol_PlotLinesHovered, - ImGuiCol_PlotHistogram, - ImGuiCol_PlotHistogramHovered, - ImGuiCol_TableHeaderBg, - ImGuiCol_TableBorderStrong, - ImGuiCol_TableBorderLight, - ImGuiCol_TableRowBg, - ImGuiCol_TableRowBgAlt, - ImGuiCol_TextSelectedBg, - ImGuiCol_DragDropTarget, - ImGuiCol_NavHighlight, - ImGuiCol_NavWindowingHighlight, - ImGuiCol_NavWindowingDimBg, - ImGuiCol_ModalWindowDimBg, - ImGuiCol_COUNT -} ImGuiCol_; -typedef enum { - ImGuiStyleVar_Alpha, - ImGuiStyleVar_DisabledAlpha, - ImGuiStyleVar_WindowPadding, - ImGuiStyleVar_WindowRounding, - ImGuiStyleVar_WindowBorderSize, - ImGuiStyleVar_WindowMinSize, - ImGuiStyleVar_WindowTitleAlign, - ImGuiStyleVar_ChildRounding, - ImGuiStyleVar_ChildBorderSize, - ImGuiStyleVar_PopupRounding, - ImGuiStyleVar_PopupBorderSize, - ImGuiStyleVar_FramePadding, - ImGuiStyleVar_FrameRounding, - ImGuiStyleVar_FrameBorderSize, - ImGuiStyleVar_ItemSpacing, - ImGuiStyleVar_ItemInnerSpacing, - ImGuiStyleVar_IndentSpacing, - ImGuiStyleVar_CellPadding, - ImGuiStyleVar_ScrollbarSize, - ImGuiStyleVar_ScrollbarRounding, - ImGuiStyleVar_GrabMinSize, - ImGuiStyleVar_GrabRounding, - ImGuiStyleVar_TabRounding, - ImGuiStyleVar_TabBorderSize, - ImGuiStyleVar_TabBarBorderSize, - ImGuiStyleVar_TableAngledHeadersAngle, - ImGuiStyleVar_TableAngledHeadersTextAlign, - ImGuiStyleVar_ButtonTextAlign, - ImGuiStyleVar_SelectableTextAlign, - ImGuiStyleVar_SeparatorTextBorderSize, - ImGuiStyleVar_SeparatorTextAlign, - ImGuiStyleVar_SeparatorTextPadding, - ImGuiStyleVar_DockingSeparatorSize, - ImGuiStyleVar_COUNT -} ImGuiStyleVar_; -typedef enum { - ImGuiButtonFlags_None = 0, - ImGuiButtonFlags_MouseButtonLeft = 1 << 0, - ImGuiButtonFlags_MouseButtonRight = 1 << 1, - ImGuiButtonFlags_MouseButtonMiddle = 1 << 2, - ImGuiButtonFlags_MouseButtonMask_ = ImGuiButtonFlags_MouseButtonLeft | - ImGuiButtonFlags_MouseButtonRight | - ImGuiButtonFlags_MouseButtonMiddle, - ImGuiButtonFlags_MouseButtonDefault_ = ImGuiButtonFlags_MouseButtonLeft, -} ImGuiButtonFlags_; -typedef enum { - ImGuiColorEditFlags_None = 0, - ImGuiColorEditFlags_NoAlpha = 1 << 1, - ImGuiColorEditFlags_NoPicker = 1 << 2, - ImGuiColorEditFlags_NoOptions = 1 << 3, - ImGuiColorEditFlags_NoSmallPreview = 1 << 4, - ImGuiColorEditFlags_NoInputs = 1 << 5, - ImGuiColorEditFlags_NoTooltip = 1 << 6, - ImGuiColorEditFlags_NoLabel = 1 << 7, - ImGuiColorEditFlags_NoSidePreview = 1 << 8, - ImGuiColorEditFlags_NoDragDrop = 1 << 9, - ImGuiColorEditFlags_NoBorder = 1 << 10, - ImGuiColorEditFlags_AlphaBar = 1 << 16, - ImGuiColorEditFlags_AlphaPreview = 1 << 17, - ImGuiColorEditFlags_AlphaPreviewHalf = 1 << 18, - ImGuiColorEditFlags_HDR = 1 << 19, - ImGuiColorEditFlags_DisplayRGB = 1 << 20, - ImGuiColorEditFlags_DisplayHSV = 1 << 21, - ImGuiColorEditFlags_DisplayHex = 1 << 22, - ImGuiColorEditFlags_Uint8 = 1 << 23, - ImGuiColorEditFlags_Float = 1 << 24, - ImGuiColorEditFlags_PickerHueBar = 1 << 25, - ImGuiColorEditFlags_PickerHueWheel = 1 << 26, - ImGuiColorEditFlags_InputRGB = 1 << 27, - ImGuiColorEditFlags_InputHSV = 1 << 28, - ImGuiColorEditFlags_DefaultOptions_ = - ImGuiColorEditFlags_Uint8 | ImGuiColorEditFlags_DisplayRGB | - ImGuiColorEditFlags_InputRGB | ImGuiColorEditFlags_PickerHueBar, - ImGuiColorEditFlags_DisplayMask_ = ImGuiColorEditFlags_DisplayRGB | - ImGuiColorEditFlags_DisplayHSV | - ImGuiColorEditFlags_DisplayHex, - ImGuiColorEditFlags_DataTypeMask_ = - ImGuiColorEditFlags_Uint8 | ImGuiColorEditFlags_Float, - ImGuiColorEditFlags_PickerMask_ = - ImGuiColorEditFlags_PickerHueWheel | ImGuiColorEditFlags_PickerHueBar, - ImGuiColorEditFlags_InputMask_ = - ImGuiColorEditFlags_InputRGB | ImGuiColorEditFlags_InputHSV, -} ImGuiColorEditFlags_; -typedef enum { - ImGuiSliderFlags_None = 0, - ImGuiSliderFlags_AlwaysClamp = 1 << 4, - ImGuiSliderFlags_Logarithmic = 1 << 5, - ImGuiSliderFlags_NoRoundToFormat = 1 << 6, - ImGuiSliderFlags_NoInput = 1 << 7, - ImGuiSliderFlags_InvalidMask_ = 0x7000000F, -} ImGuiSliderFlags_; -typedef enum { - ImGuiMouseButton_Left = 0, - ImGuiMouseButton_Right = 1, - ImGuiMouseButton_Middle = 2, - ImGuiMouseButton_COUNT = 5 -} ImGuiMouseButton_; -typedef enum { - ImGuiMouseCursor_None = -1, - ImGuiMouseCursor_Arrow = 0, - ImGuiMouseCursor_TextInput, - ImGuiMouseCursor_ResizeAll, - ImGuiMouseCursor_ResizeNS, - ImGuiMouseCursor_ResizeEW, - ImGuiMouseCursor_ResizeNESW, - ImGuiMouseCursor_ResizeNWSE, - ImGuiMouseCursor_Hand, - ImGuiMouseCursor_NotAllowed, - ImGuiMouseCursor_COUNT -} ImGuiMouseCursor_; -typedef enum { - ImGuiMouseSource_Mouse = 0, - ImGuiMouseSource_TouchScreen = 1, - ImGuiMouseSource_Pen = 2, - ImGuiMouseSource_COUNT = 3, -} ImGuiMouseSource; -typedef enum { - ImGuiCond_None = 0, - ImGuiCond_Always = 1 << 0, - ImGuiCond_Once = 1 << 1, - ImGuiCond_FirstUseEver = 1 << 2, - ImGuiCond_Appearing = 1 << 3, -} ImGuiCond_; -typedef enum { - ImGuiTableFlags_None = 0, - ImGuiTableFlags_Resizable = 1 << 0, - ImGuiTableFlags_Reorderable = 1 << 1, - ImGuiTableFlags_Hideable = 1 << 2, - ImGuiTableFlags_Sortable = 1 << 3, - ImGuiTableFlags_NoSavedSettings = 1 << 4, - ImGuiTableFlags_ContextMenuInBody = 1 << 5, - ImGuiTableFlags_RowBg = 1 << 6, - ImGuiTableFlags_BordersInnerH = 1 << 7, - ImGuiTableFlags_BordersOuterH = 1 << 8, - ImGuiTableFlags_BordersInnerV = 1 << 9, - ImGuiTableFlags_BordersOuterV = 1 << 10, - ImGuiTableFlags_BordersH = - ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_BordersOuterH, - ImGuiTableFlags_BordersV = - ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersOuterV, - ImGuiTableFlags_BordersInner = - ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersInnerH, - ImGuiTableFlags_BordersOuter = - ImGuiTableFlags_BordersOuterV | ImGuiTableFlags_BordersOuterH, - ImGuiTableFlags_Borders = - ImGuiTableFlags_BordersInner | ImGuiTableFlags_BordersOuter, - ImGuiTableFlags_NoBordersInBody = 1 << 11, - ImGuiTableFlags_NoBordersInBodyUntilResize = 1 << 12, - ImGuiTableFlags_SizingFixedFit = 1 << 13, - ImGuiTableFlags_SizingFixedSame = 2 << 13, - ImGuiTableFlags_SizingStretchProp = 3 << 13, - ImGuiTableFlags_SizingStretchSame = 4 << 13, - ImGuiTableFlags_NoHostExtendX = 1 << 16, - ImGuiTableFlags_NoHostExtendY = 1 << 17, - ImGuiTableFlags_NoKeepColumnsVisible = 1 << 18, - ImGuiTableFlags_PreciseWidths = 1 << 19, - ImGuiTableFlags_NoClip = 1 << 20, - ImGuiTableFlags_PadOuterX = 1 << 21, - ImGuiTableFlags_NoPadOuterX = 1 << 22, - ImGuiTableFlags_NoPadInnerX = 1 << 23, - ImGuiTableFlags_ScrollX = 1 << 24, - ImGuiTableFlags_ScrollY = 1 << 25, - ImGuiTableFlags_SortMulti = 1 << 26, - ImGuiTableFlags_SortTristate = 1 << 27, - ImGuiTableFlags_HighlightHoveredColumn = 1 << 28, - ImGuiTableFlags_SizingMask_ = - ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_SizingFixedSame | - ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_SizingStretchSame, -} ImGuiTableFlags_; -typedef enum { - ImGuiTableColumnFlags_None = 0, - ImGuiTableColumnFlags_Disabled = 1 << 0, - ImGuiTableColumnFlags_DefaultHide = 1 << 1, - ImGuiTableColumnFlags_DefaultSort = 1 << 2, - ImGuiTableColumnFlags_WidthStretch = 1 << 3, - ImGuiTableColumnFlags_WidthFixed = 1 << 4, - ImGuiTableColumnFlags_NoResize = 1 << 5, - ImGuiTableColumnFlags_NoReorder = 1 << 6, - ImGuiTableColumnFlags_NoHide = 1 << 7, - ImGuiTableColumnFlags_NoClip = 1 << 8, - ImGuiTableColumnFlags_NoSort = 1 << 9, - ImGuiTableColumnFlags_NoSortAscending = 1 << 10, - ImGuiTableColumnFlags_NoSortDescending = 1 << 11, - ImGuiTableColumnFlags_NoHeaderLabel = 1 << 12, - ImGuiTableColumnFlags_NoHeaderWidth = 1 << 13, - ImGuiTableColumnFlags_PreferSortAscending = 1 << 14, - ImGuiTableColumnFlags_PreferSortDescending = 1 << 15, - ImGuiTableColumnFlags_IndentEnable = 1 << 16, - ImGuiTableColumnFlags_IndentDisable = 1 << 17, - ImGuiTableColumnFlags_AngledHeader = 1 << 18, - ImGuiTableColumnFlags_IsEnabled = 1 << 24, - ImGuiTableColumnFlags_IsVisible = 1 << 25, - ImGuiTableColumnFlags_IsSorted = 1 << 26, - ImGuiTableColumnFlags_IsHovered = 1 << 27, - ImGuiTableColumnFlags_WidthMask_ = - ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_WidthFixed, - ImGuiTableColumnFlags_IndentMask_ = - ImGuiTableColumnFlags_IndentEnable | ImGuiTableColumnFlags_IndentDisable, - ImGuiTableColumnFlags_StatusMask_ = - ImGuiTableColumnFlags_IsEnabled | ImGuiTableColumnFlags_IsVisible | - ImGuiTableColumnFlags_IsSorted | ImGuiTableColumnFlags_IsHovered, - ImGuiTableColumnFlags_NoDirectResize_ = 1 << 30, -} ImGuiTableColumnFlags_; -typedef enum { - ImGuiTableRowFlags_None = 0, - ImGuiTableRowFlags_Headers = 1 << 0, -} ImGuiTableRowFlags_; -typedef enum { - ImGuiTableBgTarget_None = 0, - ImGuiTableBgTarget_RowBg0 = 1, - ImGuiTableBgTarget_RowBg1 = 2, - ImGuiTableBgTarget_CellBg = 3, -} ImGuiTableBgTarget_; -struct ImGuiTableSortSpecs { - const ImGuiTableColumnSortSpecs* Specs; - int SpecsCount; - bool SpecsDirty; -}; -struct ImGuiTableColumnSortSpecs { - ImGuiID ColumnUserID; - ImS16 ColumnIndex; - ImS16 SortOrder; - ImGuiSortDirection SortDirection : 8; -}; -struct ImGuiStyle { - float Alpha; - float DisabledAlpha; - ImVec2 WindowPadding; - float WindowRounding; - float WindowBorderSize; - ImVec2 WindowMinSize; - ImVec2 WindowTitleAlign; - ImGuiDir WindowMenuButtonPosition; - float ChildRounding; - float ChildBorderSize; - float PopupRounding; - float PopupBorderSize; - ImVec2 FramePadding; - float FrameRounding; - float FrameBorderSize; - ImVec2 ItemSpacing; - ImVec2 ItemInnerSpacing; - ImVec2 CellPadding; - ImVec2 TouchExtraPadding; - float IndentSpacing; - float ColumnsMinSpacing; - float ScrollbarSize; - float ScrollbarRounding; - float GrabMinSize; - float GrabRounding; - float LogSliderDeadzone; - float TabRounding; - float TabBorderSize; - float TabMinWidthForCloseButton; - float TabBarBorderSize; - float TableAngledHeadersAngle; - ImVec2 TableAngledHeadersTextAlign; - ImGuiDir ColorButtonPosition; - ImVec2 ButtonTextAlign; - ImVec2 SelectableTextAlign; - float SeparatorTextBorderSize; - ImVec2 SeparatorTextAlign; - ImVec2 SeparatorTextPadding; - ImVec2 DisplayWindowPadding; - ImVec2 DisplaySafeAreaPadding; - float DockingSeparatorSize; - float MouseCursorScale; - bool AntiAliasedLines; - bool AntiAliasedLinesUseTex; - bool AntiAliasedFill; - float CurveTessellationTol; - float CircleTessellationMaxError; - ImVec4 Colors[ImGuiCol_COUNT]; - float HoverStationaryDelay; - float HoverDelayShort; - float HoverDelayNormal; - ImGuiHoveredFlags HoverFlagsForTooltipMouse; - ImGuiHoveredFlags HoverFlagsForTooltipNav; -}; -struct ImGuiKeyData { - bool Down; - float DownDuration; - float DownDurationPrev; - float AnalogValue; -}; -typedef struct ImVector_ImWchar { - int Size; - int Capacity; - ImWchar* Data; -} ImVector_ImWchar; - -struct ImGuiIO { - ImGuiConfigFlags ConfigFlags; - ImGuiBackendFlags BackendFlags; - ImVec2 DisplaySize; - float DeltaTime; - float IniSavingRate; - const char* IniFilename; - const char* LogFilename; - void* UserData; - ImFontAtlas* Fonts; - float FontGlobalScale; - bool FontAllowUserScaling; - ImFont* FontDefault; - ImVec2 DisplayFramebufferScale; - bool ConfigDockingNoSplit; - bool ConfigDockingWithShift; - bool ConfigDockingAlwaysTabBar; - bool ConfigDockingTransparentPayload; - bool ConfigViewportsNoAutoMerge; - bool ConfigViewportsNoTaskBarIcon; - bool ConfigViewportsNoDecoration; - bool ConfigViewportsNoDefaultParent; - bool MouseDrawCursor; - bool ConfigMacOSXBehaviors; - bool ConfigInputTrickleEventQueue; - bool ConfigInputTextCursorBlink; - bool ConfigInputTextEnterKeepActive; - bool ConfigDragClickToInputText; - bool ConfigWindowsResizeFromEdges; - bool ConfigWindowsMoveFromTitleBarOnly; - float ConfigMemoryCompactTimer; - float MouseDoubleClickTime; - float MouseDoubleClickMaxDist; - float MouseDragThreshold; - float KeyRepeatDelay; - float KeyRepeatRate; - bool ConfigDebugIsDebuggerPresent; - bool ConfigDebugBeginReturnValueOnce; - bool ConfigDebugBeginReturnValueLoop; - bool ConfigDebugIgnoreFocusLoss; - bool ConfigDebugIniSettings; - const char* BackendPlatformName; - const char* BackendRendererName; - void* BackendPlatformUserData; - void* BackendRendererUserData; - void* BackendLanguageUserData; - const char* (*GetClipboardTextFn)(void* user_data); - void (*SetClipboardTextFn)(void* user_data, const char* text); - void* ClipboardUserData; - void (*SetPlatformImeDataFn)(ImGuiViewport* viewport, - ImGuiPlatformImeData* data); - ImWchar PlatformLocaleDecimalPoint; - bool WantCaptureMouse; - bool WantCaptureKeyboard; - bool WantTextInput; - bool WantSetMousePos; - bool WantSaveIniSettings; - bool NavActive; - bool NavVisible; - float Framerate; - int MetricsRenderVertices; - int MetricsRenderIndices; - int MetricsRenderWindows; - int MetricsActiveWindows; - ImVec2 MouseDelta; - ImGuiContext* Ctx; - ImVec2 MousePos; - bool MouseDown[5]; - float MouseWheel; - float MouseWheelH; - ImGuiMouseSource MouseSource; - ImGuiID MouseHoveredViewport; - bool KeyCtrl; - bool KeyShift; - bool KeyAlt; - bool KeySuper; - ImGuiKeyChord KeyMods; - ImGuiKeyData KeysData[ImGuiKey_KeysData_SIZE]; - bool WantCaptureMouseUnlessPopupClose; - ImVec2 MousePosPrev; - ImVec2 MouseClickedPos[5]; - double MouseClickedTime[5]; - bool MouseClicked[5]; - bool MouseDoubleClicked[5]; - ImU16 MouseClickedCount[5]; - ImU16 MouseClickedLastCount[5]; - bool MouseReleased[5]; - bool MouseDownOwned[5]; - bool MouseDownOwnedUnlessPopupClose[5]; - bool MouseWheelRequestAxisSwap; - float MouseDownDuration[5]; - float MouseDownDurationPrev[5]; - ImVec2 MouseDragMaxDistanceAbs[5]; - float MouseDragMaxDistanceSqr[5]; - float PenPressure; - bool AppFocusLost; - bool AppAcceptingEvents; - ImS8 BackendUsingLegacyKeyArrays; - bool BackendUsingLegacyNavInputArray; - ImWchar16 InputQueueSurrogate; - ImVector_ImWchar InputQueueCharacters; -}; -struct ImGuiInputTextCallbackData { - ImGuiContext* Ctx; - ImGuiInputTextFlags EventFlag; - ImGuiInputTextFlags Flags; - void* UserData; - ImWchar EventChar; - ImGuiKey EventKey; - char* Buf; - int BufTextLen; - int BufSize; - bool BufDirty; - int CursorPos; - int SelectionStart; - int SelectionEnd; -}; -struct ImGuiSizeCallbackData { - void* UserData; - ImVec2 Pos; - ImVec2 CurrentSize; - ImVec2 DesiredSize; -}; -struct ImGuiWindowClass { - ImGuiID ClassId; - ImGuiID ParentViewportId; - ImGuiID FocusRouteParentWindowId; - ImGuiViewportFlags ViewportFlagsOverrideSet; - ImGuiViewportFlags ViewportFlagsOverrideClear; - ImGuiTabItemFlags TabItemFlagsOverrideSet; - ImGuiDockNodeFlags DockNodeFlagsOverrideSet; - bool DockingAlwaysTabBar; - bool DockingAllowUnclassed; -}; -struct ImGuiPayload { - void* Data; - int DataSize; - ImGuiID SourceId; - ImGuiID SourceParentId; - int DataFrameCount; - char DataType[32 + 1]; - bool Preview; - bool Delivery; -}; -struct ImGuiOnceUponAFrame { - int RefFrame; -}; -struct ImGuiTextRange { - const char* b; - const char* e; -}; -typedef struct ImGuiTextRange ImGuiTextRange; - -typedef struct ImVector_ImGuiTextRange { - int Size; - int Capacity; - ImGuiTextRange* Data; -} ImVector_ImGuiTextRange; - -struct ImGuiTextFilter { - char InputBuf[256]; - ImVector_ImGuiTextRange Filters; - int CountGrep; -}; -typedef struct ImGuiTextRange ImGuiTextRange; -typedef struct ImVector_char { - int Size; - int Capacity; - char* Data; -} ImVector_char; - -struct ImGuiTextBuffer { - ImVector_char Buf; -}; -struct ImGuiStoragePair { - ImGuiID key; - union { - int val_i; - float val_f; - void* val_p; - }; -}; -typedef struct ImGuiStoragePair ImGuiStoragePair; - -typedef struct ImVector_ImGuiStoragePair { - int Size; - int Capacity; - ImGuiStoragePair* Data; -} ImVector_ImGuiStoragePair; - -struct ImGuiStorage { - ImVector_ImGuiStoragePair Data; -}; -typedef struct ImGuiStoragePair ImGuiStoragePair; -struct ImGuiListClipper { - ImGuiContext* Ctx; - int DisplayStart; - int DisplayEnd; - int ItemsCount; - float ItemsHeight; - float StartPosY; - void* TempData; -}; -struct ImColor { - ImVec4 Value; -}; -typedef void (*ImDrawCallback)(const ImDrawList* parent_list, - const ImDrawCmd* cmd); -struct ImDrawCmd { - ImVec4 ClipRect; - ImTextureID TextureId; - unsigned int VtxOffset; - unsigned int IdxOffset; - unsigned int ElemCount; - ImDrawCallback UserCallback; - void* UserCallbackData; -}; -struct ImDrawVert { - ImVec2 pos; - ImVec2 uv; - ImU32 col; -}; -typedef struct ImDrawCmdHeader ImDrawCmdHeader; -struct ImDrawCmdHeader { - ImVec4 ClipRect; - ImTextureID TextureId; - unsigned int VtxOffset; -}; -typedef struct ImVector_ImDrawCmd { - int Size; - int Capacity; - ImDrawCmd* Data; -} ImVector_ImDrawCmd; - -typedef struct ImVector_ImDrawIdx { - int Size; - int Capacity; - ImDrawIdx* Data; -} ImVector_ImDrawIdx; - -struct ImDrawChannel { - ImVector_ImDrawCmd _CmdBuffer; - ImVector_ImDrawIdx _IdxBuffer; -}; -typedef struct ImVector_ImDrawChannel { - int Size; - int Capacity; - ImDrawChannel* Data; -} ImVector_ImDrawChannel; - -struct ImDrawListSplitter { - int _Current; - int _Count; - ImVector_ImDrawChannel _Channels; -}; -typedef enum { - ImDrawFlags_None = 0, - ImDrawFlags_Closed = 1 << 0, - ImDrawFlags_RoundCornersTopLeft = 1 << 4, - ImDrawFlags_RoundCornersTopRight = 1 << 5, - ImDrawFlags_RoundCornersBottomLeft = 1 << 6, - ImDrawFlags_RoundCornersBottomRight = 1 << 7, - ImDrawFlags_RoundCornersNone = 1 << 8, - ImDrawFlags_RoundCornersTop = - ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight, - ImDrawFlags_RoundCornersBottom = - ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight, - ImDrawFlags_RoundCornersLeft = - ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersTopLeft, - ImDrawFlags_RoundCornersRight = - ImDrawFlags_RoundCornersBottomRight | ImDrawFlags_RoundCornersTopRight, - ImDrawFlags_RoundCornersAll = - ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight | - ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight, - ImDrawFlags_RoundCornersDefault_ = ImDrawFlags_RoundCornersAll, - ImDrawFlags_RoundCornersMask_ = - ImDrawFlags_RoundCornersAll | ImDrawFlags_RoundCornersNone, -} ImDrawFlags_; -typedef enum { - ImDrawListFlags_None = 0, - ImDrawListFlags_AntiAliasedLines = 1 << 0, - ImDrawListFlags_AntiAliasedLinesUseTex = 1 << 1, - ImDrawListFlags_AntiAliasedFill = 1 << 2, - ImDrawListFlags_AllowVtxOffset = 1 << 3, -} ImDrawListFlags_; -typedef struct ImVector_ImDrawVert { - int Size; - int Capacity; - ImDrawVert* Data; -} ImVector_ImDrawVert; - -typedef struct ImVector_ImVec2 { - int Size; - int Capacity; - ImVec2* Data; -} ImVector_ImVec2; - -typedef struct ImVector_ImVec4 { - int Size; - int Capacity; - ImVec4* Data; -} ImVector_ImVec4; - -typedef struct ImVector_ImTextureID { - int Size; - int Capacity; - ImTextureID* Data; -} ImVector_ImTextureID; - -struct ImDrawList { - ImVector_ImDrawCmd CmdBuffer; - ImVector_ImDrawIdx IdxBuffer; - ImVector_ImDrawVert VtxBuffer; - ImDrawListFlags Flags; - unsigned int _VtxCurrentIdx; - ImDrawListSharedData* _Data; - ImDrawVert* _VtxWritePtr; - ImDrawIdx* _IdxWritePtr; - ImVector_ImVec2 _Path; - ImDrawCmdHeader _CmdHeader; - ImDrawListSplitter _Splitter; - ImVector_ImVec4 _ClipRectStack; - ImVector_ImTextureID _TextureIdStack; - float _FringeScale; - const char* _OwnerName; -}; -typedef struct ImVector_ImDrawListPtr { - int Size; - int Capacity; - ImDrawList** Data; -} ImVector_ImDrawListPtr; - -struct ImDrawData { - bool Valid; - int CmdListsCount; - int TotalIdxCount; - int TotalVtxCount; - ImVector_ImDrawListPtr CmdLists; - ImVec2 DisplayPos; - ImVec2 DisplaySize; - ImVec2 FramebufferScale; - ImGuiViewport* OwnerViewport; -}; -struct ImFontConfig { - void* FontData; - int FontDataSize; - bool FontDataOwnedByAtlas; - int FontNo; - float SizePixels; - int OversampleH; - int OversampleV; - bool PixelSnapH; - ImVec2 GlyphExtraSpacing; - ImVec2 GlyphOffset; - const ImWchar* GlyphRanges; - float GlyphMinAdvanceX; - float GlyphMaxAdvanceX; - bool MergeMode; - unsigned int FontBuilderFlags; - float RasterizerMultiply; - float RasterizerDensity; - ImWchar EllipsisChar; - char Name[40]; - ImFont* DstFont; -}; -struct ImFontGlyph { - unsigned int Colored : 1; - unsigned int Visible : 1; - unsigned int Codepoint : 30; - float AdvanceX; - float X0, Y0, X1, Y1; - float U0, V0, U1, V1; -}; -typedef struct ImVector_ImU32 { - int Size; - int Capacity; - ImU32* Data; -} ImVector_ImU32; - -struct ImFontGlyphRangesBuilder { - ImVector_ImU32 UsedChars; -}; -typedef struct ImFontAtlasCustomRect ImFontAtlasCustomRect; -struct ImFontAtlasCustomRect { - unsigned short Width, Height; - unsigned short X, Y; - unsigned int GlyphID; - float GlyphAdvanceX; - ImVec2 GlyphOffset; - ImFont* Font; -}; -typedef enum { - ImFontAtlasFlags_None = 0, - ImFontAtlasFlags_NoPowerOfTwoHeight = 1 << 0, - ImFontAtlasFlags_NoMouseCursors = 1 << 1, - ImFontAtlasFlags_NoBakedLines = 1 << 2, -} ImFontAtlasFlags_; -typedef struct ImVector_ImFontPtr { - int Size; - int Capacity; - ImFont** Data; -} ImVector_ImFontPtr; - -typedef struct ImVector_ImFontAtlasCustomRect { - int Size; - int Capacity; - ImFontAtlasCustomRect* Data; -} ImVector_ImFontAtlasCustomRect; - -typedef struct ImVector_ImFontConfig { - int Size; - int Capacity; - ImFontConfig* Data; -} ImVector_ImFontConfig; - -struct ImFontAtlas { - ImFontAtlasFlags Flags; - ImTextureID TexID; - int TexDesiredWidth; - int TexGlyphPadding; - bool Locked; - void* UserData; - bool TexReady; - bool TexPixelsUseColors; - unsigned char* TexPixelsAlpha8; - unsigned int* TexPixelsRGBA32; - int TexWidth; - int TexHeight; - ImVec2 TexUvScale; - ImVec2 TexUvWhitePixel; - ImVector_ImFontPtr Fonts; - ImVector_ImFontAtlasCustomRect CustomRects; - ImVector_ImFontConfig ConfigData; - ImVec4 TexUvLines[(63) + 1]; - const ImFontBuilderIO* FontBuilderIO; - unsigned int FontBuilderFlags; - int PackIdMouseCursors; - int PackIdLines; -}; -typedef struct ImVector_float { - int Size; - int Capacity; - float* Data; -} ImVector_float; - -typedef struct ImVector_ImFontGlyph { - int Size; - int Capacity; - ImFontGlyph* Data; -} ImVector_ImFontGlyph; - -struct ImFont { - ImVector_float IndexAdvanceX; - float FallbackAdvanceX; - float FontSize; - ImVector_ImWchar IndexLookup; - ImVector_ImFontGlyph Glyphs; - const ImFontGlyph* FallbackGlyph; - ImFontAtlas* ContainerAtlas; - const ImFontConfig* ConfigData; - short ConfigDataCount; - ImWchar FallbackChar; - ImWchar EllipsisChar; - short EllipsisCharCount; - float EllipsisWidth; - float EllipsisCharStep; - bool DirtyLookupTables; - float Scale; - float Ascent, Descent; - int MetricsTotalSurface; - ImU8 Used4kPagesMap[(0xFFFF + 1) / 4096 / 8]; -}; -typedef enum { - ImGuiViewportFlags_None = 0, - ImGuiViewportFlags_IsPlatformWindow = 1 << 0, - ImGuiViewportFlags_IsPlatformMonitor = 1 << 1, - ImGuiViewportFlags_OwnedByApp = 1 << 2, - ImGuiViewportFlags_NoDecoration = 1 << 3, - ImGuiViewportFlags_NoTaskBarIcon = 1 << 4, - ImGuiViewportFlags_NoFocusOnAppearing = 1 << 5, - ImGuiViewportFlags_NoFocusOnClick = 1 << 6, - ImGuiViewportFlags_NoInputs = 1 << 7, - ImGuiViewportFlags_NoRendererClear = 1 << 8, - ImGuiViewportFlags_NoAutoMerge = 1 << 9, - ImGuiViewportFlags_TopMost = 1 << 10, - ImGuiViewportFlags_CanHostOtherWindows = 1 << 11, - ImGuiViewportFlags_IsMinimized = 1 << 12, - ImGuiViewportFlags_IsFocused = 1 << 13, -} ImGuiViewportFlags_; -struct ImGuiViewport { - ImGuiID ID; - ImGuiViewportFlags Flags; - ImVec2 Pos; - ImVec2 Size; - ImVec2 WorkPos; - ImVec2 WorkSize; - float DpiScale; - ImGuiID ParentViewportId; - ImDrawData* DrawData; - void* RendererUserData; - void* PlatformUserData; - void* PlatformHandle; - void* PlatformHandleRaw; - bool PlatformWindowCreated; - bool PlatformRequestMove; - bool PlatformRequestResize; - bool PlatformRequestClose; -}; -typedef struct ImVector_ImGuiPlatformMonitor { - int Size; - int Capacity; - ImGuiPlatformMonitor* Data; -} ImVector_ImGuiPlatformMonitor; - -typedef struct ImVector_ImGuiViewportPtr { - int Size; - int Capacity; - ImGuiViewport** Data; -} ImVector_ImGuiViewportPtr; - -struct ImGuiPlatformIO { - void (*Platform_CreateWindow)(ImGuiViewport* vp); - void (*Platform_DestroyWindow)(ImGuiViewport* vp); - void (*Platform_ShowWindow)(ImGuiViewport* vp); - void (*Platform_SetWindowPos)(ImGuiViewport* vp, ImVec2 pos); - ImVec2 (*Platform_GetWindowPos)(ImGuiViewport* vp); - void (*Platform_SetWindowSize)(ImGuiViewport* vp, ImVec2 size); - ImVec2 (*Platform_GetWindowSize)(ImGuiViewport* vp); - void (*Platform_SetWindowFocus)(ImGuiViewport* vp); - bool (*Platform_GetWindowFocus)(ImGuiViewport* vp); - bool (*Platform_GetWindowMinimized)(ImGuiViewport* vp); - void (*Platform_SetWindowTitle)(ImGuiViewport* vp, const char* str); - void (*Platform_SetWindowAlpha)(ImGuiViewport* vp, float alpha); - void (*Platform_UpdateWindow)(ImGuiViewport* vp); - void (*Platform_RenderWindow)(ImGuiViewport* vp, void* render_arg); - void (*Platform_SwapBuffers)(ImGuiViewport* vp, void* render_arg); - float (*Platform_GetWindowDpiScale)(ImGuiViewport* vp); - void (*Platform_OnChangedViewport)(ImGuiViewport* vp); - int (*Platform_CreateVkSurface)(ImGuiViewport* vp, - ImU64 vk_inst, - const void* vk_allocators, - ImU64* out_vk_surface); - void (*Renderer_CreateWindow)(ImGuiViewport* vp); - void (*Renderer_DestroyWindow)(ImGuiViewport* vp); - void (*Renderer_SetWindowSize)(ImGuiViewport* vp, ImVec2 size); - void (*Renderer_RenderWindow)(ImGuiViewport* vp, void* render_arg); - void (*Renderer_SwapBuffers)(ImGuiViewport* vp, void* render_arg); - ImVector_ImGuiPlatformMonitor Monitors; - ImVector_ImGuiViewportPtr Viewports; -}; -struct ImGuiPlatformMonitor { - ImVec2 MainPos, MainSize; - ImVec2 WorkPos, WorkSize; - float DpiScale; - void* PlatformHandle; -}; -struct ImGuiPlatformImeData { - bool WantVisible; - ImVec2 InputPos; - float InputLineHeight; -}; -struct ImBitVector; -struct ImRect; -struct ImDrawDataBuilder; -struct ImDrawListSharedData; -struct ImGuiColorMod; -struct ImGuiContext; -struct ImGuiContextHook; -struct ImGuiDataVarInfo; -struct ImGuiDataTypeInfo; -struct ImGuiDockContext; -struct ImGuiDockRequest; -struct ImGuiDockNode; -struct ImGuiDockNodeSettings; -struct ImGuiGroupData; -struct ImGuiInputTextState; -struct ImGuiInputTextDeactivateData; -struct ImGuiLastItemData; -struct ImGuiLocEntry; -struct ImGuiMenuColumns; -struct ImGuiNavItemData; -struct ImGuiNavTreeNodeData; -struct ImGuiMetricsConfig; -struct ImGuiNextWindowData; -struct ImGuiNextItemData; -struct ImGuiOldColumnData; -struct ImGuiOldColumns; -struct ImGuiPopupData; -struct ImGuiSettingsHandler; -struct ImGuiStackSizes; -struct ImGuiStyleMod; -struct ImGuiTabBar; -struct ImGuiTabItem; -struct ImGuiTable; -struct ImGuiTableHeaderData; -struct ImGuiTableColumn; -struct ImGuiTableInstanceData; -struct ImGuiTableTempData; -struct ImGuiTableSettings; -struct ImGuiTableColumnsSettings; -struct ImGuiTypingSelectState; -struct ImGuiTypingSelectRequest; -struct ImGuiWindow; -struct ImGuiWindowDockStyle; -struct ImGuiWindowTempData; -struct ImGuiWindowSettings; -typedef int ImGuiDataAuthority; -typedef int ImGuiLayoutType; -typedef int ImGuiActivateFlags; -typedef int ImGuiDebugLogFlags; -typedef int ImGuiFocusRequestFlags; -typedef int ImGuiInputFlags; -typedef int ImGuiItemFlags; -typedef int ImGuiItemStatusFlags; -typedef int ImGuiOldColumnFlags; -typedef int ImGuiNavHighlightFlags; -typedef int ImGuiNavMoveFlags; -typedef int ImGuiNextItemDataFlags; -typedef int ImGuiNextWindowDataFlags; -typedef int ImGuiScrollFlags; -typedef int ImGuiSeparatorFlags; -typedef int ImGuiTextFlags; -typedef int ImGuiTooltipFlags; -typedef int ImGuiTypingSelectFlags; -typedef int ImGuiWindowRefreshFlags; -typedef void (*ImGuiErrorLogCallback)(void* user_data, const char* fmt, ...); -extern ImGuiContext* GImGui; -typedef struct StbUndoRecord StbUndoRecord; -struct StbUndoRecord { - int where; - int insert_length; - int delete_length; - int char_storage; -}; -typedef struct StbUndoState StbUndoState; -struct StbUndoState { - StbUndoRecord undo_rec[99]; - ImWchar undo_char[999]; - short undo_point, redo_point; - int undo_char_point, redo_char_point; -}; -typedef struct STB_TexteditState STB_TexteditState; -struct STB_TexteditState { - int cursor; - int select_start; - int select_end; - unsigned char insert_mode; - int row_count_per_page; - unsigned char cursor_at_end_of_line; - unsigned char initialized; - unsigned char has_preferred_x; - unsigned char single_line; - unsigned char padding1, padding2, padding3; - float preferred_x; - StbUndoState undostate; -}; -typedef struct StbTexteditRow StbTexteditRow; -struct StbTexteditRow { - float x0, x1; - float baseline_y_delta; - float ymin, ymax; - int num_chars; -}; -typedef FILE* ImFileHandle; -typedef struct ImVec1 ImVec1; -struct ImVec1 { - float x; -}; -typedef struct ImVec2ih ImVec2ih; -struct ImVec2ih { - short x, y; -}; -struct ImRect { - ImVec2 Min; - ImVec2 Max; -}; -typedef ImU32* ImBitArrayPtr; -struct ImBitVector { - ImVector_ImU32 Storage; -}; -typedef int ImPoolIdx; -typedef struct ImGuiTextIndex ImGuiTextIndex; -typedef struct ImVector_int { - int Size; - int Capacity; - int* Data; -} ImVector_int; - -struct ImGuiTextIndex { - ImVector_int LineOffsets; - int EndOffset; -}; -struct ImDrawListSharedData { - ImVec2 TexUvWhitePixel; - ImFont* Font; - float FontSize; - float CurveTessellationTol; - float CircleSegmentMaxError; - ImVec4 ClipRectFullscreen; - ImDrawListFlags InitialFlags; - ImVector_ImVec2 TempBuffer; - ImVec2 ArcFastVtx[48]; - float ArcFastRadiusCutoff; - ImU8 CircleSegmentCounts[64]; - const ImVec4* TexUvLines; -}; -struct ImDrawDataBuilder { - ImVector_ImDrawListPtr* Layers[2]; - ImVector_ImDrawListPtr LayerData1; -}; -typedef enum { - ImGuiItemFlags_None = 0, - ImGuiItemFlags_NoTabStop = 1 << 0, - ImGuiItemFlags_ButtonRepeat = 1 << 1, - ImGuiItemFlags_Disabled = 1 << 2, - ImGuiItemFlags_NoNav = 1 << 3, - ImGuiItemFlags_NoNavDefaultFocus = 1 << 4, - ImGuiItemFlags_SelectableDontClosePopup = 1 << 5, - ImGuiItemFlags_MixedValue = 1 << 6, - ImGuiItemFlags_ReadOnly = 1 << 7, - ImGuiItemFlags_NoWindowHoverableCheck = 1 << 8, - ImGuiItemFlags_AllowOverlap = 1 << 9, - ImGuiItemFlags_Inputable = 1 << 10, - ImGuiItemFlags_HasSelectionUserData = 1 << 11, -} ImGuiItemFlags_; -typedef enum { - ImGuiItemStatusFlags_None = 0, - ImGuiItemStatusFlags_HoveredRect = 1 << 0, - ImGuiItemStatusFlags_HasDisplayRect = 1 << 1, - ImGuiItemStatusFlags_Edited = 1 << 2, - ImGuiItemStatusFlags_ToggledSelection = 1 << 3, - ImGuiItemStatusFlags_ToggledOpen = 1 << 4, - ImGuiItemStatusFlags_HasDeactivated = 1 << 5, - ImGuiItemStatusFlags_Deactivated = 1 << 6, - ImGuiItemStatusFlags_HoveredWindow = 1 << 7, - ImGuiItemStatusFlags_Visible = 1 << 8, - ImGuiItemStatusFlags_HasClipRect = 1 << 9, -} ImGuiItemStatusFlags_; -typedef enum { - ImGuiHoveredFlags_DelayMask_ = - ImGuiHoveredFlags_DelayNone | ImGuiHoveredFlags_DelayShort | - ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay, - ImGuiHoveredFlags_AllowedMaskForIsWindowHovered = - ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_RootWindow | - ImGuiHoveredFlags_AnyWindow | ImGuiHoveredFlags_NoPopupHierarchy | - ImGuiHoveredFlags_DockHierarchy | - ImGuiHoveredFlags_AllowWhenBlockedByPopup | - ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | - ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_Stationary, - ImGuiHoveredFlags_AllowedMaskForIsItemHovered = - ImGuiHoveredFlags_AllowWhenBlockedByPopup | - ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | - ImGuiHoveredFlags_AllowWhenOverlapped | - ImGuiHoveredFlags_AllowWhenDisabled | ImGuiHoveredFlags_NoNavOverride | - ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_Stationary | - ImGuiHoveredFlags_DelayMask_, -} ImGuiHoveredFlagsPrivate_; -typedef enum { - ImGuiInputTextFlags_Multiline = 1 << 26, - ImGuiInputTextFlags_NoMarkEdited = 1 << 27, - ImGuiInputTextFlags_MergedItem = 1 << 28, - ImGuiInputTextFlags_LocalizeDecimalPoint = 1 << 29, -} ImGuiInputTextFlagsPrivate_; -typedef enum { - ImGuiButtonFlags_PressedOnClick = 1 << 4, - ImGuiButtonFlags_PressedOnClickRelease = 1 << 5, - ImGuiButtonFlags_PressedOnClickReleaseAnywhere = 1 << 6, - ImGuiButtonFlags_PressedOnRelease = 1 << 7, - ImGuiButtonFlags_PressedOnDoubleClick = 1 << 8, - ImGuiButtonFlags_PressedOnDragDropHold = 1 << 9, - ImGuiButtonFlags_Repeat = 1 << 10, - ImGuiButtonFlags_FlattenChildren = 1 << 11, - ImGuiButtonFlags_AllowOverlap = 1 << 12, - ImGuiButtonFlags_DontClosePopups = 1 << 13, - ImGuiButtonFlags_AlignTextBaseLine = 1 << 15, - ImGuiButtonFlags_NoKeyModifiers = 1 << 16, - ImGuiButtonFlags_NoHoldingActiveId = 1 << 17, - ImGuiButtonFlags_NoNavFocus = 1 << 18, - ImGuiButtonFlags_NoHoveredOnFocus = 1 << 19, - ImGuiButtonFlags_NoSetKeyOwner = 1 << 20, - ImGuiButtonFlags_NoTestKeyOwner = 1 << 21, - ImGuiButtonFlags_PressedOnMask_ = - ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnClickRelease | - ImGuiButtonFlags_PressedOnClickReleaseAnywhere | - ImGuiButtonFlags_PressedOnRelease | - ImGuiButtonFlags_PressedOnDoubleClick | - ImGuiButtonFlags_PressedOnDragDropHold, - ImGuiButtonFlags_PressedOnDefault_ = ImGuiButtonFlags_PressedOnClickRelease, -} ImGuiButtonFlagsPrivate_; -typedef enum { - ImGuiComboFlags_CustomPreview = 1 << 20, -} ImGuiComboFlagsPrivate_; -typedef enum { - ImGuiSliderFlags_Vertical = 1 << 20, - ImGuiSliderFlags_ReadOnly = 1 << 21, -} ImGuiSliderFlagsPrivate_; -typedef enum { - ImGuiSelectableFlags_NoHoldingActiveID = 1 << 20, - ImGuiSelectableFlags_SelectOnNav = 1 << 21, - ImGuiSelectableFlags_SelectOnClick = 1 << 22, - ImGuiSelectableFlags_SelectOnRelease = 1 << 23, - ImGuiSelectableFlags_SpanAvailWidth = 1 << 24, - ImGuiSelectableFlags_SetNavIdOnHover = 1 << 25, - ImGuiSelectableFlags_NoPadWithHalfSpacing = 1 << 26, - ImGuiSelectableFlags_NoSetKeyOwner = 1 << 27, -} ImGuiSelectableFlagsPrivate_; -typedef enum { - ImGuiTreeNodeFlags_ClipLabelForTrailingButton = 1 << 20, - ImGuiTreeNodeFlags_UpsideDownArrow = 1 << 21, -} ImGuiTreeNodeFlagsPrivate_; -typedef enum { - ImGuiSeparatorFlags_None = 0, - ImGuiSeparatorFlags_Horizontal = 1 << 0, - ImGuiSeparatorFlags_Vertical = 1 << 1, - ImGuiSeparatorFlags_SpanAllColumns = 1 << 2, -} ImGuiSeparatorFlags_; -typedef enum { - ImGuiFocusRequestFlags_None = 0, - ImGuiFocusRequestFlags_RestoreFocusedChild = 1 << 0, - ImGuiFocusRequestFlags_UnlessBelowModal = 1 << 1, -} ImGuiFocusRequestFlags_; -typedef enum { - ImGuiTextFlags_None = 0, - ImGuiTextFlags_NoWidthForLargeClippedText = 1 << 0, -} ImGuiTextFlags_; -typedef enum { - ImGuiTooltipFlags_None = 0, - ImGuiTooltipFlags_OverridePrevious = 1 << 1, -} ImGuiTooltipFlags_; -typedef enum { - ImGuiLayoutType_Horizontal = 0, - ImGuiLayoutType_Vertical = 1 -} ImGuiLayoutType_; -typedef enum { - ImGuiLogType_None = 0, - ImGuiLogType_TTY, - ImGuiLogType_File, - ImGuiLogType_Buffer, - ImGuiLogType_Clipboard, -} ImGuiLogType; -typedef enum { - ImGuiAxis_None = -1, - ImGuiAxis_X = 0, - ImGuiAxis_Y = 1 -} ImGuiAxis; -typedef enum { - ImGuiPlotType_Lines, - ImGuiPlotType_Histogram, -} ImGuiPlotType; -struct ImGuiColorMod { - ImGuiCol Col; - ImVec4 BackupValue; -}; -struct ImGuiStyleMod { - ImGuiStyleVar VarIdx; - union { - int BackupInt[2]; - float BackupFloat[2]; - }; -}; -typedef struct ImGuiComboPreviewData ImGuiComboPreviewData; -struct ImGuiComboPreviewData { - ImRect PreviewRect; - ImVec2 BackupCursorPos; - ImVec2 BackupCursorMaxPos; - ImVec2 BackupCursorPosPrevLine; - float BackupPrevLineTextBaseOffset; - ImGuiLayoutType BackupLayout; -}; -struct ImGuiGroupData { - ImGuiID WindowID; - ImVec2 BackupCursorPos; - ImVec2 BackupCursorMaxPos; - ImVec2 BackupCursorPosPrevLine; - ImVec1 BackupIndent; - ImVec1 BackupGroupOffset; - ImVec2 BackupCurrLineSize; - float BackupCurrLineTextBaseOffset; - ImGuiID BackupActiveIdIsAlive; - bool BackupActiveIdPreviousFrameIsAlive; - bool BackupHoveredIdIsAlive; - bool BackupIsSameLine; - bool EmitItem; -}; -struct ImGuiMenuColumns { - ImU32 TotalWidth; - ImU32 NextTotalWidth; - ImU16 Spacing; - ImU16 OffsetIcon; - ImU16 OffsetLabel; - ImU16 OffsetShortcut; - ImU16 OffsetMark; - ImU16 Widths[4]; -}; -typedef struct ImGuiInputTextDeactivatedState ImGuiInputTextDeactivatedState; -struct ImGuiInputTextDeactivatedState { - ImGuiID ID; - ImVector_char TextA; -}; -struct ImGuiInputTextState { - ImGuiContext* Ctx; - ImGuiID ID; - int CurLenW, CurLenA; - ImVector_ImWchar TextW; - ImVector_char TextA; - ImVector_char InitialTextA; - bool TextAIsValid; - int BufCapacityA; - float ScrollX; - STB_TexteditState Stb; - float CursorAnim; - bool CursorFollow; - bool SelectedAllMouseLock; - bool Edited; - ImGuiInputTextFlags Flags; - bool ReloadUserBuf; - int ReloadSelectionStart; - int ReloadSelectionEnd; -}; -typedef enum { - ImGuiWindowRefreshFlags_None = 0, - ImGuiWindowRefreshFlags_TryToAvoidRefresh = 1 << 0, - ImGuiWindowRefreshFlags_RefreshOnHover = 1 << 1, - ImGuiWindowRefreshFlags_RefreshOnFocus = 1 << 2, -} ImGuiWindowRefreshFlags_; -typedef enum { - ImGuiNextWindowDataFlags_None = 0, - ImGuiNextWindowDataFlags_HasPos = 1 << 0, - ImGuiNextWindowDataFlags_HasSize = 1 << 1, - ImGuiNextWindowDataFlags_HasContentSize = 1 << 2, - ImGuiNextWindowDataFlags_HasCollapsed = 1 << 3, - ImGuiNextWindowDataFlags_HasSizeConstraint = 1 << 4, - ImGuiNextWindowDataFlags_HasFocus = 1 << 5, - ImGuiNextWindowDataFlags_HasBgAlpha = 1 << 6, - ImGuiNextWindowDataFlags_HasScroll = 1 << 7, - ImGuiNextWindowDataFlags_HasChildFlags = 1 << 8, - ImGuiNextWindowDataFlags_HasRefreshPolicy = 1 << 9, - ImGuiNextWindowDataFlags_HasViewport = 1 << 10, - ImGuiNextWindowDataFlags_HasDock = 1 << 11, - ImGuiNextWindowDataFlags_HasWindowClass = 1 << 12, -} ImGuiNextWindowDataFlags_; -struct ImGuiNextWindowData { - ImGuiNextWindowDataFlags Flags; - ImGuiCond PosCond; - ImGuiCond SizeCond; - ImGuiCond CollapsedCond; - ImGuiCond DockCond; - ImVec2 PosVal; - ImVec2 PosPivotVal; - ImVec2 SizeVal; - ImVec2 ContentSizeVal; - ImVec2 ScrollVal; - ImGuiChildFlags ChildFlags; - bool PosUndock; - bool CollapsedVal; - ImRect SizeConstraintRect; - ImGuiSizeCallback SizeCallback; - void* SizeCallbackUserData; - float BgAlphaVal; - ImGuiID ViewportId; - ImGuiID DockId; - ImGuiWindowClass WindowClass; - ImVec2 MenuBarOffsetMinVal; - ImGuiWindowRefreshFlags RefreshFlagsVal; -}; -typedef ImS64 ImGuiSelectionUserData; -typedef enum { - ImGuiNextItemDataFlags_None = 0, - ImGuiNextItemDataFlags_HasWidth = 1 << 0, - ImGuiNextItemDataFlags_HasOpen = 1 << 1, - ImGuiNextItemDataFlags_HasShortcut = 1 << 2, -} ImGuiNextItemDataFlags_; -struct ImGuiNextItemData { - ImGuiNextItemDataFlags Flags; - ImGuiItemFlags ItemFlags; - ImGuiSelectionUserData SelectionUserData; - float Width; - ImGuiKeyChord Shortcut; - bool OpenVal; - ImGuiCond OpenCond : 8; -}; -struct ImGuiLastItemData { - ImGuiID ID; - ImGuiItemFlags InFlags; - ImGuiItemStatusFlags StatusFlags; - ImRect Rect; - ImRect NavRect; - ImRect DisplayRect; - ImRect ClipRect; -}; -struct ImGuiNavTreeNodeData { - ImGuiID ID; - ImGuiItemFlags InFlags; - ImRect NavRect; -}; -struct ImGuiStackSizes { - short SizeOfIDStack; - short SizeOfColorStack; - short SizeOfStyleVarStack; - short SizeOfFontStack; - short SizeOfFocusScopeStack; - short SizeOfGroupStack; - short SizeOfItemFlagsStack; - short SizeOfBeginPopupStack; - short SizeOfDisabledStack; -}; -typedef struct ImGuiWindowStackData ImGuiWindowStackData; -struct ImGuiWindowStackData { - ImGuiWindow* Window; - ImGuiLastItemData ParentLastItemDataBackup; - ImGuiStackSizes StackSizesOnBegin; -}; -typedef struct ImGuiShrinkWidthItem ImGuiShrinkWidthItem; -struct ImGuiShrinkWidthItem { - int Index; - float Width; - float InitialWidth; -}; -typedef struct ImGuiPtrOrIndex ImGuiPtrOrIndex; -struct ImGuiPtrOrIndex { - void* Ptr; - int Index; -}; -struct ImGuiDataVarInfo { - ImGuiDataType Type; - ImU32 Count; - ImU32 Offset; -}; -typedef struct ImGuiDataTypeTempStorage ImGuiDataTypeTempStorage; -struct ImGuiDataTypeTempStorage { - ImU8 Data[8]; -}; -struct ImGuiDataTypeInfo { - size_t Size; - const char* Name; - const char* PrintFmt; - const char* ScanFmt; -}; -typedef enum { - ImGuiDataType_String = ImGuiDataType_COUNT + 1, - ImGuiDataType_Pointer, - ImGuiDataType_ID, -} ImGuiDataTypePrivate_; -typedef enum { - ImGuiPopupPositionPolicy_Default, - ImGuiPopupPositionPolicy_ComboBox, - ImGuiPopupPositionPolicy_Tooltip, -} ImGuiPopupPositionPolicy; -struct ImGuiPopupData { - ImGuiID PopupId; - ImGuiWindow* Window; - ImGuiWindow* RestoreNavWindow; - int ParentNavLayer; - int OpenFrameCount; - ImGuiID OpenParentId; - ImVec2 OpenPopupPos; - ImVec2 OpenMousePos; -}; -typedef struct ImBitArray_ImGuiKey_NamedKey_COUNT__lessImGuiKey_NamedKey_BEGIN { - ImU32 Storage[(ImGuiKey_NamedKey_COUNT + 31) >> 5]; -} ImBitArray_ImGuiKey_NamedKey_COUNT__lessImGuiKey_NamedKey_BEGIN; - -typedef ImBitArray_ImGuiKey_NamedKey_COUNT__lessImGuiKey_NamedKey_BEGIN - ImBitArrayForNamedKeys; -typedef enum { - ImGuiInputEventType_None = 0, - ImGuiInputEventType_MousePos, - ImGuiInputEventType_MouseWheel, - ImGuiInputEventType_MouseButton, - ImGuiInputEventType_MouseViewport, - ImGuiInputEventType_Key, - ImGuiInputEventType_Text, - ImGuiInputEventType_Focus, - ImGuiInputEventType_COUNT -} ImGuiInputEventType; -typedef enum { - ImGuiInputSource_None = 0, - ImGuiInputSource_Mouse, - ImGuiInputSource_Keyboard, - ImGuiInputSource_Gamepad, - ImGuiInputSource_COUNT -} ImGuiInputSource; -typedef struct ImGuiInputEventMousePos ImGuiInputEventMousePos; -struct ImGuiInputEventMousePos { - float PosX, PosY; - ImGuiMouseSource MouseSource; -}; -typedef struct ImGuiInputEventMouseWheel ImGuiInputEventMouseWheel; -struct ImGuiInputEventMouseWheel { - float WheelX, WheelY; - ImGuiMouseSource MouseSource; -}; -typedef struct ImGuiInputEventMouseButton ImGuiInputEventMouseButton; -struct ImGuiInputEventMouseButton { - int Button; - bool Down; - ImGuiMouseSource MouseSource; -}; -typedef struct ImGuiInputEventMouseViewport ImGuiInputEventMouseViewport; -struct ImGuiInputEventMouseViewport { - ImGuiID HoveredViewportID; -}; -typedef struct ImGuiInputEventKey ImGuiInputEventKey; -struct ImGuiInputEventKey { - ImGuiKey Key; - bool Down; - float AnalogValue; -}; -typedef struct ImGuiInputEventText ImGuiInputEventText; -struct ImGuiInputEventText { - unsigned int Char; -}; -typedef struct ImGuiInputEventAppFocused ImGuiInputEventAppFocused; -struct ImGuiInputEventAppFocused { - bool Focused; -}; -typedef struct ImGuiInputEvent ImGuiInputEvent; -struct ImGuiInputEvent { - ImGuiInputEventType Type; - ImGuiInputSource Source; - ImU32 EventId; - union { - ImGuiInputEventMousePos MousePos; - ImGuiInputEventMouseWheel MouseWheel; - ImGuiInputEventMouseButton MouseButton; - ImGuiInputEventMouseViewport MouseViewport; - ImGuiInputEventKey Key; - ImGuiInputEventText Text; - ImGuiInputEventAppFocused AppFocused; - }; - bool AddedByTestEngine; -}; -typedef ImS16 ImGuiKeyRoutingIndex; -typedef struct ImGuiKeyRoutingData ImGuiKeyRoutingData; -struct ImGuiKeyRoutingData { - ImGuiKeyRoutingIndex NextEntryIndex; - ImU16 Mods; - ImU8 RoutingCurrScore; - ImU8 RoutingNextScore; - ImGuiID RoutingCurr; - ImGuiID RoutingNext; -}; -typedef struct ImGuiKeyRoutingTable ImGuiKeyRoutingTable; -typedef struct ImVector_ImGuiKeyRoutingData { - int Size; - int Capacity; - ImGuiKeyRoutingData* Data; -} ImVector_ImGuiKeyRoutingData; - -struct ImGuiKeyRoutingTable { - ImGuiKeyRoutingIndex Index[ImGuiKey_NamedKey_COUNT]; - ImVector_ImGuiKeyRoutingData Entries; - ImVector_ImGuiKeyRoutingData EntriesNext; -}; -typedef struct ImGuiKeyOwnerData ImGuiKeyOwnerData; -struct ImGuiKeyOwnerData { - ImGuiID OwnerCurr; - ImGuiID OwnerNext; - bool LockThisFrame; - bool LockUntilRelease; -}; -typedef enum { - ImGuiInputFlags_None = 0, - ImGuiInputFlags_Repeat = 1 << 0, - ImGuiInputFlags_RepeatRateDefault = 1 << 1, - ImGuiInputFlags_RepeatRateNavMove = 1 << 2, - ImGuiInputFlags_RepeatRateNavTweak = 1 << 3, - ImGuiInputFlags_RepeatUntilRelease = 1 << 4, - ImGuiInputFlags_RepeatUntilKeyModsChange = 1 << 5, - ImGuiInputFlags_RepeatUntilKeyModsChangeFromNone = 1 << 6, - ImGuiInputFlags_RepeatUntilOtherKeyPress = 1 << 7, - ImGuiInputFlags_CondHovered = 1 << 8, - ImGuiInputFlags_CondActive = 1 << 9, - ImGuiInputFlags_CondDefault_ = - ImGuiInputFlags_CondHovered | ImGuiInputFlags_CondActive, - ImGuiInputFlags_LockThisFrame = 1 << 10, - ImGuiInputFlags_LockUntilRelease = 1 << 11, - ImGuiInputFlags_RouteFocused = 1 << 12, - ImGuiInputFlags_RouteGlobalLow = 1 << 13, - ImGuiInputFlags_RouteGlobal = 1 << 14, - ImGuiInputFlags_RouteGlobalHigh = 1 << 15, - ImGuiInputFlags_RouteAlways = 1 << 16, - ImGuiInputFlags_RouteUnlessBgFocused = 1 << 17, - ImGuiInputFlags_RepeatRateMask_ = ImGuiInputFlags_RepeatRateDefault | - ImGuiInputFlags_RepeatRateNavMove | - ImGuiInputFlags_RepeatRateNavTweak, - ImGuiInputFlags_RepeatUntilMask_ = - ImGuiInputFlags_RepeatUntilRelease | - ImGuiInputFlags_RepeatUntilKeyModsChange | - ImGuiInputFlags_RepeatUntilKeyModsChangeFromNone | - ImGuiInputFlags_RepeatUntilOtherKeyPress, - ImGuiInputFlags_RepeatMask_ = ImGuiInputFlags_Repeat | - ImGuiInputFlags_RepeatRateMask_ | - ImGuiInputFlags_RepeatUntilMask_, - ImGuiInputFlags_CondMask_ = - ImGuiInputFlags_CondHovered | ImGuiInputFlags_CondActive, - ImGuiInputFlags_RouteMask_ = - ImGuiInputFlags_RouteFocused | ImGuiInputFlags_RouteGlobal | - ImGuiInputFlags_RouteGlobalLow | ImGuiInputFlags_RouteGlobalHigh, - ImGuiInputFlags_SupportedByIsKeyPressed = ImGuiInputFlags_RepeatMask_, - ImGuiInputFlags_SupportedByIsMouseClicked = ImGuiInputFlags_Repeat, - ImGuiInputFlags_SupportedByShortcut = - ImGuiInputFlags_RepeatMask_ | ImGuiInputFlags_RouteMask_ | - ImGuiInputFlags_RouteAlways | ImGuiInputFlags_RouteUnlessBgFocused, - ImGuiInputFlags_SupportedBySetKeyOwner = - ImGuiInputFlags_LockThisFrame | ImGuiInputFlags_LockUntilRelease, - ImGuiInputFlags_SupportedBySetItemKeyOwner = - ImGuiInputFlags_SupportedBySetKeyOwner | ImGuiInputFlags_CondMask_, -} ImGuiInputFlags_; -typedef struct ImGuiListClipperRange ImGuiListClipperRange; -struct ImGuiListClipperRange { - int Min; - int Max; - bool PosToIndexConvert; - ImS8 PosToIndexOffsetMin; - ImS8 PosToIndexOffsetMax; -}; -typedef struct ImGuiListClipperData ImGuiListClipperData; -typedef struct ImVector_ImGuiListClipperRange { - int Size; - int Capacity; - ImGuiListClipperRange* Data; -} ImVector_ImGuiListClipperRange; - -struct ImGuiListClipperData { - ImGuiListClipper* ListClipper; - float LossynessOffset; - int StepNo; - int ItemsFrozen; - ImVector_ImGuiListClipperRange Ranges; -}; -typedef enum { - ImGuiActivateFlags_None = 0, - ImGuiActivateFlags_PreferInput = 1 << 0, - ImGuiActivateFlags_PreferTweak = 1 << 1, - ImGuiActivateFlags_TryToPreserveState = 1 << 2, - ImGuiActivateFlags_FromTabbing = 1 << 3, - ImGuiActivateFlags_FromShortcut = 1 << 4, -} ImGuiActivateFlags_; -typedef enum { - ImGuiScrollFlags_None = 0, - ImGuiScrollFlags_KeepVisibleEdgeX = 1 << 0, - ImGuiScrollFlags_KeepVisibleEdgeY = 1 << 1, - ImGuiScrollFlags_KeepVisibleCenterX = 1 << 2, - ImGuiScrollFlags_KeepVisibleCenterY = 1 << 3, - ImGuiScrollFlags_AlwaysCenterX = 1 << 4, - ImGuiScrollFlags_AlwaysCenterY = 1 << 5, - ImGuiScrollFlags_NoScrollParent = 1 << 6, - ImGuiScrollFlags_MaskX_ = ImGuiScrollFlags_KeepVisibleEdgeX | - ImGuiScrollFlags_KeepVisibleCenterX | - ImGuiScrollFlags_AlwaysCenterX, - ImGuiScrollFlags_MaskY_ = ImGuiScrollFlags_KeepVisibleEdgeY | - ImGuiScrollFlags_KeepVisibleCenterY | - ImGuiScrollFlags_AlwaysCenterY, -} ImGuiScrollFlags_; -typedef enum { - ImGuiNavHighlightFlags_None = 0, - ImGuiNavHighlightFlags_Compact = 1 << 1, - ImGuiNavHighlightFlags_AlwaysDraw = 1 << 2, - ImGuiNavHighlightFlags_NoRounding = 1 << 3, -} ImGuiNavHighlightFlags_; -typedef enum { - ImGuiNavMoveFlags_None = 0, - ImGuiNavMoveFlags_LoopX = 1 << 0, - ImGuiNavMoveFlags_LoopY = 1 << 1, - ImGuiNavMoveFlags_WrapX = 1 << 2, - ImGuiNavMoveFlags_WrapY = 1 << 3, - ImGuiNavMoveFlags_WrapMask_ = - ImGuiNavMoveFlags_LoopX | ImGuiNavMoveFlags_LoopY | - ImGuiNavMoveFlags_WrapX | ImGuiNavMoveFlags_WrapY, - ImGuiNavMoveFlags_AllowCurrentNavId = 1 << 4, - ImGuiNavMoveFlags_AlsoScoreVisibleSet = 1 << 5, - ImGuiNavMoveFlags_ScrollToEdgeY = 1 << 6, - ImGuiNavMoveFlags_Forwarded = 1 << 7, - ImGuiNavMoveFlags_DebugNoResult = 1 << 8, - ImGuiNavMoveFlags_FocusApi = 1 << 9, - ImGuiNavMoveFlags_IsTabbing = 1 << 10, - ImGuiNavMoveFlags_IsPageMove = 1 << 11, - ImGuiNavMoveFlags_Activate = 1 << 12, - ImGuiNavMoveFlags_NoSelect = 1 << 13, - ImGuiNavMoveFlags_NoSetNavHighlight = 1 << 14, - ImGuiNavMoveFlags_NoClearActiveId = 1 << 15, -} ImGuiNavMoveFlags_; -typedef enum { - ImGuiNavLayer_Main = 0, - ImGuiNavLayer_Menu = 1, - ImGuiNavLayer_COUNT -} ImGuiNavLayer; -struct ImGuiNavItemData { - ImGuiWindow* Window; - ImGuiID ID; - ImGuiID FocusScopeId; - ImRect RectRel; - ImGuiItemFlags InFlags; - float DistBox; - float DistCenter; - float DistAxial; - ImGuiSelectionUserData SelectionUserData; -}; -typedef struct ImGuiFocusScopeData ImGuiFocusScopeData; -struct ImGuiFocusScopeData { - ImGuiID ID; - ImGuiID WindowID; -}; -typedef enum { - ImGuiTypingSelectFlags_None = 0, - ImGuiTypingSelectFlags_AllowBackspace = 1 << 0, - ImGuiTypingSelectFlags_AllowSingleCharMode = 1 << 1, -} ImGuiTypingSelectFlags_; -struct ImGuiTypingSelectRequest { - ImGuiTypingSelectFlags Flags; - int SearchBufferLen; - const char* SearchBuffer; - bool SelectRequest; - bool SingleCharMode; - ImS8 SingleCharSize; -}; -struct ImGuiTypingSelectState { - ImGuiTypingSelectRequest Request; - char SearchBuffer[64]; - ImGuiID FocusScope; - int LastRequestFrame; - float LastRequestTime; - bool SingleCharModeLock; -}; -typedef enum { - ImGuiOldColumnFlags_None = 0, - ImGuiOldColumnFlags_NoBorder = 1 << 0, - ImGuiOldColumnFlags_NoResize = 1 << 1, - ImGuiOldColumnFlags_NoPreserveWidths = 1 << 2, - ImGuiOldColumnFlags_NoForceWithinWindow = 1 << 3, - ImGuiOldColumnFlags_GrowParentContentsSize = 1 << 4, -} ImGuiOldColumnFlags_; -struct ImGuiOldColumnData { - float OffsetNorm; - float OffsetNormBeforeResize; - ImGuiOldColumnFlags Flags; - ImRect ClipRect; -}; -typedef struct ImVector_ImGuiOldColumnData { - int Size; - int Capacity; - ImGuiOldColumnData* Data; -} ImVector_ImGuiOldColumnData; - -struct ImGuiOldColumns { - ImGuiID ID; - ImGuiOldColumnFlags Flags; - bool IsFirstFrame; - bool IsBeingResized; - int Current; - int Count; - float OffMinX, OffMaxX; - float LineMinY, LineMaxY; - float HostCursorPosY; - float HostCursorMaxPosX; - ImRect HostInitialClipRect; - ImRect HostBackupClipRect; - ImRect HostBackupParentWorkRect; - ImVector_ImGuiOldColumnData Columns; - ImDrawListSplitter Splitter; -}; -typedef enum { - ImGuiDockNodeFlags_DockSpace = 1 << 10, - ImGuiDockNodeFlags_CentralNode = 1 << 11, - ImGuiDockNodeFlags_NoTabBar = 1 << 12, - ImGuiDockNodeFlags_HiddenTabBar = 1 << 13, - ImGuiDockNodeFlags_NoWindowMenuButton = 1 << 14, - ImGuiDockNodeFlags_NoCloseButton = 1 << 15, - ImGuiDockNodeFlags_NoResizeX = 1 << 16, - ImGuiDockNodeFlags_NoResizeY = 1 << 17, - ImGuiDockNodeFlags_DockedWindowsInFocusRoute = 1 << 18, - ImGuiDockNodeFlags_NoDockingSplitOther = 1 << 19, - ImGuiDockNodeFlags_NoDockingOverMe = 1 << 20, - ImGuiDockNodeFlags_NoDockingOverOther = 1 << 21, - ImGuiDockNodeFlags_NoDockingOverEmpty = 1 << 22, - ImGuiDockNodeFlags_NoDocking = ImGuiDockNodeFlags_NoDockingOverMe | - ImGuiDockNodeFlags_NoDockingOverOther | - ImGuiDockNodeFlags_NoDockingOverEmpty | - ImGuiDockNodeFlags_NoDockingSplit | - ImGuiDockNodeFlags_NoDockingSplitOther, - ImGuiDockNodeFlags_SharedFlagsInheritMask_ = ~0, - ImGuiDockNodeFlags_NoResizeFlagsMask_ = ImGuiDockNodeFlags_NoResize | - ImGuiDockNodeFlags_NoResizeX | - ImGuiDockNodeFlags_NoResizeY, - ImGuiDockNodeFlags_LocalFlagsTransferMask_ = - ImGuiDockNodeFlags_NoDockingSplit | - ImGuiDockNodeFlags_NoResizeFlagsMask_ | - ImGuiDockNodeFlags_AutoHideTabBar | ImGuiDockNodeFlags_CentralNode | - ImGuiDockNodeFlags_NoTabBar | ImGuiDockNodeFlags_HiddenTabBar | - ImGuiDockNodeFlags_NoWindowMenuButton | ImGuiDockNodeFlags_NoCloseButton, - ImGuiDockNodeFlags_SavedFlagsMask_ = - ImGuiDockNodeFlags_NoResizeFlagsMask_ | ImGuiDockNodeFlags_DockSpace | - ImGuiDockNodeFlags_CentralNode | ImGuiDockNodeFlags_NoTabBar | - ImGuiDockNodeFlags_HiddenTabBar | ImGuiDockNodeFlags_NoWindowMenuButton | - ImGuiDockNodeFlags_NoCloseButton, -} ImGuiDockNodeFlagsPrivate_; -typedef enum { - ImGuiDataAuthority_Auto, - ImGuiDataAuthority_DockNode, - ImGuiDataAuthority_Window, -} ImGuiDataAuthority_; -typedef enum { - ImGuiDockNodeState_Unknown, - ImGuiDockNodeState_HostWindowHiddenBecauseSingleWindow, - ImGuiDockNodeState_HostWindowHiddenBecauseWindowsAreResizing, - ImGuiDockNodeState_HostWindowVisible, -} ImGuiDockNodeState; -typedef struct ImVector_ImGuiWindowPtr { - int Size; - int Capacity; - ImGuiWindow** Data; -} ImVector_ImGuiWindowPtr; - -struct ImGuiDockNode { - ImGuiID ID; - ImGuiDockNodeFlags SharedFlags; - ImGuiDockNodeFlags LocalFlags; - ImGuiDockNodeFlags LocalFlagsInWindows; - ImGuiDockNodeFlags MergedFlags; - ImGuiDockNodeState State; - ImGuiDockNode* ParentNode; - ImGuiDockNode* ChildNodes[2]; - ImVector_ImGuiWindowPtr Windows; - ImGuiTabBar* TabBar; - ImVec2 Pos; - ImVec2 Size; - ImVec2 SizeRef; - ImGuiAxis SplitAxis; - ImGuiWindowClass WindowClass; - ImU32 LastBgColor; - ImGuiWindow* HostWindow; - ImGuiWindow* VisibleWindow; - ImGuiDockNode* CentralNode; - ImGuiDockNode* OnlyNodeWithWindows; - int CountNodeWithWindows; - int LastFrameAlive; - int LastFrameActive; - int LastFrameFocused; - ImGuiID LastFocusedNodeId; - ImGuiID SelectedTabId; - ImGuiID WantCloseTabId; - ImGuiID RefViewportId; - ImGuiDataAuthority AuthorityForPos : 3; - ImGuiDataAuthority AuthorityForSize : 3; - ImGuiDataAuthority AuthorityForViewport : 3; - bool IsVisible : 1; - bool IsFocused : 1; - bool IsBgDrawnThisFrame : 1; - bool HasCloseButton : 1; - bool HasWindowMenuButton : 1; - bool HasCentralNodeChild : 1; - bool WantCloseAll : 1; - bool WantLockSizeOnce : 1; - bool WantMouseMove : 1; - bool WantHiddenTabBarUpdate : 1; - bool WantHiddenTabBarToggle : 1; -}; -typedef enum { - ImGuiWindowDockStyleCol_Text, - ImGuiWindowDockStyleCol_Tab, - ImGuiWindowDockStyleCol_TabHovered, - ImGuiWindowDockStyleCol_TabActive, - ImGuiWindowDockStyleCol_TabUnfocused, - ImGuiWindowDockStyleCol_TabUnfocusedActive, - ImGuiWindowDockStyleCol_COUNT -} ImGuiWindowDockStyleCol; -struct ImGuiWindowDockStyle { - ImU32 Colors[ImGuiWindowDockStyleCol_COUNT]; -}; -typedef struct ImVector_ImGuiDockRequest { - int Size; - int Capacity; - ImGuiDockRequest* Data; -} ImVector_ImGuiDockRequest; - -typedef struct ImVector_ImGuiDockNodeSettings { - int Size; - int Capacity; - ImGuiDockNodeSettings* Data; -} ImVector_ImGuiDockNodeSettings; - -struct ImGuiDockContext { - ImGuiStorage Nodes; - ImVector_ImGuiDockRequest Requests; - ImVector_ImGuiDockNodeSettings NodesSettings; - bool WantFullRebuild; -}; -typedef struct ImGuiViewportP ImGuiViewportP; -struct ImGuiViewportP { - ImGuiViewport _ImGuiViewport; - ImGuiWindow* Window; - int Idx; - int LastFrameActive; - int LastFocusedStampCount; - ImGuiID LastNameHash; - ImVec2 LastPos; - float Alpha; - float LastAlpha; - bool LastFocusedHadNavWindow; - short PlatformMonitor; - int BgFgDrawListsLastFrame[2]; - ImDrawList* BgFgDrawLists[2]; - ImDrawData DrawDataP; - ImDrawDataBuilder DrawDataBuilder; - ImVec2 LastPlatformPos; - ImVec2 LastPlatformSize; - ImVec2 LastRendererSize; - ImVec2 WorkOffsetMin; - ImVec2 WorkOffsetMax; - ImVec2 BuildWorkOffsetMin; - ImVec2 BuildWorkOffsetMax; -}; -struct ImGuiWindowSettings { - ImGuiID ID; - ImVec2ih Pos; - ImVec2ih Size; - ImVec2ih ViewportPos; - ImGuiID ViewportId; - ImGuiID DockId; - ImGuiID ClassId; - short DockOrder; - bool Collapsed; - bool IsChild; - bool WantApply; - bool WantDelete; -}; -struct ImGuiSettingsHandler { - const char* TypeName; - ImGuiID TypeHash; - void (*ClearAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler); - void (*ReadInitFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler); - void* (*ReadOpenFn)(ImGuiContext* ctx, - ImGuiSettingsHandler* handler, - const char* name); - void (*ReadLineFn)(ImGuiContext* ctx, - ImGuiSettingsHandler* handler, - void* entry, - const char* line); - void (*ApplyAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler); - void (*WriteAllFn)(ImGuiContext* ctx, - ImGuiSettingsHandler* handler, - ImGuiTextBuffer* out_buf); - void* UserData; -}; -typedef enum { - ImGuiLocKey_VersionStr = 0, - ImGuiLocKey_TableSizeOne = 1, - ImGuiLocKey_TableSizeAllFit = 2, - ImGuiLocKey_TableSizeAllDefault = 3, - ImGuiLocKey_TableResetOrder = 4, - ImGuiLocKey_WindowingMainMenuBar = 5, - ImGuiLocKey_WindowingPopup = 6, - ImGuiLocKey_WindowingUntitled = 7, - ImGuiLocKey_DockingHideTabBar = 8, - ImGuiLocKey_DockingHoldShiftToDock = 9, - ImGuiLocKey_DockingDragToUndockOrMoveNode = 10, - ImGuiLocKey_COUNT = 11, -} ImGuiLocKey; -struct ImGuiLocEntry { - ImGuiLocKey Key; - const char* Text; -}; -typedef enum { - ImGuiDebugLogFlags_None = 0, - ImGuiDebugLogFlags_EventActiveId = 1 << 0, - ImGuiDebugLogFlags_EventFocus = 1 << 1, - ImGuiDebugLogFlags_EventPopup = 1 << 2, - ImGuiDebugLogFlags_EventNav = 1 << 3, - ImGuiDebugLogFlags_EventClipper = 1 << 4, - ImGuiDebugLogFlags_EventSelection = 1 << 5, - ImGuiDebugLogFlags_EventIO = 1 << 6, - ImGuiDebugLogFlags_EventInputRouting = 1 << 7, - ImGuiDebugLogFlags_EventDocking = 1 << 8, - ImGuiDebugLogFlags_EventViewport = 1 << 9, - ImGuiDebugLogFlags_EventMask_ = - ImGuiDebugLogFlags_EventActiveId | ImGuiDebugLogFlags_EventFocus | - ImGuiDebugLogFlags_EventPopup | ImGuiDebugLogFlags_EventNav | - ImGuiDebugLogFlags_EventClipper | ImGuiDebugLogFlags_EventSelection | - ImGuiDebugLogFlags_EventIO | ImGuiDebugLogFlags_EventInputRouting | - ImGuiDebugLogFlags_EventDocking | ImGuiDebugLogFlags_EventViewport, - ImGuiDebugLogFlags_OutputToTTY = 1 << 20, - ImGuiDebugLogFlags_OutputToTestEngine = 1 << 21, -} ImGuiDebugLogFlags_; -typedef struct ImGuiDebugAllocEntry ImGuiDebugAllocEntry; -struct ImGuiDebugAllocEntry { - int FrameCount; - ImS16 AllocCount; - ImS16 FreeCount; -}; -typedef struct ImGuiDebugAllocInfo ImGuiDebugAllocInfo; -struct ImGuiDebugAllocInfo { - int TotalAllocCount; - int TotalFreeCount; - ImS16 LastEntriesIdx; - ImGuiDebugAllocEntry LastEntriesBuf[6]; -}; -struct ImGuiMetricsConfig { - bool ShowDebugLog; - bool ShowIDStackTool; - bool ShowWindowsRects; - bool ShowWindowsBeginOrder; - bool ShowTablesRects; - bool ShowDrawCmdMesh; - bool ShowDrawCmdBoundingBoxes; - bool ShowTextEncodingViewer; - bool ShowAtlasTintedWithTextColor; - bool ShowDockingNodes; - int ShowWindowsRectsType; - int ShowTablesRectsType; - int HighlightMonitorIdx; - ImGuiID HighlightViewportID; -}; -typedef struct ImGuiStackLevelInfo ImGuiStackLevelInfo; -struct ImGuiStackLevelInfo { - ImGuiID ID; - ImS8 QueryFrameCount; - bool QuerySuccess; - ImGuiDataType DataType : 8; - char Desc[57]; -}; -typedef struct ImGuiIDStackTool ImGuiIDStackTool; -typedef struct ImVector_ImGuiStackLevelInfo { - int Size; - int Capacity; - ImGuiStackLevelInfo* Data; -} ImVector_ImGuiStackLevelInfo; - -struct ImGuiIDStackTool { - int LastActiveFrame; - int StackLevel; - ImGuiID QueryId; - ImVector_ImGuiStackLevelInfo Results; - bool CopyToClipboardOnCtrlC; - float CopyToClipboardLastTime; -}; -typedef void (*ImGuiContextHookCallback)(ImGuiContext* ctx, - ImGuiContextHook* hook); -typedef enum { - ImGuiContextHookType_NewFramePre, - ImGuiContextHookType_NewFramePost, - ImGuiContextHookType_EndFramePre, - ImGuiContextHookType_EndFramePost, - ImGuiContextHookType_RenderPre, - ImGuiContextHookType_RenderPost, - ImGuiContextHookType_Shutdown, - ImGuiContextHookType_PendingRemoval_ -} ImGuiContextHookType; -struct ImGuiContextHook { - ImGuiID HookId; - ImGuiContextHookType Type; - ImGuiID Owner; - ImGuiContextHookCallback Callback; - void* UserData; -}; -typedef struct ImVector_ImGuiInputEvent { - int Size; - int Capacity; - ImGuiInputEvent* Data; -} ImVector_ImGuiInputEvent; - -typedef struct ImVector_ImGuiWindowStackData { - int Size; - int Capacity; - ImGuiWindowStackData* Data; -} ImVector_ImGuiWindowStackData; - -typedef struct ImVector_ImGuiColorMod { - int Size; - int Capacity; - ImGuiColorMod* Data; -} ImVector_ImGuiColorMod; - -typedef struct ImVector_ImGuiStyleMod { - int Size; - int Capacity; - ImGuiStyleMod* Data; -} ImVector_ImGuiStyleMod; - -typedef struct ImVector_ImGuiFocusScopeData { - int Size; - int Capacity; - ImGuiFocusScopeData* Data; -} ImVector_ImGuiFocusScopeData; - -typedef struct ImVector_ImGuiItemFlags { - int Size; - int Capacity; - ImGuiItemFlags* Data; -} ImVector_ImGuiItemFlags; - -typedef struct ImVector_ImGuiGroupData { - int Size; - int Capacity; - ImGuiGroupData* Data; -} ImVector_ImGuiGroupData; - -typedef struct ImVector_ImGuiPopupData { - int Size; - int Capacity; - ImGuiPopupData* Data; -} ImVector_ImGuiPopupData; - -typedef struct ImVector_ImGuiNavTreeNodeData { - int Size; - int Capacity; - ImGuiNavTreeNodeData* Data; -} ImVector_ImGuiNavTreeNodeData; - -typedef struct ImVector_ImGuiViewportPPtr { - int Size; - int Capacity; - ImGuiViewportP** Data; -} ImVector_ImGuiViewportPPtr; - -typedef struct ImVector_unsigned_char { - int Size; - int Capacity; - unsigned char* Data; -} ImVector_unsigned_char; - -typedef struct ImVector_ImGuiListClipperData { - int Size; - int Capacity; - ImGuiListClipperData* Data; -} ImVector_ImGuiListClipperData; - -typedef struct ImVector_ImGuiTableTempData { - int Size; - int Capacity; - ImGuiTableTempData* Data; -} ImVector_ImGuiTableTempData; - -typedef struct ImVector_ImGuiTable { - int Size; - int Capacity; - ImGuiTable* Data; -} ImVector_ImGuiTable; - -typedef struct ImPool_ImGuiTable { - ImVector_ImGuiTable Buf; - ImGuiStorage Map; - ImPoolIdx FreeIdx; - ImPoolIdx AliveCount; -} ImPool_ImGuiTable; - -typedef struct ImVector_ImGuiTabBar { - int Size; - int Capacity; - ImGuiTabBar* Data; -} ImVector_ImGuiTabBar; - -typedef struct ImPool_ImGuiTabBar { - ImVector_ImGuiTabBar Buf; - ImGuiStorage Map; - ImPoolIdx FreeIdx; - ImPoolIdx AliveCount; -} ImPool_ImGuiTabBar; - -typedef struct ImVector_ImGuiPtrOrIndex { - int Size; - int Capacity; - ImGuiPtrOrIndex* Data; -} ImVector_ImGuiPtrOrIndex; - -typedef struct ImVector_ImGuiShrinkWidthItem { - int Size; - int Capacity; - ImGuiShrinkWidthItem* Data; -} ImVector_ImGuiShrinkWidthItem; - -typedef struct ImVector_ImGuiID { - int Size; - int Capacity; - ImGuiID* Data; -} ImVector_ImGuiID; - -typedef struct ImVector_ImGuiSettingsHandler { - int Size; - int Capacity; - ImGuiSettingsHandler* Data; -} ImVector_ImGuiSettingsHandler; - -typedef struct ImChunkStream_ImGuiWindowSettings { - ImVector_char Buf; -} ImChunkStream_ImGuiWindowSettings; - -typedef struct ImChunkStream_ImGuiTableSettings { - ImVector_char Buf; -} ImChunkStream_ImGuiTableSettings; - -typedef struct ImVector_ImGuiContextHook { - int Size; - int Capacity; - ImGuiContextHook* Data; -} ImVector_ImGuiContextHook; - -struct ImGuiContext { - bool Initialized; - bool FontAtlasOwnedByContext; - ImGuiIO IO; - ImGuiPlatformIO PlatformIO; - ImGuiStyle Style; - ImGuiConfigFlags ConfigFlagsCurrFrame; - ImGuiConfigFlags ConfigFlagsLastFrame; - ImFont* Font; - float FontSize; - float FontBaseSize; - ImDrawListSharedData DrawListSharedData; - double Time; - int FrameCount; - int FrameCountEnded; - int FrameCountPlatformEnded; - int FrameCountRendered; - bool WithinFrameScope; - bool WithinFrameScopeWithImplicitWindow; - bool WithinEndChild; - bool GcCompactAll; - bool TestEngineHookItems; - void* TestEngine; - ImVector_ImGuiInputEvent InputEventsQueue; - ImVector_ImGuiInputEvent InputEventsTrail; - ImGuiMouseSource InputEventsNextMouseSource; - ImU32 InputEventsNextEventId; - ImVector_ImGuiWindowPtr Windows; - ImVector_ImGuiWindowPtr WindowsFocusOrder; - ImVector_ImGuiWindowPtr WindowsTempSortBuffer; - ImVector_ImGuiWindowStackData CurrentWindowStack; - ImGuiStorage WindowsById; - int WindowsActiveCount; - ImVec2 WindowsHoverPadding; - ImGuiID DebugBreakInWindow; - ImGuiWindow* CurrentWindow; - ImGuiWindow* HoveredWindow; - ImGuiWindow* HoveredWindowUnderMovingWindow; - ImGuiWindow* MovingWindow; - ImGuiWindow* WheelingWindow; - ImVec2 WheelingWindowRefMousePos; - int WheelingWindowStartFrame; - int WheelingWindowScrolledFrame; - float WheelingWindowReleaseTimer; - ImVec2 WheelingWindowWheelRemainder; - ImVec2 WheelingAxisAvg; - ImGuiID DebugHookIdInfo; - ImGuiID HoveredId; - ImGuiID HoveredIdPreviousFrame; - bool HoveredIdAllowOverlap; - bool HoveredIdDisabled; - float HoveredIdTimer; - float HoveredIdNotActiveTimer; - ImGuiID ActiveId; - ImGuiID ActiveIdIsAlive; - float ActiveIdTimer; - bool ActiveIdIsJustActivated; - bool ActiveIdAllowOverlap; - bool ActiveIdNoClearOnFocusLoss; - bool ActiveIdHasBeenPressedBefore; - bool ActiveIdHasBeenEditedBefore; - bool ActiveIdHasBeenEditedThisFrame; - bool ActiveIdFromShortcut; - int ActiveIdMouseButton : 8; - ImVec2 ActiveIdClickOffset; - ImGuiWindow* ActiveIdWindow; - ImGuiInputSource ActiveIdSource; - ImGuiID ActiveIdPreviousFrame; - bool ActiveIdPreviousFrameIsAlive; - bool ActiveIdPreviousFrameHasBeenEditedBefore; - ImGuiWindow* ActiveIdPreviousFrameWindow; - ImGuiID LastActiveId; - float LastActiveIdTimer; - double LastKeyModsChangeTime; - double LastKeyModsChangeFromNoneTime; - double LastKeyboardKeyPressTime; - ImBitArrayForNamedKeys KeysMayBeCharInput; - ImGuiKeyOwnerData KeysOwnerData[ImGuiKey_NamedKey_COUNT]; - ImGuiKeyRoutingTable KeysRoutingTable; - ImU32 ActiveIdUsingNavDirMask; - bool ActiveIdUsingAllKeyboardKeys; - ImGuiKeyChord DebugBreakInShortcutRouting; - ImGuiID CurrentFocusScopeId; - ImGuiItemFlags CurrentItemFlags; - ImGuiID DebugLocateId; - ImGuiNextItemData NextItemData; - ImGuiLastItemData LastItemData; - ImGuiNextWindowData NextWindowData; - bool DebugShowGroupRects; - ImGuiCol DebugFlashStyleColorIdx; - ImVector_ImGuiColorMod ColorStack; - ImVector_ImGuiStyleMod StyleVarStack; - ImVector_ImFontPtr FontStack; - ImVector_ImGuiFocusScopeData FocusScopeStack; - ImVector_ImGuiItemFlags ItemFlagsStack; - ImVector_ImGuiGroupData GroupStack; - ImVector_ImGuiPopupData OpenPopupStack; - ImVector_ImGuiPopupData BeginPopupStack; - ImVector_ImGuiNavTreeNodeData NavTreeNodeStack; - ImVector_ImGuiViewportPPtr Viewports; - float CurrentDpiScale; - ImGuiViewportP* CurrentViewport; - ImGuiViewportP* MouseViewport; - ImGuiViewportP* MouseLastHoveredViewport; - ImGuiID PlatformLastFocusedViewportId; - ImGuiPlatformMonitor FallbackMonitor; - ImRect PlatformMonitorsFullWorkRect; - int ViewportCreatedCount; - int PlatformWindowsCreatedCount; - int ViewportFocusedStampCount; - ImGuiWindow* NavWindow; - ImGuiID NavId; - ImGuiID NavFocusScopeId; - ImVector_ImGuiFocusScopeData NavFocusRoute; - ImGuiID NavActivateId; - ImGuiID NavActivateDownId; - ImGuiID NavActivatePressedId; - ImGuiActivateFlags NavActivateFlags; - ImGuiID NavHighlightActivatedId; - float NavHighlightActivatedTimer; - ImGuiID NavJustMovedToId; - ImGuiID NavJustMovedToFocusScopeId; - ImGuiKeyChord NavJustMovedToKeyMods; - ImGuiID NavNextActivateId; - ImGuiActivateFlags NavNextActivateFlags; - ImGuiInputSource NavInputSource; - ImGuiNavLayer NavLayer; - ImGuiSelectionUserData NavLastValidSelectionUserData; - bool NavIdIsAlive; - bool NavMousePosDirty; - bool NavDisableHighlight; - bool NavDisableMouseHover; - bool NavAnyRequest; - bool NavInitRequest; - bool NavInitRequestFromMove; - ImGuiNavItemData NavInitResult; - bool NavMoveSubmitted; - bool NavMoveScoringItems; - bool NavMoveForwardToNextFrame; - ImGuiNavMoveFlags NavMoveFlags; - ImGuiScrollFlags NavMoveScrollFlags; - ImGuiKeyChord NavMoveKeyMods; - ImGuiDir NavMoveDir; - ImGuiDir NavMoveDirForDebug; - ImGuiDir NavMoveClipDir; - ImRect NavScoringRect; - ImRect NavScoringNoClipRect; - int NavScoringDebugCount; - int NavTabbingDir; - int NavTabbingCounter; - ImGuiNavItemData NavMoveResultLocal; - ImGuiNavItemData NavMoveResultLocalVisible; - ImGuiNavItemData NavMoveResultOther; - ImGuiNavItemData NavTabbingResultFirst; - ImGuiKeyChord ConfigNavWindowingKeyNext; - ImGuiKeyChord ConfigNavWindowingKeyPrev; - ImGuiWindow* NavWindowingTarget; - ImGuiWindow* NavWindowingTargetAnim; - ImGuiWindow* NavWindowingListWindow; - float NavWindowingTimer; - float NavWindowingHighlightAlpha; - bool NavWindowingToggleLayer; - ImGuiKey NavWindowingToggleKey; - ImVec2 NavWindowingAccumDeltaPos; - ImVec2 NavWindowingAccumDeltaSize; - float DimBgRatio; - bool DragDropActive; - bool DragDropWithinSource; - bool DragDropWithinTarget; - ImGuiDragDropFlags DragDropSourceFlags; - int DragDropSourceFrameCount; - int DragDropMouseButton; - ImGuiPayload DragDropPayload; - ImRect DragDropTargetRect; - ImRect DragDropTargetClipRect; - ImGuiID DragDropTargetId; - ImGuiDragDropFlags DragDropAcceptFlags; - float DragDropAcceptIdCurrRectSurface; - ImGuiID DragDropAcceptIdCurr; - ImGuiID DragDropAcceptIdPrev; - int DragDropAcceptFrameCount; - ImGuiID DragDropHoldJustPressedId; - ImVector_unsigned_char DragDropPayloadBufHeap; - unsigned char DragDropPayloadBufLocal[16]; - int ClipperTempDataStacked; - ImVector_ImGuiListClipperData ClipperTempData; - ImGuiTable* CurrentTable; - ImGuiID DebugBreakInTable; - int TablesTempDataStacked; - ImVector_ImGuiTableTempData TablesTempData; - ImPool_ImGuiTable Tables; - ImVector_float TablesLastTimeActive; - ImVector_ImDrawChannel DrawChannelsTempMergeBuffer; - ImGuiTabBar* CurrentTabBar; - ImPool_ImGuiTabBar TabBars; - ImVector_ImGuiPtrOrIndex CurrentTabBarStack; - ImVector_ImGuiShrinkWidthItem ShrinkWidthBuffer; - ImGuiID HoverItemDelayId; - ImGuiID HoverItemDelayIdPreviousFrame; - float HoverItemDelayTimer; - float HoverItemDelayClearTimer; - ImGuiID HoverItemUnlockedStationaryId; - ImGuiID HoverWindowUnlockedStationaryId; - ImGuiMouseCursor MouseCursor; - float MouseStationaryTimer; - ImVec2 MouseLastValidPos; - ImGuiInputTextState InputTextState; - ImGuiInputTextDeactivatedState InputTextDeactivatedState; - ImFont InputTextPasswordFont; - ImGuiID TempInputId; - int BeginMenuDepth; - int BeginComboDepth; - ImGuiColorEditFlags ColorEditOptions; - ImGuiID ColorEditCurrentID; - ImGuiID ColorEditSavedID; - float ColorEditSavedHue; - float ColorEditSavedSat; - ImU32 ColorEditSavedColor; - ImVec4 ColorPickerRef; - ImGuiComboPreviewData ComboPreviewData; - ImRect WindowResizeBorderExpectedRect; - bool WindowResizeRelativeMode; - float SliderGrabClickOffset; - float SliderCurrentAccum; - bool SliderCurrentAccumDirty; - bool DragCurrentAccumDirty; - float DragCurrentAccum; - float DragSpeedDefaultRatio; - float ScrollbarClickDeltaToGrabCenter; - float DisabledAlphaBackup; - short DisabledStackSize; - short LockMarkEdited; - short TooltipOverrideCount; - ImVector_char ClipboardHandlerData; - ImVector_ImGuiID MenusIdSubmittedThisFrame; - ImGuiTypingSelectState TypingSelectState; - ImGuiPlatformImeData PlatformImeData; - ImGuiPlatformImeData PlatformImeDataPrev; - ImGuiID PlatformImeViewport; - ImGuiDockContext DockContext; - void (*DockNodeWindowMenuHandler)(ImGuiContext* ctx, - ImGuiDockNode* node, - ImGuiTabBar* tab_bar); - bool SettingsLoaded; - float SettingsDirtyTimer; - ImGuiTextBuffer SettingsIniData; - ImVector_ImGuiSettingsHandler SettingsHandlers; - ImChunkStream_ImGuiWindowSettings SettingsWindows; - ImChunkStream_ImGuiTableSettings SettingsTables; - ImVector_ImGuiContextHook Hooks; - ImGuiID HookIdNext; - const char* LocalizationTable[ImGuiLocKey_COUNT]; - bool LogEnabled; - ImGuiLogType LogType; - ImFileHandle LogFile; - ImGuiTextBuffer LogBuffer; - const char* LogNextPrefix; - const char* LogNextSuffix; - float LogLinePosY; - bool LogLineFirstItem; - int LogDepthRef; - int LogDepthToExpand; - int LogDepthToExpandDefault; - ImGuiDebugLogFlags DebugLogFlags; - ImGuiTextBuffer DebugLogBuf; - ImGuiTextIndex DebugLogIndex; - ImGuiDebugLogFlags DebugLogAutoDisableFlags; - ImU8 DebugLogAutoDisableFrames; - ImU8 DebugLocateFrames; - bool DebugBreakInLocateId; - ImGuiKeyChord DebugBreakKeyChord; - ImS8 DebugBeginReturnValueCullDepth; - bool DebugItemPickerActive; - ImU8 DebugItemPickerMouseButton; - ImGuiID DebugItemPickerBreakId; - float DebugFlashStyleColorTime; - ImVec4 DebugFlashStyleColorBackup; - ImGuiMetricsConfig DebugMetricsConfig; - ImGuiIDStackTool DebugIDStackTool; - ImGuiDebugAllocInfo DebugAllocInfo; - ImGuiDockNode* DebugHoveredDockNode; - float FramerateSecPerFrame[60]; - int FramerateSecPerFrameIdx; - int FramerateSecPerFrameCount; - float FramerateSecPerFrameAccum; - int WantCaptureMouseNextFrame; - int WantCaptureKeyboardNextFrame; - int WantTextInputNextFrame; - ImVector_char TempBuffer; - char TempKeychordName[64]; -}; -struct ImGuiWindowTempData { - ImVec2 CursorPos; - ImVec2 CursorPosPrevLine; - ImVec2 CursorStartPos; - ImVec2 CursorMaxPos; - ImVec2 IdealMaxPos; - ImVec2 CurrLineSize; - ImVec2 PrevLineSize; - float CurrLineTextBaseOffset; - float PrevLineTextBaseOffset; - bool IsSameLine; - bool IsSetPos; - ImVec1 Indent; - ImVec1 ColumnsOffset; - ImVec1 GroupOffset; - ImVec2 CursorStartPosLossyness; - ImGuiNavLayer NavLayerCurrent; - short NavLayersActiveMask; - short NavLayersActiveMaskNext; - bool NavIsScrollPushableX; - bool NavHideHighlightOneFrame; - bool NavWindowHasScrollY; - bool MenuBarAppending; - ImVec2 MenuBarOffset; - ImGuiMenuColumns MenuColumns; - int TreeDepth; - ImU32 TreeJumpToParentOnPopMask; - ImVector_ImGuiWindowPtr ChildWindows; - ImGuiStorage* StateStorage; - ImGuiOldColumns* CurrentColumns; - int CurrentTableIdx; - ImGuiLayoutType LayoutType; - ImGuiLayoutType ParentLayoutType; - ImU32 ModalDimBgColor; - float ItemWidth; - float TextWrapPos; - ImVector_float ItemWidthStack; - ImVector_float TextWrapPosStack; -}; -typedef struct ImVector_ImGuiOldColumns { - int Size; - int Capacity; - ImGuiOldColumns* Data; -} ImVector_ImGuiOldColumns; - -struct ImGuiWindow { - ImGuiContext* Ctx; - char* Name; - ImGuiID ID; - ImGuiWindowFlags Flags, FlagsPreviousFrame; - ImGuiChildFlags ChildFlags; - ImGuiWindowClass WindowClass; - ImGuiViewportP* Viewport; - ImGuiID ViewportId; - ImVec2 ViewportPos; - int ViewportAllowPlatformMonitorExtend; - ImVec2 Pos; - ImVec2 Size; - ImVec2 SizeFull; - ImVec2 ContentSize; - ImVec2 ContentSizeIdeal; - ImVec2 ContentSizeExplicit; - ImVec2 WindowPadding; - float WindowRounding; - float WindowBorderSize; - float DecoOuterSizeX1, DecoOuterSizeY1; - float DecoOuterSizeX2, DecoOuterSizeY2; - float DecoInnerSizeX1, DecoInnerSizeY1; - int NameBufLen; - ImGuiID MoveId; - ImGuiID TabId; - ImGuiID ChildId; - ImVec2 Scroll; - ImVec2 ScrollMax; - ImVec2 ScrollTarget; - ImVec2 ScrollTargetCenterRatio; - ImVec2 ScrollTargetEdgeSnapDist; - ImVec2 ScrollbarSizes; - bool ScrollbarX, ScrollbarY; - bool ViewportOwned; - bool Active; - bool WasActive; - bool WriteAccessed; - bool Collapsed; - bool WantCollapseToggle; - bool SkipItems; - bool SkipRefresh; - bool Appearing; - bool Hidden; - bool IsFallbackWindow; - bool IsExplicitChild; - bool HasCloseButton; - signed char ResizeBorderHovered; - signed char ResizeBorderHeld; - short BeginCount; - short BeginCountPreviousFrame; - short BeginOrderWithinParent; - short BeginOrderWithinContext; - short FocusOrder; - ImGuiID PopupId; - ImS8 AutoFitFramesX, AutoFitFramesY; - bool AutoFitOnlyGrows; - ImGuiDir AutoPosLastDirection; - ImS8 HiddenFramesCanSkipItems; - ImS8 HiddenFramesCannotSkipItems; - ImS8 HiddenFramesForRenderOnly; - ImS8 DisableInputsFrames; - ImGuiCond SetWindowPosAllowFlags : 8; - ImGuiCond SetWindowSizeAllowFlags : 8; - ImGuiCond SetWindowCollapsedAllowFlags : 8; - ImGuiCond SetWindowDockAllowFlags : 8; - ImVec2 SetWindowPosVal; - ImVec2 SetWindowPosPivot; - ImVector_ImGuiID IDStack; - ImGuiWindowTempData DC; - ImRect OuterRectClipped; - ImRect InnerRect; - ImRect InnerClipRect; - ImRect WorkRect; - ImRect ParentWorkRect; - ImRect ClipRect; - ImRect ContentRegionRect; - ImVec2ih HitTestHoleSize; - ImVec2ih HitTestHoleOffset; - int LastFrameActive; - int LastFrameJustFocused; - float LastTimeActive; - float ItemWidthDefault; - ImGuiStorage StateStorage; - ImVector_ImGuiOldColumns ColumnsStorage; - float FontWindowScale; - float FontDpiScale; - int SettingsOffset; - ImDrawList* DrawList; - ImDrawList DrawListInst; - ImGuiWindow* ParentWindow; - ImGuiWindow* ParentWindowInBeginStack; - ImGuiWindow* RootWindow; - ImGuiWindow* RootWindowPopupTree; - ImGuiWindow* RootWindowDockTree; - ImGuiWindow* RootWindowForTitleBarHighlight; - ImGuiWindow* RootWindowForNav; - ImGuiWindow* ParentWindowForFocusRoute; - ImGuiWindow* NavLastChildNavWindow; - ImGuiID NavLastIds[ImGuiNavLayer_COUNT]; - ImRect NavRectRel[ImGuiNavLayer_COUNT]; - ImVec2 NavPreferredScoringPosRel[ImGuiNavLayer_COUNT]; - ImGuiID NavRootFocusScopeId; - int MemoryDrawListIdxCapacity; - int MemoryDrawListVtxCapacity; - bool MemoryCompacted; - bool DockIsActive : 1; - bool DockNodeIsVisible : 1; - bool DockTabIsVisible : 1; - bool DockTabWantClose : 1; - short DockOrder; - ImGuiWindowDockStyle DockStyle; - ImGuiDockNode* DockNode; - ImGuiDockNode* DockNodeAsHost; - ImGuiID DockId; - ImGuiItemStatusFlags DockTabItemStatusFlags; - ImRect DockTabItemRect; -}; -typedef enum { - ImGuiTabBarFlags_DockNode = 1 << 20, - ImGuiTabBarFlags_IsFocused = 1 << 21, - ImGuiTabBarFlags_SaveSettings = 1 << 22, -} ImGuiTabBarFlagsPrivate_; -typedef enum { - ImGuiTabItemFlags_SectionMask_ = - ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing, - ImGuiTabItemFlags_NoCloseButton = 1 << 20, - ImGuiTabItemFlags_Button = 1 << 21, - ImGuiTabItemFlags_Unsorted = 1 << 22, -} ImGuiTabItemFlagsPrivate_; -struct ImGuiTabItem { - ImGuiID ID; - ImGuiTabItemFlags Flags; - ImGuiWindow* Window; - int LastFrameVisible; - int LastFrameSelected; - float Offset; - float Width; - float ContentWidth; - float RequestedWidth; - ImS32 NameOffset; - ImS16 BeginOrder; - ImS16 IndexDuringLayout; - bool WantClose; -}; -typedef struct ImVector_ImGuiTabItem { - int Size; - int Capacity; - ImGuiTabItem* Data; -} ImVector_ImGuiTabItem; - -struct ImGuiTabBar { - ImVector_ImGuiTabItem Tabs; - ImGuiTabBarFlags Flags; - ImGuiID ID; - ImGuiID SelectedTabId; - ImGuiID NextSelectedTabId; - ImGuiID VisibleTabId; - int CurrFrameVisible; - int PrevFrameVisible; - ImRect BarRect; - float CurrTabsContentsHeight; - float PrevTabsContentsHeight; - float WidthAllTabs; - float WidthAllTabsIdeal; - float ScrollingAnim; - float ScrollingTarget; - float ScrollingTargetDistToVisibility; - float ScrollingSpeed; - float ScrollingRectMinX; - float ScrollingRectMaxX; - float SeparatorMinX; - float SeparatorMaxX; - ImGuiID ReorderRequestTabId; - ImS16 ReorderRequestOffset; - ImS8 BeginCount; - bool WantLayout; - bool VisibleTabWasSubmitted; - bool TabsAddedNew; - ImS16 TabsActiveCount; - ImS16 LastTabItemIdx; - float ItemSpacingY; - ImVec2 FramePadding; - ImVec2 BackupCursorPos; - ImGuiTextBuffer TabsNames; -}; -typedef ImS16 ImGuiTableColumnIdx; -typedef ImU16 ImGuiTableDrawChannelIdx; -struct ImGuiTableColumn { - ImGuiTableColumnFlags Flags; - float WidthGiven; - float MinX; - float MaxX; - float WidthRequest; - float WidthAuto; - float StretchWeight; - float InitStretchWeightOrWidth; - ImRect ClipRect; - ImGuiID UserID; - float WorkMinX; - float WorkMaxX; - float ItemWidth; - float ContentMaxXFrozen; - float ContentMaxXUnfrozen; - float ContentMaxXHeadersUsed; - float ContentMaxXHeadersIdeal; - ImS16 NameOffset; - ImGuiTableColumnIdx DisplayOrder; - ImGuiTableColumnIdx IndexWithinEnabledSet; - ImGuiTableColumnIdx PrevEnabledColumn; - ImGuiTableColumnIdx NextEnabledColumn; - ImGuiTableColumnIdx SortOrder; - ImGuiTableDrawChannelIdx DrawChannelCurrent; - ImGuiTableDrawChannelIdx DrawChannelFrozen; - ImGuiTableDrawChannelIdx DrawChannelUnfrozen; - bool IsEnabled; - bool IsUserEnabled; - bool IsUserEnabledNextFrame; - bool IsVisibleX; - bool IsVisibleY; - bool IsRequestOutput; - bool IsSkipItems; - bool IsPreserveWidthAuto; - ImS8 NavLayerCurrent; - ImU8 AutoFitQueue; - ImU8 CannotSkipItemsQueue; - ImU8 SortDirection : 2; - ImU8 SortDirectionsAvailCount : 2; - ImU8 SortDirectionsAvailMask : 4; - ImU8 SortDirectionsAvailList; -}; -typedef struct ImGuiTableCellData ImGuiTableCellData; -struct ImGuiTableCellData { - ImU32 BgColor; - ImGuiTableColumnIdx Column; -}; -struct ImGuiTableHeaderData { - ImGuiTableColumnIdx Index; - ImU32 TextColor; - ImU32 BgColor0; - ImU32 BgColor1; -}; -struct ImGuiTableInstanceData { - ImGuiID TableInstanceID; - float LastOuterHeight; - float LastTopHeadersRowHeight; - float LastFrozenHeight; - int HoveredRowLast; - int HoveredRowNext; -}; -typedef struct ImSpan_ImGuiTableColumn { - ImGuiTableColumn* Data; - ImGuiTableColumn* DataEnd; -} ImSpan_ImGuiTableColumn; - -typedef struct ImSpan_ImGuiTableColumnIdx { - ImGuiTableColumnIdx* Data; - ImGuiTableColumnIdx* DataEnd; -} ImSpan_ImGuiTableColumnIdx; - -typedef struct ImSpan_ImGuiTableCellData { - ImGuiTableCellData* Data; - ImGuiTableCellData* DataEnd; -} ImSpan_ImGuiTableCellData; - -typedef struct ImVector_ImGuiTableInstanceData { - int Size; - int Capacity; - ImGuiTableInstanceData* Data; -} ImVector_ImGuiTableInstanceData; - -typedef struct ImVector_ImGuiTableColumnSortSpecs { - int Size; - int Capacity; - ImGuiTableColumnSortSpecs* Data; -} ImVector_ImGuiTableColumnSortSpecs; - -struct ImGuiTable { - ImGuiID ID; - ImGuiTableFlags Flags; - void* RawData; - ImGuiTableTempData* TempData; - ImSpan_ImGuiTableColumn Columns; - ImSpan_ImGuiTableColumnIdx DisplayOrderToIndex; - ImSpan_ImGuiTableCellData RowCellData; - ImBitArrayPtr EnabledMaskByDisplayOrder; - ImBitArrayPtr EnabledMaskByIndex; - ImBitArrayPtr VisibleMaskByIndex; - ImGuiTableFlags SettingsLoadedFlags; - int SettingsOffset; - int LastFrameActive; - int ColumnsCount; - int CurrentRow; - int CurrentColumn; - ImS16 InstanceCurrent; - ImS16 InstanceInteracted; - float RowPosY1; - float RowPosY2; - float RowMinHeight; - float RowCellPaddingY; - float RowTextBaseline; - float RowIndentOffsetX; - ImGuiTableRowFlags RowFlags : 16; - ImGuiTableRowFlags LastRowFlags : 16; - int RowBgColorCounter; - ImU32 RowBgColor[2]; - ImU32 BorderColorStrong; - ImU32 BorderColorLight; - float BorderX1; - float BorderX2; - float HostIndentX; - float MinColumnWidth; - float OuterPaddingX; - float CellPaddingX; - float CellSpacingX1; - float CellSpacingX2; - float InnerWidth; - float ColumnsGivenWidth; - float ColumnsAutoFitWidth; - float ColumnsStretchSumWeights; - float ResizedColumnNextWidth; - float ResizeLockMinContentsX2; - float RefScale; - float AngledHeadersHeight; - float AngledHeadersSlope; - ImRect OuterRect; - ImRect InnerRect; - ImRect WorkRect; - ImRect InnerClipRect; - ImRect BgClipRect; - ImRect Bg0ClipRectForDrawCmd; - ImRect Bg2ClipRectForDrawCmd; - ImRect HostClipRect; - ImRect HostBackupInnerClipRect; - ImGuiWindow* OuterWindow; - ImGuiWindow* InnerWindow; - ImGuiTextBuffer ColumnsNames; - ImDrawListSplitter* DrawSplitter; - ImGuiTableInstanceData InstanceDataFirst; - ImVector_ImGuiTableInstanceData InstanceDataExtra; - ImGuiTableColumnSortSpecs SortSpecsSingle; - ImVector_ImGuiTableColumnSortSpecs SortSpecsMulti; - ImGuiTableSortSpecs SortSpecs; - ImGuiTableColumnIdx SortSpecsCount; - ImGuiTableColumnIdx ColumnsEnabledCount; - ImGuiTableColumnIdx ColumnsEnabledFixedCount; - ImGuiTableColumnIdx DeclColumnsCount; - ImGuiTableColumnIdx AngledHeadersCount; - ImGuiTableColumnIdx HoveredColumnBody; - ImGuiTableColumnIdx HoveredColumnBorder; - ImGuiTableColumnIdx HighlightColumnHeader; - ImGuiTableColumnIdx AutoFitSingleColumn; - ImGuiTableColumnIdx ResizedColumn; - ImGuiTableColumnIdx LastResizedColumn; - ImGuiTableColumnIdx HeldHeaderColumn; - ImGuiTableColumnIdx ReorderColumn; - ImGuiTableColumnIdx ReorderColumnDir; - ImGuiTableColumnIdx LeftMostEnabledColumn; - ImGuiTableColumnIdx RightMostEnabledColumn; - ImGuiTableColumnIdx LeftMostStretchedColumn; - ImGuiTableColumnIdx RightMostStretchedColumn; - ImGuiTableColumnIdx ContextPopupColumn; - ImGuiTableColumnIdx FreezeRowsRequest; - ImGuiTableColumnIdx FreezeRowsCount; - ImGuiTableColumnIdx FreezeColumnsRequest; - ImGuiTableColumnIdx FreezeColumnsCount; - ImGuiTableColumnIdx RowCellDataCurrent; - ImGuiTableDrawChannelIdx DummyDrawChannel; - ImGuiTableDrawChannelIdx Bg2DrawChannelCurrent; - ImGuiTableDrawChannelIdx Bg2DrawChannelUnfrozen; - bool IsLayoutLocked; - bool IsInsideRow; - bool IsInitializing; - bool IsSortSpecsDirty; - bool IsUsingHeaders; - bool IsContextPopupOpen; - bool DisableDefaultContextMenu; - bool IsSettingsRequestLoad; - bool IsSettingsDirty; - bool IsDefaultDisplayOrder; - bool IsResetAllRequest; - bool IsResetDisplayOrderRequest; - bool IsUnfrozenRows; - bool IsDefaultSizingPolicy; - bool IsActiveIdAliveBeforeTable; - bool IsActiveIdInTable; - bool HasScrollbarYCurr; - bool HasScrollbarYPrev; - bool MemoryCompacted; - bool HostSkipItems; -}; -typedef struct ImVector_ImGuiTableHeaderData { - int Size; - int Capacity; - ImGuiTableHeaderData* Data; -} ImVector_ImGuiTableHeaderData; - -struct ImGuiTableTempData { - int TableIndex; - float LastTimeActive; - float AngledHeadersExtraWidth; - ImVector_ImGuiTableHeaderData AngledHeadersRequests; - ImVec2 UserOuterSize; - ImDrawListSplitter DrawSplitter; - ImRect HostBackupWorkRect; - ImRect HostBackupParentWorkRect; - ImVec2 HostBackupPrevLineSize; - ImVec2 HostBackupCurrLineSize; - ImVec2 HostBackupCursorMaxPos; - ImVec1 HostBackupColumnsOffset; - float HostBackupItemWidth; - int HostBackupItemWidthStackSize; -}; -typedef struct ImGuiTableColumnSettings ImGuiTableColumnSettings; -struct ImGuiTableColumnSettings { - float WidthOrWeight; - ImGuiID UserID; - ImGuiTableColumnIdx Index; - ImGuiTableColumnIdx DisplayOrder; - ImGuiTableColumnIdx SortOrder; - ImU8 SortDirection : 2; - ImU8 IsEnabled : 1; - ImU8 IsStretch : 1; -}; -struct ImGuiTableSettings { - ImGuiID ID; - ImGuiTableFlags SaveFlags; - float RefScale; - ImGuiTableColumnIdx ColumnsCount; - ImGuiTableColumnIdx ColumnsCountMax; - bool WantApply; -}; -struct ImFontBuilderIO { - bool (*FontBuilder_Build)(ImFontAtlas* atlas); -}; -#define IMGUI_HAS_DOCK 1 - -#define ImDrawCallback_ResetRenderState (ImDrawCallback)(-8) - -#else -struct GLFWwindow; -struct SDL_Window; -typedef union SDL_Event SDL_Event; -#endif // CIMGUI_DEFINE_ENUMS_AND_STRUCTS - -#ifndef CIMGUI_DEFINE_ENUMS_AND_STRUCTS -typedef struct ImGuiStorage::ImGuiStoragePair ImGuiStoragePair; -typedef struct ImGuiTextFilter::ImGuiTextRange ImGuiTextRange; -typedef ImStb::STB_TexteditState STB_TexteditState; -typedef ImStb::StbTexteditRow StbTexteditRow; -typedef ImStb::StbUndoRecord StbUndoRecord; -typedef ImStb::StbUndoState StbUndoState; -typedef ImChunkStream ImChunkStream_ImGuiTableSettings; -typedef ImChunkStream ImChunkStream_ImGuiWindowSettings; -typedef ImPool ImPool_ImGuiTabBar; -typedef ImPool ImPool_ImGuiTable; -typedef ImSpan ImSpan_ImGuiTableCellData; -typedef ImSpan ImSpan_ImGuiTableColumn; -typedef ImSpan ImSpan_ImGuiTableColumnIdx; -typedef ImVector ImVector_ImDrawChannel; -typedef ImVector ImVector_ImDrawCmd; -typedef ImVector ImVector_ImDrawIdx; -typedef ImVector ImVector_ImDrawListPtr; -typedef ImVector ImVector_ImDrawVert; -typedef ImVector ImVector_ImFontPtr; -typedef ImVector ImVector_ImFontAtlasCustomRect; -typedef ImVector ImVector_ImFontConfig; -typedef ImVector ImVector_ImFontGlyph; -typedef ImVector ImVector_ImGuiColorMod; -typedef ImVector ImVector_ImGuiContextHook; -typedef ImVector ImVector_ImGuiDockNodeSettings; -typedef ImVector ImVector_ImGuiDockRequest; -typedef ImVector ImVector_ImGuiFocusScopeData; -typedef ImVector ImVector_ImGuiGroupData; -typedef ImVector ImVector_ImGuiID; -typedef ImVector ImVector_ImGuiInputEvent; -typedef ImVector ImVector_ImGuiItemFlags; -typedef ImVector ImVector_ImGuiKeyRoutingData; -typedef ImVector ImVector_ImGuiListClipperData; -typedef ImVector ImVector_ImGuiListClipperRange; -typedef ImVector ImVector_ImGuiNavTreeNodeData; -typedef ImVector ImVector_ImGuiOldColumnData; -typedef ImVector ImVector_ImGuiOldColumns; -typedef ImVector ImVector_ImGuiPlatformMonitor; -typedef ImVector ImVector_ImGuiPopupData; -typedef ImVector ImVector_ImGuiPtrOrIndex; -typedef ImVector ImVector_ImGuiSettingsHandler; -typedef ImVector ImVector_ImGuiShrinkWidthItem; -typedef ImVector ImVector_ImGuiStackLevelInfo; -typedef ImVector ImVector_ImGuiStoragePair; -typedef ImVector ImVector_ImGuiStyleMod; -typedef ImVector ImVector_ImGuiTabItem; -typedef ImVector ImVector_ImGuiTableColumnSortSpecs; -typedef ImVector ImVector_ImGuiTableHeaderData; -typedef ImVector ImVector_ImGuiTableInstanceData; -typedef ImVector ImVector_ImGuiTableTempData; -typedef ImVector ImVector_ImGuiTextRange; -typedef ImVector ImVector_ImGuiViewportPtr; -typedef ImVector ImVector_ImGuiViewportPPtr; -typedef ImVector ImVector_ImGuiWindowPtr; -typedef ImVector ImVector_ImGuiWindowStackData; -typedef ImVector ImVector_ImTextureID; -typedef ImVector ImVector_ImU32; -typedef ImVector ImVector_ImVec2; -typedef ImVector ImVector_ImVec4; -typedef ImVector ImVector_ImWchar; -typedef ImVector ImVector_char; -typedef ImVector ImVector_const_charPtr; -typedef ImVector ImVector_float; -typedef ImVector ImVector_int; -typedef ImVector ImVector_unsigned_char; -#endif // CIMGUI_DEFINE_ENUMS_AND_STRUCTS -CIMGUI_API ImVec2* ImVec2_ImVec2_Nil(void); -CIMGUI_API void ImVec2_destroy(ImVec2* self); -CIMGUI_API ImVec2* ImVec2_ImVec2_Float(float _x, float _y); -CIMGUI_API ImVec4* ImVec4_ImVec4_Nil(void); -CIMGUI_API void ImVec4_destroy(ImVec4* self); -CIMGUI_API ImVec4* ImVec4_ImVec4_Float(float _x, float _y, float _z, float _w); -CIMGUI_API ImGuiContext* igCreateContext(ImFontAtlas* shared_font_atlas); -CIMGUI_API void igDestroyContext(ImGuiContext* ctx); -CIMGUI_API ImGuiContext* igGetCurrentContext(void); -CIMGUI_API void igSetCurrentContext(ImGuiContext* ctx); -CIMGUI_API ImGuiIO* igGetIO(void); -CIMGUI_API ImGuiStyle* igGetStyle(void); -CIMGUI_API void igNewFrame(void); -CIMGUI_API void igEndFrame(void); -CIMGUI_API void igRender(void); -CIMGUI_API ImDrawData* igGetDrawData(void); -CIMGUI_API void igShowDemoWindow(bool* p_open); -CIMGUI_API void igShowMetricsWindow(bool* p_open); -CIMGUI_API void igShowDebugLogWindow(bool* p_open); -CIMGUI_API void igShowIDStackToolWindow(bool* p_open); -CIMGUI_API void igShowAboutWindow(bool* p_open); -CIMGUI_API void igShowStyleEditor(ImGuiStyle* ref); -CIMGUI_API bool igShowStyleSelector(const char* label); -CIMGUI_API void igShowFontSelector(const char* label); -CIMGUI_API void igShowUserGuide(void); -CIMGUI_API const char* igGetVersion(void); -CIMGUI_API void igStyleColorsDark(ImGuiStyle* dst); -CIMGUI_API void igStyleColorsLight(ImGuiStyle* dst); -CIMGUI_API void igStyleColorsClassic(ImGuiStyle* dst); -CIMGUI_API bool igBegin(const char* name, bool* p_open, ImGuiWindowFlags flags); -CIMGUI_API void igEnd(void); -CIMGUI_API bool igBeginChild_Str(const char* str_id, - const ImVec2 size, - ImGuiChildFlags child_flags, - ImGuiWindowFlags window_flags); -CIMGUI_API bool igBeginChild_ID(ImGuiID id, - const ImVec2 size, - ImGuiChildFlags child_flags, - ImGuiWindowFlags window_flags); -CIMGUI_API void igEndChild(void); -CIMGUI_API bool igIsWindowAppearing(void); -CIMGUI_API bool igIsWindowCollapsed(void); -CIMGUI_API bool igIsWindowFocused(ImGuiFocusedFlags flags); -CIMGUI_API bool igIsWindowHovered(ImGuiHoveredFlags flags); -CIMGUI_API ImDrawList* igGetWindowDrawList(void); -CIMGUI_API float igGetWindowDpiScale(void); -CIMGUI_API void igGetWindowPos(ImVec2* pOut); -CIMGUI_API void igGetWindowSize(ImVec2* pOut); -CIMGUI_API float igGetWindowWidth(void); -CIMGUI_API float igGetWindowHeight(void); -CIMGUI_API ImGuiViewport* igGetWindowViewport(void); -CIMGUI_API void igSetNextWindowPos(const ImVec2 pos, - ImGuiCond cond, - const ImVec2 pivot); -CIMGUI_API void igSetNextWindowSize(const ImVec2 size, ImGuiCond cond); -CIMGUI_API void igSetNextWindowSizeConstraints( - const ImVec2 size_min, - const ImVec2 size_max, - ImGuiSizeCallback custom_callback, - void* custom_callback_data); -CIMGUI_API void igSetNextWindowContentSize(const ImVec2 size); -CIMGUI_API void igSetNextWindowCollapsed(bool collapsed, ImGuiCond cond); -CIMGUI_API void igSetNextWindowFocus(void); -CIMGUI_API void igSetNextWindowScroll(const ImVec2 scroll); -CIMGUI_API void igSetNextWindowBgAlpha(float alpha); -CIMGUI_API void igSetNextWindowViewport(ImGuiID viewport_id); -CIMGUI_API void igSetWindowPos_Vec2(const ImVec2 pos, ImGuiCond cond); -CIMGUI_API void igSetWindowSize_Vec2(const ImVec2 size, ImGuiCond cond); -CIMGUI_API void igSetWindowCollapsed_Bool(bool collapsed, ImGuiCond cond); -CIMGUI_API void igSetWindowFocus_Nil(void); -CIMGUI_API void igSetWindowFontScale(float scale); -CIMGUI_API void igSetWindowPos_Str(const char* name, - const ImVec2 pos, - ImGuiCond cond); -CIMGUI_API void igSetWindowSize_Str(const char* name, - const ImVec2 size, - ImGuiCond cond); -CIMGUI_API void igSetWindowCollapsed_Str(const char* name, - bool collapsed, - ImGuiCond cond); -CIMGUI_API void igSetWindowFocus_Str(const char* name); -CIMGUI_API void igGetContentRegionAvail(ImVec2* pOut); -CIMGUI_API void igGetContentRegionMax(ImVec2* pOut); -CIMGUI_API void igGetWindowContentRegionMin(ImVec2* pOut); -CIMGUI_API void igGetWindowContentRegionMax(ImVec2* pOut); -CIMGUI_API float igGetScrollX(void); -CIMGUI_API float igGetScrollY(void); -CIMGUI_API void igSetScrollX_Float(float scroll_x); -CIMGUI_API void igSetScrollY_Float(float scroll_y); -CIMGUI_API float igGetScrollMaxX(void); -CIMGUI_API float igGetScrollMaxY(void); -CIMGUI_API void igSetScrollHereX(float center_x_ratio); -CIMGUI_API void igSetScrollHereY(float center_y_ratio); -CIMGUI_API void igSetScrollFromPosX_Float(float local_x, float center_x_ratio); -CIMGUI_API void igSetScrollFromPosY_Float(float local_y, float center_y_ratio); -CIMGUI_API void igPushFont(ImFont* font); -CIMGUI_API void igPopFont(void); -CIMGUI_API void igPushStyleColor_U32(ImGuiCol idx, ImU32 col); -CIMGUI_API void igPushStyleColor_Vec4(ImGuiCol idx, const ImVec4 col); -CIMGUI_API void igPopStyleColor(int count); -CIMGUI_API void igPushStyleVar_Float(ImGuiStyleVar idx, float val); -CIMGUI_API void igPushStyleVar_Vec2(ImGuiStyleVar idx, const ImVec2 val); -CIMGUI_API void igPopStyleVar(int count); -CIMGUI_API void igPushTabStop(bool tab_stop); -CIMGUI_API void igPopTabStop(void); -CIMGUI_API void igPushButtonRepeat(bool repeat); -CIMGUI_API void igPopButtonRepeat(void); -CIMGUI_API void igPushItemWidth(float item_width); -CIMGUI_API void igPopItemWidth(void); -CIMGUI_API void igSetNextItemWidth(float item_width); -CIMGUI_API float igCalcItemWidth(void); -CIMGUI_API void igPushTextWrapPos(float wrap_local_pos_x); -CIMGUI_API void igPopTextWrapPos(void); -CIMGUI_API ImFont* igGetFont(void); -CIMGUI_API float igGetFontSize(void); -CIMGUI_API void igGetFontTexUvWhitePixel(ImVec2* pOut); -CIMGUI_API ImU32 igGetColorU32_Col(ImGuiCol idx, float alpha_mul); -CIMGUI_API ImU32 igGetColorU32_Vec4(const ImVec4 col); -CIMGUI_API ImU32 igGetColorU32_U32(ImU32 col, float alpha_mul); -CIMGUI_API const ImVec4* igGetStyleColorVec4(ImGuiCol idx); -CIMGUI_API void igGetCursorScreenPos(ImVec2* pOut); -CIMGUI_API void igSetCursorScreenPos(const ImVec2 pos); -CIMGUI_API void igGetCursorPos(ImVec2* pOut); -CIMGUI_API float igGetCursorPosX(void); -CIMGUI_API float igGetCursorPosY(void); -CIMGUI_API void igSetCursorPos(const ImVec2 local_pos); -CIMGUI_API void igSetCursorPosX(float local_x); -CIMGUI_API void igSetCursorPosY(float local_y); -CIMGUI_API void igGetCursorStartPos(ImVec2* pOut); -CIMGUI_API void igSeparator(void); -CIMGUI_API void igSameLine(float offset_from_start_x, float spacing); -CIMGUI_API void igNewLine(void); -CIMGUI_API void igSpacing(void); -CIMGUI_API void igDummy(const ImVec2 size); -CIMGUI_API void igIndent(float indent_w); -CIMGUI_API void igUnindent(float indent_w); -CIMGUI_API void igBeginGroup(void); -CIMGUI_API void igEndGroup(void); -CIMGUI_API void igAlignTextToFramePadding(void); -CIMGUI_API float igGetTextLineHeight(void); -CIMGUI_API float igGetTextLineHeightWithSpacing(void); -CIMGUI_API float igGetFrameHeight(void); -CIMGUI_API float igGetFrameHeightWithSpacing(void); -CIMGUI_API void igPushID_Str(const char* str_id); -CIMGUI_API void igPushID_StrStr(const char* str_id_begin, - const char* str_id_end); -CIMGUI_API void igPushID_Ptr(const void* ptr_id); -CIMGUI_API void igPushID_Int(int int_id); -CIMGUI_API void igPopID(void); -CIMGUI_API ImGuiID igGetID_Str(const char* str_id); -CIMGUI_API ImGuiID igGetID_StrStr(const char* str_id_begin, - const char* str_id_end); -CIMGUI_API ImGuiID igGetID_Ptr(const void* ptr_id); -CIMGUI_API void igTextUnformatted(const char* text, const char* text_end); -CIMGUI_API void igText(const char* fmt, ...); -CIMGUI_API void igTextV(const char* fmt, va_list args); -CIMGUI_API void igTextColored(const ImVec4 col, const char* fmt, ...); -CIMGUI_API void igTextColoredV(const ImVec4 col, const char* fmt, va_list args); -CIMGUI_API void igTextDisabled(const char* fmt, ...); -CIMGUI_API void igTextDisabledV(const char* fmt, va_list args); -CIMGUI_API void igTextWrapped(const char* fmt, ...); -CIMGUI_API void igTextWrappedV(const char* fmt, va_list args); -CIMGUI_API void igLabelText(const char* label, const char* fmt, ...); -CIMGUI_API void igLabelTextV(const char* label, const char* fmt, va_list args); -CIMGUI_API void igBulletText(const char* fmt, ...); -CIMGUI_API void igBulletTextV(const char* fmt, va_list args); -CIMGUI_API void igSeparatorText(const char* label); -CIMGUI_API bool igButton(const char* label, const ImVec2 size); -CIMGUI_API bool igSmallButton(const char* label); -CIMGUI_API bool igInvisibleButton(const char* str_id, - const ImVec2 size, - ImGuiButtonFlags flags); -CIMGUI_API bool igArrowButton(const char* str_id, ImGuiDir dir); -CIMGUI_API bool igCheckbox(const char* label, bool* v); -CIMGUI_API bool igCheckboxFlags_IntPtr(const char* label, - int* flags, - int flags_value); -CIMGUI_API bool igCheckboxFlags_UintPtr(const char* label, - unsigned int* flags, - unsigned int flags_value); -CIMGUI_API bool igRadioButton_Bool(const char* label, bool active); -CIMGUI_API bool igRadioButton_IntPtr(const char* label, int* v, int v_button); -CIMGUI_API void igProgressBar(float fraction, - const ImVec2 size_arg, - const char* overlay); -CIMGUI_API void igBullet(void); -CIMGUI_API void igImage(ImTextureID user_texture_id, - const ImVec2 image_size, - const ImVec2 uv0, - const ImVec2 uv1, - const ImVec4 tint_col, - const ImVec4 border_col); -CIMGUI_API bool igImageButton(const char* str_id, - ImTextureID user_texture_id, - const ImVec2 image_size, - const ImVec2 uv0, - const ImVec2 uv1, - const ImVec4 bg_col, - const ImVec4 tint_col); -CIMGUI_API bool igBeginCombo(const char* label, - const char* preview_value, - ImGuiComboFlags flags); -CIMGUI_API void igEndCombo(void); -CIMGUI_API bool igCombo_Str_arr(const char* label, - int* current_item, - const char* const items[], - int items_count, - int popup_max_height_in_items); -CIMGUI_API bool igCombo_Str(const char* label, - int* current_item, - const char* items_separated_by_zeros, - int popup_max_height_in_items); -CIMGUI_API bool igCombo_FnStrPtr(const char* label, - int* current_item, - const char* (*getter)(void* user_data, - int idx), - void* user_data, - int items_count, - int popup_max_height_in_items); -CIMGUI_API bool igDragFloat(const char* label, - float* v, - float v_speed, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragFloat2(const char* label, - float v[2], - float v_speed, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragFloat3(const char* label, - float v[3], - float v_speed, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragFloat4(const char* label, - float v[4], - float v_speed, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragFloatRange2(const char* label, - float* v_current_min, - float* v_current_max, - float v_speed, - float v_min, - float v_max, - const char* format, - const char* format_max, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragInt(const char* label, - int* v, - float v_speed, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragInt2(const char* label, - int v[2], - float v_speed, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragInt3(const char* label, - int v[3], - float v_speed, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragInt4(const char* label, - int v[4], - float v_speed, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragIntRange2(const char* label, - int* v_current_min, - int* v_current_max, - float v_speed, - int v_min, - int v_max, - const char* format, - const char* format_max, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragScalar(const char* label, - ImGuiDataType data_type, - void* p_data, - float v_speed, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragScalarN(const char* label, - ImGuiDataType data_type, - void* p_data, - int components, - float v_speed, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderFloat(const char* label, - float* v, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderFloat2(const char* label, - float v[2], - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderFloat3(const char* label, - float v[3], - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderFloat4(const char* label, - float v[4], - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderAngle(const char* label, - float* v_rad, - float v_degrees_min, - float v_degrees_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderInt(const char* label, - int* v, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderInt2(const char* label, - int v[2], - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderInt3(const char* label, - int v[3], - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderInt4(const char* label, - int v[4], - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderScalar(const char* label, - ImGuiDataType data_type, - void* p_data, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderScalarN(const char* label, - ImGuiDataType data_type, - void* p_data, - int components, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igVSliderFloat(const char* label, - const ImVec2 size, - float* v, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igVSliderInt(const char* label, - const ImVec2 size, - int* v, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igVSliderScalar(const char* label, - const ImVec2 size, - ImGuiDataType data_type, - void* p_data, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igInputText(const char* label, - char* buf, - size_t buf_size, - ImGuiInputTextFlags flags, - ImGuiInputTextCallback callback, - void* user_data); -CIMGUI_API bool igInputTextMultiline(const char* label, - char* buf, - size_t buf_size, - const ImVec2 size, - ImGuiInputTextFlags flags, - ImGuiInputTextCallback callback, - void* user_data); -CIMGUI_API bool igInputTextWithHint(const char* label, - const char* hint, - char* buf, - size_t buf_size, - ImGuiInputTextFlags flags, - ImGuiInputTextCallback callback, - void* user_data); -CIMGUI_API bool igInputFloat(const char* label, - float* v, - float step, - float step_fast, - const char* format, - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputFloat2(const char* label, - float v[2], - const char* format, - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputFloat3(const char* label, - float v[3], - const char* format, - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputFloat4(const char* label, - float v[4], - const char* format, - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputInt(const char* label, - int* v, - int step, - int step_fast, - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputInt2(const char* label, - int v[2], - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputInt3(const char* label, - int v[3], - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputInt4(const char* label, - int v[4], - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputDouble(const char* label, - double* v, - double step, - double step_fast, - const char* format, - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputScalar(const char* label, - ImGuiDataType data_type, - void* p_data, - const void* p_step, - const void* p_step_fast, - const char* format, - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputScalarN(const char* label, - ImGuiDataType data_type, - void* p_data, - int components, - const void* p_step, - const void* p_step_fast, - const char* format, - ImGuiInputTextFlags flags); -CIMGUI_API bool igColorEdit3(const char* label, - float col[3], - ImGuiColorEditFlags flags); -CIMGUI_API bool igColorEdit4(const char* label, - float col[4], - ImGuiColorEditFlags flags); -CIMGUI_API bool igColorPicker3(const char* label, - float col[3], - ImGuiColorEditFlags flags); -CIMGUI_API bool igColorPicker4(const char* label, - float col[4], - ImGuiColorEditFlags flags, - const float* ref_col); -CIMGUI_API bool igColorButton(const char* desc_id, - const ImVec4 col, - ImGuiColorEditFlags flags, - const ImVec2 size); -CIMGUI_API void igSetColorEditOptions(ImGuiColorEditFlags flags); -CIMGUI_API bool igTreeNode_Str(const char* label); -CIMGUI_API bool igTreeNode_StrStr(const char* str_id, const char* fmt, ...); -CIMGUI_API bool igTreeNode_Ptr(const void* ptr_id, const char* fmt, ...); -CIMGUI_API bool igTreeNodeV_Str(const char* str_id, - const char* fmt, - va_list args); -CIMGUI_API bool igTreeNodeV_Ptr(const void* ptr_id, - const char* fmt, - va_list args); -CIMGUI_API bool igTreeNodeEx_Str(const char* label, ImGuiTreeNodeFlags flags); -CIMGUI_API bool igTreeNodeEx_StrStr(const char* str_id, - ImGuiTreeNodeFlags flags, - const char* fmt, - ...); -CIMGUI_API bool igTreeNodeEx_Ptr(const void* ptr_id, - ImGuiTreeNodeFlags flags, - const char* fmt, - ...); -CIMGUI_API bool igTreeNodeExV_Str(const char* str_id, - ImGuiTreeNodeFlags flags, - const char* fmt, - va_list args); -CIMGUI_API bool igTreeNodeExV_Ptr(const void* ptr_id, - ImGuiTreeNodeFlags flags, - const char* fmt, - va_list args); -CIMGUI_API void igTreePush_Str(const char* str_id); -CIMGUI_API void igTreePush_Ptr(const void* ptr_id); -CIMGUI_API void igTreePop(void); -CIMGUI_API float igGetTreeNodeToLabelSpacing(void); -CIMGUI_API bool igCollapsingHeader_TreeNodeFlags(const char* label, - ImGuiTreeNodeFlags flags); -CIMGUI_API bool igCollapsingHeader_BoolPtr(const char* label, - bool* p_visible, - ImGuiTreeNodeFlags flags); -CIMGUI_API void igSetNextItemOpen(bool is_open, ImGuiCond cond); -CIMGUI_API bool igSelectable_Bool(const char* label, - bool selected, - ImGuiSelectableFlags flags, - const ImVec2 size); -CIMGUI_API bool igSelectable_BoolPtr(const char* label, - bool* p_selected, - ImGuiSelectableFlags flags, - const ImVec2 size); -CIMGUI_API bool igBeginListBox(const char* label, const ImVec2 size); -CIMGUI_API void igEndListBox(void); -CIMGUI_API bool igListBox_Str_arr(const char* label, - int* current_item, - const char* const items[], - int items_count, - int height_in_items); -CIMGUI_API bool igListBox_FnStrPtr(const char* label, - int* current_item, - const char* (*getter)(void* user_data, - int idx), - void* user_data, - int items_count, - int height_in_items); -CIMGUI_API void igPlotLines_FloatPtr(const char* label, - const float* values, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - ImVec2 graph_size, - int stride); -CIMGUI_API void igPlotLines_FnFloatPtr(const char* label, - float (*values_getter)(void* data, - int idx), - void* data, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - ImVec2 graph_size); -CIMGUI_API void igPlotHistogram_FloatPtr(const char* label, - const float* values, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - ImVec2 graph_size, - int stride); -CIMGUI_API void igPlotHistogram_FnFloatPtr(const char* label, - float (*values_getter)(void* data, - int idx), - void* data, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - ImVec2 graph_size); -CIMGUI_API void igValue_Bool(const char* prefix, bool b); -CIMGUI_API void igValue_Int(const char* prefix, int v); -CIMGUI_API void igValue_Uint(const char* prefix, unsigned int v); -CIMGUI_API void igValue_Float(const char* prefix, - float v, - const char* float_format); -CIMGUI_API bool igBeginMenuBar(void); -CIMGUI_API void igEndMenuBar(void); -CIMGUI_API bool igBeginMainMenuBar(void); -CIMGUI_API void igEndMainMenuBar(void); -CIMGUI_API bool igBeginMenu(const char* label, bool enabled); -CIMGUI_API void igEndMenu(void); -CIMGUI_API bool igMenuItem_Bool(const char* label, - const char* shortcut, - bool selected, - bool enabled); -CIMGUI_API bool igMenuItem_BoolPtr(const char* label, - const char* shortcut, - bool* p_selected, - bool enabled); -CIMGUI_API bool igBeginTooltip(void); -CIMGUI_API void igEndTooltip(void); -CIMGUI_API void igSetTooltip(const char* fmt, ...); -CIMGUI_API void igSetTooltipV(const char* fmt, va_list args); -CIMGUI_API bool igBeginItemTooltip(void); -CIMGUI_API void igSetItemTooltip(const char* fmt, ...); -CIMGUI_API void igSetItemTooltipV(const char* fmt, va_list args); -CIMGUI_API bool igBeginPopup(const char* str_id, ImGuiWindowFlags flags); -CIMGUI_API bool igBeginPopupModal(const char* name, - bool* p_open, - ImGuiWindowFlags flags); -CIMGUI_API void igEndPopup(void); -CIMGUI_API void igOpenPopup_Str(const char* str_id, - ImGuiPopupFlags popup_flags); -CIMGUI_API void igOpenPopup_ID(ImGuiID id, ImGuiPopupFlags popup_flags); -CIMGUI_API void igOpenPopupOnItemClick(const char* str_id, - ImGuiPopupFlags popup_flags); -CIMGUI_API void igCloseCurrentPopup(void); -CIMGUI_API bool igBeginPopupContextItem(const char* str_id, - ImGuiPopupFlags popup_flags); -CIMGUI_API bool igBeginPopupContextWindow(const char* str_id, - ImGuiPopupFlags popup_flags); -CIMGUI_API bool igBeginPopupContextVoid(const char* str_id, - ImGuiPopupFlags popup_flags); -CIMGUI_API bool igIsPopupOpen_Str(const char* str_id, ImGuiPopupFlags flags); -CIMGUI_API bool igBeginTable(const char* str_id, - int column, - ImGuiTableFlags flags, - const ImVec2 outer_size, - float inner_width); -CIMGUI_API void igEndTable(void); -CIMGUI_API void igTableNextRow(ImGuiTableRowFlags row_flags, - float min_row_height); -CIMGUI_API bool igTableNextColumn(void); -CIMGUI_API bool igTableSetColumnIndex(int column_n); -CIMGUI_API void igTableSetupColumn(const char* label, - ImGuiTableColumnFlags flags, - float init_width_or_weight, - ImGuiID user_id); -CIMGUI_API void igTableSetupScrollFreeze(int cols, int rows); -CIMGUI_API void igTableHeader(const char* label); -CIMGUI_API void igTableHeadersRow(void); -CIMGUI_API void igTableAngledHeadersRow(void); -CIMGUI_API ImGuiTableSortSpecs* igTableGetSortSpecs(void); -CIMGUI_API int igTableGetColumnCount(void); -CIMGUI_API int igTableGetColumnIndex(void); -CIMGUI_API int igTableGetRowIndex(void); -CIMGUI_API const char* igTableGetColumnName_Int(int column_n); -CIMGUI_API ImGuiTableColumnFlags igTableGetColumnFlags(int column_n); -CIMGUI_API void igTableSetColumnEnabled(int column_n, bool v); -CIMGUI_API void igTableSetBgColor(ImGuiTableBgTarget target, - ImU32 color, - int column_n); -CIMGUI_API void igColumns(int count, const char* id, bool border); -CIMGUI_API void igNextColumn(void); -CIMGUI_API int igGetColumnIndex(void); -CIMGUI_API float igGetColumnWidth(int column_index); -CIMGUI_API void igSetColumnWidth(int column_index, float width); -CIMGUI_API float igGetColumnOffset(int column_index); -CIMGUI_API void igSetColumnOffset(int column_index, float offset_x); -CIMGUI_API int igGetColumnsCount(void); -CIMGUI_API bool igBeginTabBar(const char* str_id, ImGuiTabBarFlags flags); -CIMGUI_API void igEndTabBar(void); -CIMGUI_API bool igBeginTabItem(const char* label, - bool* p_open, - ImGuiTabItemFlags flags); -CIMGUI_API void igEndTabItem(void); -CIMGUI_API bool igTabItemButton(const char* label, ImGuiTabItemFlags flags); -CIMGUI_API void igSetTabItemClosed(const char* tab_or_docked_window_label); -CIMGUI_API ImGuiID igDockSpace(ImGuiID id, - const ImVec2 size, - ImGuiDockNodeFlags flags, - const ImGuiWindowClass* window_class); -CIMGUI_API ImGuiID -igDockSpaceOverViewport(const ImGuiViewport* viewport, - ImGuiDockNodeFlags flags, - const ImGuiWindowClass* window_class); -CIMGUI_API void igSetNextWindowDockID(ImGuiID dock_id, ImGuiCond cond); -CIMGUI_API void igSetNextWindowClass(const ImGuiWindowClass* window_class); -CIMGUI_API ImGuiID igGetWindowDockID(void); -CIMGUI_API bool igIsWindowDocked(void); -CIMGUI_API void igLogToTTY(int auto_open_depth); -CIMGUI_API void igLogToFile(int auto_open_depth, const char* filename); -CIMGUI_API void igLogToClipboard(int auto_open_depth); -CIMGUI_API void igLogFinish(void); -CIMGUI_API void igLogButtons(void); -CIMGUI_API void igLogTextV(const char* fmt, va_list args); -CIMGUI_API bool igBeginDragDropSource(ImGuiDragDropFlags flags); -CIMGUI_API bool igSetDragDropPayload(const char* type, - const void* data, - size_t sz, - ImGuiCond cond); -CIMGUI_API void igEndDragDropSource(void); -CIMGUI_API bool igBeginDragDropTarget(void); -CIMGUI_API const ImGuiPayload* igAcceptDragDropPayload( - const char* type, - ImGuiDragDropFlags flags); -CIMGUI_API void igEndDragDropTarget(void); -CIMGUI_API const ImGuiPayload* igGetDragDropPayload(void); -CIMGUI_API void igBeginDisabled(bool disabled); -CIMGUI_API void igEndDisabled(void); -CIMGUI_API void igPushClipRect(const ImVec2 clip_rect_min, - const ImVec2 clip_rect_max, - bool intersect_with_current_clip_rect); -CIMGUI_API void igPopClipRect(void); -CIMGUI_API void igSetItemDefaultFocus(void); -CIMGUI_API void igSetKeyboardFocusHere(int offset); -CIMGUI_API void igSetNextItemAllowOverlap(void); -CIMGUI_API bool igIsItemHovered(ImGuiHoveredFlags flags); -CIMGUI_API bool igIsItemActive(void); -CIMGUI_API bool igIsItemFocused(void); -CIMGUI_API bool igIsItemClicked(ImGuiMouseButton mouse_button); -CIMGUI_API bool igIsItemVisible(void); -CIMGUI_API bool igIsItemEdited(void); -CIMGUI_API bool igIsItemActivated(void); -CIMGUI_API bool igIsItemDeactivated(void); -CIMGUI_API bool igIsItemDeactivatedAfterEdit(void); -CIMGUI_API bool igIsItemToggledOpen(void); -CIMGUI_API bool igIsAnyItemHovered(void); -CIMGUI_API bool igIsAnyItemActive(void); -CIMGUI_API bool igIsAnyItemFocused(void); -CIMGUI_API ImGuiID igGetItemID(void); -CIMGUI_API void igGetItemRectMin(ImVec2* pOut); -CIMGUI_API void igGetItemRectMax(ImVec2* pOut); -CIMGUI_API void igGetItemRectSize(ImVec2* pOut); -CIMGUI_API ImGuiViewport* igGetMainViewport(void); -CIMGUI_API ImDrawList* igGetBackgroundDrawList_Nil(void); -CIMGUI_API ImDrawList* igGetForegroundDrawList_Nil(void); -CIMGUI_API ImDrawList* igGetBackgroundDrawList_ViewportPtr( - ImGuiViewport* viewport); -CIMGUI_API ImDrawList* igGetForegroundDrawList_ViewportPtr( - ImGuiViewport* viewport); -CIMGUI_API bool igIsRectVisible_Nil(const ImVec2 size); -CIMGUI_API bool igIsRectVisible_Vec2(const ImVec2 rect_min, - const ImVec2 rect_max); -CIMGUI_API double igGetTime(void); -CIMGUI_API int igGetFrameCount(void); -CIMGUI_API ImDrawListSharedData* igGetDrawListSharedData(void); -CIMGUI_API const char* igGetStyleColorName(ImGuiCol idx); -CIMGUI_API void igSetStateStorage(ImGuiStorage* storage); -CIMGUI_API ImGuiStorage* igGetStateStorage(void); -CIMGUI_API void igCalcTextSize(ImVec2* pOut, - const char* text, - const char* text_end, - bool hide_text_after_double_hash, - float wrap_width); -CIMGUI_API void igColorConvertU32ToFloat4(ImVec4* pOut, ImU32 in); -CIMGUI_API ImU32 igColorConvertFloat4ToU32(const ImVec4 in); -CIMGUI_API void igColorConvertRGBtoHSV(float r, - float g, - float b, - float* out_h, - float* out_s, - float* out_v); -CIMGUI_API void igColorConvertHSVtoRGB(float h, - float s, - float v, - float* out_r, - float* out_g, - float* out_b); -CIMGUI_API bool igIsKeyDown_Nil(ImGuiKey key); -CIMGUI_API bool igIsKeyPressed_Bool(ImGuiKey key, bool repeat); -CIMGUI_API bool igIsKeyReleased_Nil(ImGuiKey key); -CIMGUI_API bool igIsKeyChordPressed_Nil(ImGuiKeyChord key_chord); -CIMGUI_API int igGetKeyPressedAmount(ImGuiKey key, - float repeat_delay, - float rate); -CIMGUI_API const char* igGetKeyName(ImGuiKey key); -CIMGUI_API void igSetNextFrameWantCaptureKeyboard(bool want_capture_keyboard); -CIMGUI_API bool igIsMouseDown_Nil(ImGuiMouseButton button); -CIMGUI_API bool igIsMouseClicked_Bool(ImGuiMouseButton button, bool repeat); -CIMGUI_API bool igIsMouseReleased_Nil(ImGuiMouseButton button); -CIMGUI_API bool igIsMouseDoubleClicked_Nil(ImGuiMouseButton button); -CIMGUI_API int igGetMouseClickedCount(ImGuiMouseButton button); -CIMGUI_API bool igIsMouseHoveringRect(const ImVec2 r_min, - const ImVec2 r_max, - bool clip); -CIMGUI_API bool igIsMousePosValid(const ImVec2* mouse_pos); -CIMGUI_API bool igIsAnyMouseDown(void); -CIMGUI_API void igGetMousePos(ImVec2* pOut); -CIMGUI_API void igGetMousePosOnOpeningCurrentPopup(ImVec2* pOut); -CIMGUI_API bool igIsMouseDragging(ImGuiMouseButton button, - float lock_threshold); -CIMGUI_API void igGetMouseDragDelta(ImVec2* pOut, - ImGuiMouseButton button, - float lock_threshold); -CIMGUI_API void igResetMouseDragDelta(ImGuiMouseButton button); -CIMGUI_API ImGuiMouseCursor igGetMouseCursor(void); -CIMGUI_API void igSetMouseCursor(ImGuiMouseCursor cursor_type); -CIMGUI_API void igSetNextFrameWantCaptureMouse(bool want_capture_mouse); -CIMGUI_API const char* igGetClipboardText(void); -CIMGUI_API void igSetClipboardText(const char* text); -CIMGUI_API void igLoadIniSettingsFromDisk(const char* ini_filename); -CIMGUI_API void igLoadIniSettingsFromMemory(const char* ini_data, - size_t ini_size); -CIMGUI_API void igSaveIniSettingsToDisk(const char* ini_filename); -CIMGUI_API const char* igSaveIniSettingsToMemory(size_t* out_ini_size); -CIMGUI_API void igDebugTextEncoding(const char* text); -CIMGUI_API void igDebugFlashStyleColor(ImGuiCol idx); -CIMGUI_API void igDebugStartItemPicker(void); -CIMGUI_API bool igDebugCheckVersionAndDataLayout(const char* version_str, - size_t sz_io, - size_t sz_style, - size_t sz_vec2, - size_t sz_vec4, - size_t sz_drawvert, - size_t sz_drawidx); -CIMGUI_API void igSetAllocatorFunctions(ImGuiMemAllocFunc alloc_func, - ImGuiMemFreeFunc free_func, - void* user_data); -CIMGUI_API void igGetAllocatorFunctions(ImGuiMemAllocFunc* p_alloc_func, - ImGuiMemFreeFunc* p_free_func, - void** p_user_data); -CIMGUI_API void* igMemAlloc(size_t size); -CIMGUI_API void igMemFree(void* ptr); -CIMGUI_API ImGuiPlatformIO* igGetPlatformIO(void); -CIMGUI_API void igUpdatePlatformWindows(void); -CIMGUI_API void igRenderPlatformWindowsDefault(void* platform_render_arg, - void* renderer_render_arg); -CIMGUI_API void igDestroyPlatformWindows(void); -CIMGUI_API ImGuiViewport* igFindViewportByID(ImGuiID id); -CIMGUI_API ImGuiViewport* igFindViewportByPlatformHandle(void* platform_handle); -CIMGUI_API ImGuiTableSortSpecs* ImGuiTableSortSpecs_ImGuiTableSortSpecs(void); -CIMGUI_API void ImGuiTableSortSpecs_destroy(ImGuiTableSortSpecs* self); -CIMGUI_API ImGuiTableColumnSortSpecs* -ImGuiTableColumnSortSpecs_ImGuiTableColumnSortSpecs(void); -CIMGUI_API void ImGuiTableColumnSortSpecs_destroy( - ImGuiTableColumnSortSpecs* self); -CIMGUI_API ImGuiStyle* ImGuiStyle_ImGuiStyle(void); -CIMGUI_API void ImGuiStyle_destroy(ImGuiStyle* self); -CIMGUI_API void ImGuiStyle_ScaleAllSizes(ImGuiStyle* self, float scale_factor); -CIMGUI_API void ImGuiIO_AddKeyEvent(ImGuiIO* self, ImGuiKey key, bool down); -CIMGUI_API void ImGuiIO_AddKeyAnalogEvent(ImGuiIO* self, - ImGuiKey key, - bool down, - float v); -CIMGUI_API void ImGuiIO_AddMousePosEvent(ImGuiIO* self, float x, float y); -CIMGUI_API void ImGuiIO_AddMouseButtonEvent(ImGuiIO* self, - int button, - bool down); -CIMGUI_API void ImGuiIO_AddMouseWheelEvent(ImGuiIO* self, - float wheel_x, - float wheel_y); -CIMGUI_API void ImGuiIO_AddMouseSourceEvent(ImGuiIO* self, - ImGuiMouseSource source); -CIMGUI_API void ImGuiIO_AddMouseViewportEvent(ImGuiIO* self, ImGuiID id); -CIMGUI_API void ImGuiIO_AddFocusEvent(ImGuiIO* self, bool focused); -CIMGUI_API void ImGuiIO_AddInputCharacter(ImGuiIO* self, unsigned int c); -CIMGUI_API void ImGuiIO_AddInputCharacterUTF16(ImGuiIO* self, ImWchar16 c); -CIMGUI_API void ImGuiIO_AddInputCharactersUTF8(ImGuiIO* self, const char* str); -CIMGUI_API void ImGuiIO_SetKeyEventNativeData(ImGuiIO* self, - ImGuiKey key, - int native_keycode, - int native_scancode, - int native_legacy_index); -CIMGUI_API void ImGuiIO_SetAppAcceptingEvents(ImGuiIO* self, - bool accepting_events); -CIMGUI_API void ImGuiIO_ClearEventsQueue(ImGuiIO* self); -CIMGUI_API void ImGuiIO_ClearInputKeys(ImGuiIO* self); -CIMGUI_API ImGuiIO* ImGuiIO_ImGuiIO(void); -CIMGUI_API void ImGuiIO_destroy(ImGuiIO* self); -CIMGUI_API ImGuiInputTextCallbackData* -ImGuiInputTextCallbackData_ImGuiInputTextCallbackData(void); -CIMGUI_API void ImGuiInputTextCallbackData_destroy( - ImGuiInputTextCallbackData* self); -CIMGUI_API void ImGuiInputTextCallbackData_DeleteChars( - ImGuiInputTextCallbackData* self, - int pos, - int bytes_count); -CIMGUI_API void ImGuiInputTextCallbackData_InsertChars( - ImGuiInputTextCallbackData* self, - int pos, - const char* text, - const char* text_end); -CIMGUI_API void ImGuiInputTextCallbackData_SelectAll( - ImGuiInputTextCallbackData* self); -CIMGUI_API void ImGuiInputTextCallbackData_ClearSelection( - ImGuiInputTextCallbackData* self); -CIMGUI_API bool ImGuiInputTextCallbackData_HasSelection( - ImGuiInputTextCallbackData* self); -CIMGUI_API ImGuiWindowClass* ImGuiWindowClass_ImGuiWindowClass(void); -CIMGUI_API void ImGuiWindowClass_destroy(ImGuiWindowClass* self); -CIMGUI_API ImGuiPayload* ImGuiPayload_ImGuiPayload(void); -CIMGUI_API void ImGuiPayload_destroy(ImGuiPayload* self); -CIMGUI_API void ImGuiPayload_Clear(ImGuiPayload* self); -CIMGUI_API bool ImGuiPayload_IsDataType(ImGuiPayload* self, const char* type); -CIMGUI_API bool ImGuiPayload_IsPreview(ImGuiPayload* self); -CIMGUI_API bool ImGuiPayload_IsDelivery(ImGuiPayload* self); -CIMGUI_API ImGuiOnceUponAFrame* ImGuiOnceUponAFrame_ImGuiOnceUponAFrame(void); -CIMGUI_API void ImGuiOnceUponAFrame_destroy(ImGuiOnceUponAFrame* self); -CIMGUI_API ImGuiTextFilter* ImGuiTextFilter_ImGuiTextFilter( - const char* default_filter); -CIMGUI_API void ImGuiTextFilter_destroy(ImGuiTextFilter* self); -CIMGUI_API bool ImGuiTextFilter_Draw(ImGuiTextFilter* self, - const char* label, - float width); -CIMGUI_API bool ImGuiTextFilter_PassFilter(ImGuiTextFilter* self, - const char* text, - const char* text_end); -CIMGUI_API void ImGuiTextFilter_Build(ImGuiTextFilter* self); -CIMGUI_API void ImGuiTextFilter_Clear(ImGuiTextFilter* self); -CIMGUI_API bool ImGuiTextFilter_IsActive(ImGuiTextFilter* self); -CIMGUI_API ImGuiTextRange* ImGuiTextRange_ImGuiTextRange_Nil(void); -CIMGUI_API void ImGuiTextRange_destroy(ImGuiTextRange* self); -CIMGUI_API ImGuiTextRange* ImGuiTextRange_ImGuiTextRange_Str(const char* _b, - const char* _e); -CIMGUI_API bool ImGuiTextRange_empty(ImGuiTextRange* self); -CIMGUI_API void ImGuiTextRange_split(ImGuiTextRange* self, - char separator, - ImVector_ImGuiTextRange* out); -CIMGUI_API ImGuiTextBuffer* ImGuiTextBuffer_ImGuiTextBuffer(void); -CIMGUI_API void ImGuiTextBuffer_destroy(ImGuiTextBuffer* self); -CIMGUI_API const char* ImGuiTextBuffer_begin(ImGuiTextBuffer* self); -CIMGUI_API const char* ImGuiTextBuffer_end(ImGuiTextBuffer* self); -CIMGUI_API int ImGuiTextBuffer_size(ImGuiTextBuffer* self); -CIMGUI_API bool ImGuiTextBuffer_empty(ImGuiTextBuffer* self); -CIMGUI_API void ImGuiTextBuffer_clear(ImGuiTextBuffer* self); -CIMGUI_API void ImGuiTextBuffer_reserve(ImGuiTextBuffer* self, int capacity); -CIMGUI_API const char* ImGuiTextBuffer_c_str(ImGuiTextBuffer* self); -CIMGUI_API void ImGuiTextBuffer_append(ImGuiTextBuffer* self, - const char* str, - const char* str_end); -CIMGUI_API void ImGuiTextBuffer_appendfv(ImGuiTextBuffer* self, - const char* fmt, - va_list args); -CIMGUI_API ImGuiStoragePair* ImGuiStoragePair_ImGuiStoragePair_Int(ImGuiID _key, - int _val); -CIMGUI_API void ImGuiStoragePair_destroy(ImGuiStoragePair* self); -CIMGUI_API ImGuiStoragePair* ImGuiStoragePair_ImGuiStoragePair_Float( - ImGuiID _key, - float _val); -CIMGUI_API ImGuiStoragePair* ImGuiStoragePair_ImGuiStoragePair_Ptr(ImGuiID _key, - void* _val); -CIMGUI_API void ImGuiStorage_Clear(ImGuiStorage* self); -CIMGUI_API int ImGuiStorage_GetInt(ImGuiStorage* self, - ImGuiID key, - int default_val); -CIMGUI_API void ImGuiStorage_SetInt(ImGuiStorage* self, ImGuiID key, int val); -CIMGUI_API bool ImGuiStorage_GetBool(ImGuiStorage* self, - ImGuiID key, - bool default_val); -CIMGUI_API void ImGuiStorage_SetBool(ImGuiStorage* self, ImGuiID key, bool val); -CIMGUI_API float ImGuiStorage_GetFloat(ImGuiStorage* self, - ImGuiID key, - float default_val); -CIMGUI_API void ImGuiStorage_SetFloat(ImGuiStorage* self, - ImGuiID key, - float val); -CIMGUI_API void* ImGuiStorage_GetVoidPtr(ImGuiStorage* self, ImGuiID key); -CIMGUI_API void ImGuiStorage_SetVoidPtr(ImGuiStorage* self, - ImGuiID key, - void* val); -CIMGUI_API int* ImGuiStorage_GetIntRef(ImGuiStorage* self, - ImGuiID key, - int default_val); -CIMGUI_API bool* ImGuiStorage_GetBoolRef(ImGuiStorage* self, - ImGuiID key, - bool default_val); -CIMGUI_API float* ImGuiStorage_GetFloatRef(ImGuiStorage* self, - ImGuiID key, - float default_val); -CIMGUI_API void** ImGuiStorage_GetVoidPtrRef(ImGuiStorage* self, - ImGuiID key, - void* default_val); -CIMGUI_API void ImGuiStorage_BuildSortByKey(ImGuiStorage* self); -CIMGUI_API void ImGuiStorage_SetAllInt(ImGuiStorage* self, int val); -CIMGUI_API ImGuiListClipper* ImGuiListClipper_ImGuiListClipper(void); -CIMGUI_API void ImGuiListClipper_destroy(ImGuiListClipper* self); -CIMGUI_API void ImGuiListClipper_Begin(ImGuiListClipper* self, - int items_count, - float items_height); -CIMGUI_API void ImGuiListClipper_End(ImGuiListClipper* self); -CIMGUI_API bool ImGuiListClipper_Step(ImGuiListClipper* self); -CIMGUI_API void ImGuiListClipper_IncludeItemByIndex(ImGuiListClipper* self, - int item_index); -CIMGUI_API void ImGuiListClipper_IncludeItemsByIndex(ImGuiListClipper* self, - int item_begin, - int item_end); -CIMGUI_API ImColor* ImColor_ImColor_Nil(void); -CIMGUI_API void ImColor_destroy(ImColor* self); -CIMGUI_API ImColor* ImColor_ImColor_Float(float r, float g, float b, float a); -CIMGUI_API ImColor* ImColor_ImColor_Vec4(const ImVec4 col); -CIMGUI_API ImColor* ImColor_ImColor_Int(int r, int g, int b, int a); -CIMGUI_API ImColor* ImColor_ImColor_U32(ImU32 rgba); -CIMGUI_API void ImColor_SetHSV(ImColor* self, - float h, - float s, - float v, - float a); -CIMGUI_API void ImColor_HSV(ImColor* pOut, float h, float s, float v, float a); -CIMGUI_API ImDrawCmd* ImDrawCmd_ImDrawCmd(void); -CIMGUI_API void ImDrawCmd_destroy(ImDrawCmd* self); -CIMGUI_API ImTextureID ImDrawCmd_GetTexID(ImDrawCmd* self); -CIMGUI_API ImDrawListSplitter* ImDrawListSplitter_ImDrawListSplitter(void); -CIMGUI_API void ImDrawListSplitter_destroy(ImDrawListSplitter* self); -CIMGUI_API void ImDrawListSplitter_Clear(ImDrawListSplitter* self); -CIMGUI_API void ImDrawListSplitter_ClearFreeMemory(ImDrawListSplitter* self); -CIMGUI_API void ImDrawListSplitter_Split(ImDrawListSplitter* self, - ImDrawList* draw_list, - int count); -CIMGUI_API void ImDrawListSplitter_Merge(ImDrawListSplitter* self, - ImDrawList* draw_list); -CIMGUI_API void ImDrawListSplitter_SetCurrentChannel(ImDrawListSplitter* self, - ImDrawList* draw_list, - int channel_idx); -CIMGUI_API ImDrawList* ImDrawList_ImDrawList(ImDrawListSharedData* shared_data); -CIMGUI_API void ImDrawList_destroy(ImDrawList* self); -CIMGUI_API void ImDrawList_PushClipRect(ImDrawList* self, - const ImVec2 clip_rect_min, - const ImVec2 clip_rect_max, - bool intersect_with_current_clip_rect); -CIMGUI_API void ImDrawList_PushClipRectFullScreen(ImDrawList* self); -CIMGUI_API void ImDrawList_PopClipRect(ImDrawList* self); -CIMGUI_API void ImDrawList_PushTextureID(ImDrawList* self, - ImTextureID texture_id); -CIMGUI_API void ImDrawList_PopTextureID(ImDrawList* self); -CIMGUI_API void ImDrawList_GetClipRectMin(ImVec2* pOut, ImDrawList* self); -CIMGUI_API void ImDrawList_GetClipRectMax(ImVec2* pOut, ImDrawList* self); -CIMGUI_API void ImDrawList_AddLine(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - ImU32 col, - float thickness); -CIMGUI_API void ImDrawList_AddRect(ImDrawList* self, - const ImVec2 p_min, - const ImVec2 p_max, - ImU32 col, - float rounding, - ImDrawFlags flags, - float thickness); -CIMGUI_API void ImDrawList_AddRectFilled(ImDrawList* self, - const ImVec2 p_min, - const ImVec2 p_max, - ImU32 col, - float rounding, - ImDrawFlags flags); -CIMGUI_API void ImDrawList_AddRectFilledMultiColor(ImDrawList* self, - const ImVec2 p_min, - const ImVec2 p_max, - ImU32 col_upr_left, - ImU32 col_upr_right, - ImU32 col_bot_right, - ImU32 col_bot_left); -CIMGUI_API void ImDrawList_AddQuad(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - ImU32 col, - float thickness); -CIMGUI_API void ImDrawList_AddQuadFilled(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - ImU32 col); -CIMGUI_API void ImDrawList_AddTriangle(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - ImU32 col, - float thickness); -CIMGUI_API void ImDrawList_AddTriangleFilled(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - ImU32 col); -CIMGUI_API void ImDrawList_AddCircle(ImDrawList* self, - const ImVec2 center, - float radius, - ImU32 col, - int num_segments, - float thickness); -CIMGUI_API void ImDrawList_AddCircleFilled(ImDrawList* self, - const ImVec2 center, - float radius, - ImU32 col, - int num_segments); -CIMGUI_API void ImDrawList_AddNgon(ImDrawList* self, - const ImVec2 center, - float radius, - ImU32 col, - int num_segments, - float thickness); -CIMGUI_API void ImDrawList_AddNgonFilled(ImDrawList* self, - const ImVec2 center, - float radius, - ImU32 col, - int num_segments); -CIMGUI_API void ImDrawList_AddEllipse(ImDrawList* self, - const ImVec2 center, - const ImVec2 radius, - ImU32 col, - float rot, - int num_segments, - float thickness); -CIMGUI_API void ImDrawList_AddEllipseFilled(ImDrawList* self, - const ImVec2 center, - const ImVec2 radius, - ImU32 col, - float rot, - int num_segments); -CIMGUI_API void ImDrawList_AddText_Vec2(ImDrawList* self, - const ImVec2 pos, - ImU32 col, - const char* text_begin, - const char* text_end); -CIMGUI_API void ImDrawList_AddText_FontPtr(ImDrawList* self, - const ImFont* font, - float font_size, - const ImVec2 pos, - ImU32 col, - const char* text_begin, - const char* text_end, - float wrap_width, - const ImVec4* cpu_fine_clip_rect); -CIMGUI_API void ImDrawList_AddBezierCubic(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - ImU32 col, - float thickness, - int num_segments); -CIMGUI_API void ImDrawList_AddBezierQuadratic(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - ImU32 col, - float thickness, - int num_segments); -CIMGUI_API void ImDrawList_AddPolyline(ImDrawList* self, - const ImVec2* points, - int num_points, - ImU32 col, - ImDrawFlags flags, - float thickness); -CIMGUI_API void ImDrawList_AddConvexPolyFilled(ImDrawList* self, - const ImVec2* points, - int num_points, - ImU32 col); -CIMGUI_API void ImDrawList_AddConcavePolyFilled(ImDrawList* self, - const ImVec2* points, - int num_points, - ImU32 col); -CIMGUI_API void ImDrawList_AddImage(ImDrawList* self, - ImTextureID user_texture_id, - const ImVec2 p_min, - const ImVec2 p_max, - const ImVec2 uv_min, - const ImVec2 uv_max, - ImU32 col); -CIMGUI_API void ImDrawList_AddImageQuad(ImDrawList* self, - ImTextureID user_texture_id, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - const ImVec2 uv1, - const ImVec2 uv2, - const ImVec2 uv3, - const ImVec2 uv4, - ImU32 col); -CIMGUI_API void ImDrawList_AddImageRounded(ImDrawList* self, - ImTextureID user_texture_id, - const ImVec2 p_min, - const ImVec2 p_max, - const ImVec2 uv_min, - const ImVec2 uv_max, - ImU32 col, - float rounding, - ImDrawFlags flags); -CIMGUI_API void ImDrawList_PathClear(ImDrawList* self); -CIMGUI_API void ImDrawList_PathLineTo(ImDrawList* self, const ImVec2 pos); -CIMGUI_API void ImDrawList_PathLineToMergeDuplicate(ImDrawList* self, - const ImVec2 pos); -CIMGUI_API void ImDrawList_PathFillConvex(ImDrawList* self, ImU32 col); -CIMGUI_API void ImDrawList_PathFillConcave(ImDrawList* self, ImU32 col); -CIMGUI_API void ImDrawList_PathStroke(ImDrawList* self, - ImU32 col, - ImDrawFlags flags, - float thickness); -CIMGUI_API void ImDrawList_PathArcTo(ImDrawList* self, - const ImVec2 center, - float radius, - float a_min, - float a_max, - int num_segments); -CIMGUI_API void ImDrawList_PathArcToFast(ImDrawList* self, - const ImVec2 center, - float radius, - int a_min_of_12, - int a_max_of_12); -CIMGUI_API void ImDrawList_PathEllipticalArcTo(ImDrawList* self, - const ImVec2 center, - const ImVec2 radius, - float rot, - float a_min, - float a_max, - int num_segments); -CIMGUI_API void ImDrawList_PathBezierCubicCurveTo(ImDrawList* self, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - int num_segments); -CIMGUI_API void ImDrawList_PathBezierQuadraticCurveTo(ImDrawList* self, - const ImVec2 p2, - const ImVec2 p3, - int num_segments); -CIMGUI_API void ImDrawList_PathRect(ImDrawList* self, - const ImVec2 rect_min, - const ImVec2 rect_max, - float rounding, - ImDrawFlags flags); -CIMGUI_API void ImDrawList_AddCallback(ImDrawList* self, - ImDrawCallback callback, - void* callback_data); -CIMGUI_API void ImDrawList_AddDrawCmd(ImDrawList* self); -CIMGUI_API ImDrawList* ImDrawList_CloneOutput(ImDrawList* self); -CIMGUI_API void ImDrawList_ChannelsSplit(ImDrawList* self, int count); -CIMGUI_API void ImDrawList_ChannelsMerge(ImDrawList* self); -CIMGUI_API void ImDrawList_ChannelsSetCurrent(ImDrawList* self, int n); -CIMGUI_API void ImDrawList_PrimReserve(ImDrawList* self, - int idx_count, - int vtx_count); -CIMGUI_API void ImDrawList_PrimUnreserve(ImDrawList* self, - int idx_count, - int vtx_count); -CIMGUI_API void ImDrawList_PrimRect(ImDrawList* self, - const ImVec2 a, - const ImVec2 b, - ImU32 col); -CIMGUI_API void ImDrawList_PrimRectUV(ImDrawList* self, - const ImVec2 a, - const ImVec2 b, - const ImVec2 uv_a, - const ImVec2 uv_b, - ImU32 col); -CIMGUI_API void ImDrawList_PrimQuadUV(ImDrawList* self, - const ImVec2 a, - const ImVec2 b, - const ImVec2 c, - const ImVec2 d, - const ImVec2 uv_a, - const ImVec2 uv_b, - const ImVec2 uv_c, - const ImVec2 uv_d, - ImU32 col); -CIMGUI_API void ImDrawList_PrimWriteVtx(ImDrawList* self, - const ImVec2 pos, - const ImVec2 uv, - ImU32 col); -CIMGUI_API void ImDrawList_PrimWriteIdx(ImDrawList* self, ImDrawIdx idx); -CIMGUI_API void ImDrawList_PrimVtx(ImDrawList* self, - const ImVec2 pos, - const ImVec2 uv, - ImU32 col); -CIMGUI_API void ImDrawList__ResetForNewFrame(ImDrawList* self); -CIMGUI_API void ImDrawList__ClearFreeMemory(ImDrawList* self); -CIMGUI_API void ImDrawList__PopUnusedDrawCmd(ImDrawList* self); -CIMGUI_API void ImDrawList__TryMergeDrawCmds(ImDrawList* self); -CIMGUI_API void ImDrawList__OnChangedClipRect(ImDrawList* self); -CIMGUI_API void ImDrawList__OnChangedTextureID(ImDrawList* self); -CIMGUI_API void ImDrawList__OnChangedVtxOffset(ImDrawList* self); -CIMGUI_API int ImDrawList__CalcCircleAutoSegmentCount(ImDrawList* self, - float radius); -CIMGUI_API void ImDrawList__PathArcToFastEx(ImDrawList* self, - const ImVec2 center, - float radius, - int a_min_sample, - int a_max_sample, - int a_step); -CIMGUI_API void ImDrawList__PathArcToN(ImDrawList* self, - const ImVec2 center, - float radius, - float a_min, - float a_max, - int num_segments); -CIMGUI_API ImDrawData* ImDrawData_ImDrawData(void); -CIMGUI_API void ImDrawData_destroy(ImDrawData* self); -CIMGUI_API void ImDrawData_Clear(ImDrawData* self); -CIMGUI_API void ImDrawData_AddDrawList(ImDrawData* self, ImDrawList* draw_list); -CIMGUI_API void ImDrawData_DeIndexAllBuffers(ImDrawData* self); -CIMGUI_API void ImDrawData_ScaleClipRects(ImDrawData* self, - const ImVec2 fb_scale); -CIMGUI_API ImFontConfig* ImFontConfig_ImFontConfig(void); -CIMGUI_API void ImFontConfig_destroy(ImFontConfig* self); -CIMGUI_API ImFontGlyphRangesBuilder* -ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder(void); -CIMGUI_API void ImFontGlyphRangesBuilder_destroy( - ImFontGlyphRangesBuilder* self); -CIMGUI_API void ImFontGlyphRangesBuilder_Clear(ImFontGlyphRangesBuilder* self); -CIMGUI_API bool ImFontGlyphRangesBuilder_GetBit(ImFontGlyphRangesBuilder* self, - size_t n); -CIMGUI_API void ImFontGlyphRangesBuilder_SetBit(ImFontGlyphRangesBuilder* self, - size_t n); -CIMGUI_API void ImFontGlyphRangesBuilder_AddChar(ImFontGlyphRangesBuilder* self, - ImWchar c); -CIMGUI_API void ImFontGlyphRangesBuilder_AddText(ImFontGlyphRangesBuilder* self, - const char* text, - const char* text_end); -CIMGUI_API void ImFontGlyphRangesBuilder_AddRanges( - ImFontGlyphRangesBuilder* self, - const ImWchar* ranges); -CIMGUI_API void ImFontGlyphRangesBuilder_BuildRanges( - ImFontGlyphRangesBuilder* self, - ImVector_ImWchar* out_ranges); -CIMGUI_API ImFontAtlasCustomRect* ImFontAtlasCustomRect_ImFontAtlasCustomRect( - void); -CIMGUI_API void ImFontAtlasCustomRect_destroy(ImFontAtlasCustomRect* self); -CIMGUI_API bool ImFontAtlasCustomRect_IsPacked(ImFontAtlasCustomRect* self); -CIMGUI_API ImFontAtlas* ImFontAtlas_ImFontAtlas(void); -CIMGUI_API void ImFontAtlas_destroy(ImFontAtlas* self); -CIMGUI_API ImFont* ImFontAtlas_AddFont(ImFontAtlas* self, - const ImFontConfig* font_cfg); -CIMGUI_API ImFont* ImFontAtlas_AddFontDefault(ImFontAtlas* self, - const ImFontConfig* font_cfg); -CIMGUI_API ImFont* ImFontAtlas_AddFontFromFileTTF(ImFontAtlas* self, - const char* filename, - float size_pixels, - const ImFontConfig* font_cfg, - const ImWchar* glyph_ranges); -CIMGUI_API ImFont* ImFontAtlas_AddFontFromMemoryTTF( - ImFontAtlas* self, - void* font_data, - int font_data_size, - float size_pixels, - const ImFontConfig* font_cfg, - const ImWchar* glyph_ranges); -CIMGUI_API ImFont* ImFontAtlas_AddFontFromMemoryCompressedTTF( - ImFontAtlas* self, - const void* compressed_font_data, - int compressed_font_data_size, - float size_pixels, - const ImFontConfig* font_cfg, - const ImWchar* glyph_ranges); -CIMGUI_API ImFont* ImFontAtlas_AddFontFromMemoryCompressedBase85TTF( - ImFontAtlas* self, - const char* compressed_font_data_base85, - float size_pixels, - const ImFontConfig* font_cfg, - const ImWchar* glyph_ranges); -CIMGUI_API void ImFontAtlas_ClearInputData(ImFontAtlas* self); -CIMGUI_API void ImFontAtlas_ClearTexData(ImFontAtlas* self); -CIMGUI_API void ImFontAtlas_ClearFonts(ImFontAtlas* self); -CIMGUI_API void ImFontAtlas_Clear(ImFontAtlas* self); -CIMGUI_API bool ImFontAtlas_Build(ImFontAtlas* self); -CIMGUI_API void ImFontAtlas_GetTexDataAsAlpha8(ImFontAtlas* self, - unsigned char** out_pixels, - int* out_width, - int* out_height, - int* out_bytes_per_pixel); -CIMGUI_API void ImFontAtlas_GetTexDataAsRGBA32(ImFontAtlas* self, - unsigned char** out_pixels, - int* out_width, - int* out_height, - int* out_bytes_per_pixel); -CIMGUI_API bool ImFontAtlas_IsBuilt(ImFontAtlas* self); -CIMGUI_API void ImFontAtlas_SetTexID(ImFontAtlas* self, ImTextureID id); -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesDefault(ImFontAtlas* self); -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesGreek(ImFontAtlas* self); -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesKorean(ImFontAtlas* self); -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesJapanese(ImFontAtlas* self); -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesChineseFull( - ImFontAtlas* self); -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesChineseSimplifiedCommon( - ImFontAtlas* self); -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesCyrillic(ImFontAtlas* self); -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesThai(ImFontAtlas* self); -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesVietnamese( - ImFontAtlas* self); -CIMGUI_API int ImFontAtlas_AddCustomRectRegular(ImFontAtlas* self, - int width, - int height); -CIMGUI_API int ImFontAtlas_AddCustomRectFontGlyph(ImFontAtlas* self, - ImFont* font, - ImWchar id, - int width, - int height, - float advance_x, - const ImVec2 offset); -CIMGUI_API ImFontAtlasCustomRect* ImFontAtlas_GetCustomRectByIndex( - ImFontAtlas* self, - int index); -CIMGUI_API void ImFontAtlas_CalcCustomRectUV(ImFontAtlas* self, - const ImFontAtlasCustomRect* rect, - ImVec2* out_uv_min, - ImVec2* out_uv_max); -CIMGUI_API bool ImFontAtlas_GetMouseCursorTexData(ImFontAtlas* self, - ImGuiMouseCursor cursor, - ImVec2* out_offset, - ImVec2* out_size, - ImVec2 out_uv_border[2], - ImVec2 out_uv_fill[2]); -CIMGUI_API ImFont* ImFont_ImFont(void); -CIMGUI_API void ImFont_destroy(ImFont* self); -CIMGUI_API const ImFontGlyph* ImFont_FindGlyph(ImFont* self, ImWchar c); -CIMGUI_API const ImFontGlyph* ImFont_FindGlyphNoFallback(ImFont* self, - ImWchar c); -CIMGUI_API float ImFont_GetCharAdvance(ImFont* self, ImWchar c); -CIMGUI_API bool ImFont_IsLoaded(ImFont* self); -CIMGUI_API const char* ImFont_GetDebugName(ImFont* self); -CIMGUI_API void ImFont_CalcTextSizeA(ImVec2* pOut, - ImFont* self, - float size, - float max_width, - float wrap_width, - const char* text_begin, - const char* text_end, - const char** remaining); -CIMGUI_API const char* ImFont_CalcWordWrapPositionA(ImFont* self, - float scale, - const char* text, - const char* text_end, - float wrap_width); -CIMGUI_API void ImFont_RenderChar(ImFont* self, - ImDrawList* draw_list, - float size, - const ImVec2 pos, - ImU32 col, - ImWchar c); -CIMGUI_API void ImFont_RenderText(ImFont* self, - ImDrawList* draw_list, - float size, - const ImVec2 pos, - ImU32 col, - const ImVec4 clip_rect, - const char* text_begin, - const char* text_end, - float wrap_width, - bool cpu_fine_clip); -CIMGUI_API void ImFont_BuildLookupTable(ImFont* self); -CIMGUI_API void ImFont_ClearOutputData(ImFont* self); -CIMGUI_API void ImFont_GrowIndex(ImFont* self, int new_size); -CIMGUI_API void ImFont_AddGlyph(ImFont* self, - const ImFontConfig* src_cfg, - ImWchar c, - float x0, - float y0, - float x1, - float y1, - float u0, - float v0, - float u1, - float v1, - float advance_x); -CIMGUI_API void ImFont_AddRemapChar(ImFont* self, - ImWchar dst, - ImWchar src, - bool overwrite_dst); -CIMGUI_API void ImFont_SetGlyphVisible(ImFont* self, ImWchar c, bool visible); -CIMGUI_API bool ImFont_IsGlyphRangeUnused(ImFont* self, - unsigned int c_begin, - unsigned int c_last); -CIMGUI_API ImGuiViewport* ImGuiViewport_ImGuiViewport(void); -CIMGUI_API void ImGuiViewport_destroy(ImGuiViewport* self); -CIMGUI_API void ImGuiViewport_GetCenter(ImVec2* pOut, ImGuiViewport* self); -CIMGUI_API void ImGuiViewport_GetWorkCenter(ImVec2* pOut, ImGuiViewport* self); -CIMGUI_API ImGuiPlatformIO* ImGuiPlatformIO_ImGuiPlatformIO(void); -CIMGUI_API void ImGuiPlatformIO_destroy(ImGuiPlatformIO* self); -CIMGUI_API ImGuiPlatformMonitor* ImGuiPlatformMonitor_ImGuiPlatformMonitor( - void); -CIMGUI_API void ImGuiPlatformMonitor_destroy(ImGuiPlatformMonitor* self); -CIMGUI_API ImGuiPlatformImeData* ImGuiPlatformImeData_ImGuiPlatformImeData( - void); -CIMGUI_API void ImGuiPlatformImeData_destroy(ImGuiPlatformImeData* self); -CIMGUI_API ImGuiID igImHashData(const void* data, - size_t data_size, - ImGuiID seed); -CIMGUI_API ImGuiID igImHashStr(const char* data, - size_t data_size, - ImGuiID seed); -CIMGUI_API void igImQsort(void* base, - size_t count, - size_t size_of_element, - int (*compare_func)(void const*, void const*)); -CIMGUI_API ImU32 igImAlphaBlendColors(ImU32 col_a, ImU32 col_b); -CIMGUI_API bool igImIsPowerOfTwo_Int(int v); -CIMGUI_API bool igImIsPowerOfTwo_U64(ImU64 v); -CIMGUI_API int igImUpperPowerOfTwo(int v); -CIMGUI_API int igImStricmp(const char* str1, const char* str2); -CIMGUI_API int igImStrnicmp(const char* str1, const char* str2, size_t count); -CIMGUI_API void igImStrncpy(char* dst, const char* src, size_t count); -CIMGUI_API char* igImStrdup(const char* str); -CIMGUI_API char* igImStrdupcpy(char* dst, size_t* p_dst_size, const char* str); -CIMGUI_API const char* igImStrchrRange(const char* str_begin, - const char* str_end, - char c); -CIMGUI_API const char* igImStreolRange(const char* str, const char* str_end); -CIMGUI_API const char* igImStristr(const char* haystack, - const char* haystack_end, - const char* needle, - const char* needle_end); -CIMGUI_API void igImStrTrimBlanks(char* str); -CIMGUI_API const char* igImStrSkipBlank(const char* str); -CIMGUI_API int igImStrlenW(const ImWchar* str); -CIMGUI_API const ImWchar* igImStrbolW(const ImWchar* buf_mid_line, - const ImWchar* buf_begin); -CIMGUI_API char igImToUpper(char c); -CIMGUI_API bool igImCharIsBlankA(char c); -CIMGUI_API bool igImCharIsBlankW(unsigned int c); -CIMGUI_API int igImFormatString(char* buf, - size_t buf_size, - const char* fmt, - ...); -CIMGUI_API int igImFormatStringV(char* buf, - size_t buf_size, - const char* fmt, - va_list args); -CIMGUI_API void igImFormatStringToTempBuffer(const char** out_buf, - const char** out_buf_end, - const char* fmt, - ...); -CIMGUI_API void igImFormatStringToTempBufferV(const char** out_buf, - const char** out_buf_end, - const char* fmt, - va_list args); -CIMGUI_API const char* igImParseFormatFindStart(const char* format); -CIMGUI_API const char* igImParseFormatFindEnd(const char* format); -CIMGUI_API const char* igImParseFormatTrimDecorations(const char* format, - char* buf, - size_t buf_size); -CIMGUI_API void igImParseFormatSanitizeForPrinting(const char* fmt_in, - char* fmt_out, - size_t fmt_out_size); -CIMGUI_API const char* igImParseFormatSanitizeForScanning(const char* fmt_in, - char* fmt_out, - size_t fmt_out_size); -CIMGUI_API int igImParseFormatPrecision(const char* format, int default_value); -CIMGUI_API const char* igImTextCharToUtf8(char out_buf[5], unsigned int c); -CIMGUI_API int igImTextStrToUtf8(char* out_buf, - int out_buf_size, - const ImWchar* in_text, - const ImWchar* in_text_end); -CIMGUI_API int igImTextCharFromUtf8(unsigned int* out_char, - const char* in_text, - const char* in_text_end); -CIMGUI_API int igImTextStrFromUtf8(ImWchar* out_buf, - int out_buf_size, - const char* in_text, - const char* in_text_end, - const char** in_remaining); -CIMGUI_API int igImTextCountCharsFromUtf8(const char* in_text, - const char* in_text_end); -CIMGUI_API int igImTextCountUtf8BytesFromChar(const char* in_text, - const char* in_text_end); -CIMGUI_API int igImTextCountUtf8BytesFromStr(const ImWchar* in_text, - const ImWchar* in_text_end); -CIMGUI_API const char* igImTextFindPreviousUtf8Codepoint( - const char* in_text_start, - const char* in_text_curr); -CIMGUI_API int igImTextCountLines(const char* in_text, const char* in_text_end); -CIMGUI_API ImFileHandle igImFileOpen(const char* filename, const char* mode); -CIMGUI_API bool igImFileClose(ImFileHandle file); -CIMGUI_API ImU64 igImFileGetSize(ImFileHandle file); -CIMGUI_API ImU64 igImFileRead(void* data, - ImU64 size, - ImU64 count, - ImFileHandle file); -CIMGUI_API ImU64 igImFileWrite(const void* data, - ImU64 size, - ImU64 count, - ImFileHandle file); -CIMGUI_API void* igImFileLoadToMemory(const char* filename, - const char* mode, - size_t* out_file_size, - int padding_bytes); -CIMGUI_API float igImPow_Float(float x, float y); -CIMGUI_API double igImPow_double(double x, double y); -CIMGUI_API float igImLog_Float(float x); -CIMGUI_API double igImLog_double(double x); -CIMGUI_API int igImAbs_Int(int x); -CIMGUI_API float igImAbs_Float(float x); -CIMGUI_API double igImAbs_double(double x); -CIMGUI_API float igImSign_Float(float x); -CIMGUI_API double igImSign_double(double x); -CIMGUI_API float igImRsqrt_Float(float x); -CIMGUI_API double igImRsqrt_double(double x); -CIMGUI_API void igImMin(ImVec2* pOut, const ImVec2 lhs, const ImVec2 rhs); -CIMGUI_API void igImMax(ImVec2* pOut, const ImVec2 lhs, const ImVec2 rhs); -CIMGUI_API void igImClamp(ImVec2* pOut, - const ImVec2 v, - const ImVec2 mn, - ImVec2 mx); -CIMGUI_API void igImLerp_Vec2Float(ImVec2* pOut, - const ImVec2 a, - const ImVec2 b, - float t); -CIMGUI_API void igImLerp_Vec2Vec2(ImVec2* pOut, - const ImVec2 a, - const ImVec2 b, - const ImVec2 t); -CIMGUI_API void igImLerp_Vec4(ImVec4* pOut, - const ImVec4 a, - const ImVec4 b, - float t); -CIMGUI_API float igImSaturate(float f); -CIMGUI_API float igImLengthSqr_Vec2(const ImVec2 lhs); -CIMGUI_API float igImLengthSqr_Vec4(const ImVec4 lhs); -CIMGUI_API float igImInvLength(const ImVec2 lhs, float fail_value); -CIMGUI_API float igImTrunc_Float(float f); -CIMGUI_API void igImTrunc_Vec2(ImVec2* pOut, const ImVec2 v); -CIMGUI_API float igImFloor_Float(float f); -CIMGUI_API void igImFloor_Vec2(ImVec2* pOut, const ImVec2 v); -CIMGUI_API int igImModPositive(int a, int b); -CIMGUI_API float igImDot(const ImVec2 a, const ImVec2 b); -CIMGUI_API void igImRotate(ImVec2* pOut, - const ImVec2 v, - float cos_a, - float sin_a); -CIMGUI_API float igImLinearSweep(float current, float target, float speed); -CIMGUI_API void igImMul(ImVec2* pOut, const ImVec2 lhs, const ImVec2 rhs); -CIMGUI_API bool igImIsFloatAboveGuaranteedIntegerPrecision(float f); -CIMGUI_API float igImExponentialMovingAverage(float avg, float sample, int n); -CIMGUI_API void igImBezierCubicCalc(ImVec2* pOut, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - float t); -CIMGUI_API void igImBezierCubicClosestPoint(ImVec2* pOut, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - const ImVec2 p, - int num_segments); -CIMGUI_API void igImBezierCubicClosestPointCasteljau(ImVec2* pOut, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - const ImVec2 p, - float tess_tol); -CIMGUI_API void igImBezierQuadraticCalc(ImVec2* pOut, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - float t); -CIMGUI_API void igImLineClosestPoint(ImVec2* pOut, - const ImVec2 a, - const ImVec2 b, - const ImVec2 p); -CIMGUI_API bool igImTriangleContainsPoint(const ImVec2 a, - const ImVec2 b, - const ImVec2 c, - const ImVec2 p); -CIMGUI_API void igImTriangleClosestPoint(ImVec2* pOut, - const ImVec2 a, - const ImVec2 b, - const ImVec2 c, - const ImVec2 p); -CIMGUI_API void igImTriangleBarycentricCoords(const ImVec2 a, - const ImVec2 b, - const ImVec2 c, - const ImVec2 p, - float* out_u, - float* out_v, - float* out_w); -CIMGUI_API float igImTriangleArea(const ImVec2 a, - const ImVec2 b, - const ImVec2 c); -CIMGUI_API bool igImTriangleIsClockwise(const ImVec2 a, - const ImVec2 b, - const ImVec2 c); -CIMGUI_API ImVec1* ImVec1_ImVec1_Nil(void); -CIMGUI_API void ImVec1_destroy(ImVec1* self); -CIMGUI_API ImVec1* ImVec1_ImVec1_Float(float _x); -CIMGUI_API ImVec2ih* ImVec2ih_ImVec2ih_Nil(void); -CIMGUI_API void ImVec2ih_destroy(ImVec2ih* self); -CIMGUI_API ImVec2ih* ImVec2ih_ImVec2ih_short(short _x, short _y); -CIMGUI_API ImVec2ih* ImVec2ih_ImVec2ih_Vec2(const ImVec2 rhs); -CIMGUI_API ImRect* ImRect_ImRect_Nil(void); -CIMGUI_API void ImRect_destroy(ImRect* self); -CIMGUI_API ImRect* ImRect_ImRect_Vec2(const ImVec2 min, const ImVec2 max); -CIMGUI_API ImRect* ImRect_ImRect_Vec4(const ImVec4 v); -CIMGUI_API ImRect* ImRect_ImRect_Float(float x1, float y1, float x2, float y2); -CIMGUI_API void ImRect_GetCenter(ImVec2* pOut, ImRect* self); -CIMGUI_API void ImRect_GetSize(ImVec2* pOut, ImRect* self); -CIMGUI_API float ImRect_GetWidth(ImRect* self); -CIMGUI_API float ImRect_GetHeight(ImRect* self); -CIMGUI_API float ImRect_GetArea(ImRect* self); -CIMGUI_API void ImRect_GetTL(ImVec2* pOut, ImRect* self); -CIMGUI_API void ImRect_GetTR(ImVec2* pOut, ImRect* self); -CIMGUI_API void ImRect_GetBL(ImVec2* pOut, ImRect* self); -CIMGUI_API void ImRect_GetBR(ImVec2* pOut, ImRect* self); -CIMGUI_API bool ImRect_Contains_Vec2(ImRect* self, const ImVec2 p); -CIMGUI_API bool ImRect_Contains_Rect(ImRect* self, const ImRect r); -CIMGUI_API bool ImRect_ContainsWithPad(ImRect* self, - const ImVec2 p, - const ImVec2 pad); -CIMGUI_API bool ImRect_Overlaps(ImRect* self, const ImRect r); -CIMGUI_API void ImRect_Add_Vec2(ImRect* self, const ImVec2 p); -CIMGUI_API void ImRect_Add_Rect(ImRect* self, const ImRect r); -CIMGUI_API void ImRect_Expand_Float(ImRect* self, const float amount); -CIMGUI_API void ImRect_Expand_Vec2(ImRect* self, const ImVec2 amount); -CIMGUI_API void ImRect_Translate(ImRect* self, const ImVec2 d); -CIMGUI_API void ImRect_TranslateX(ImRect* self, float dx); -CIMGUI_API void ImRect_TranslateY(ImRect* self, float dy); -CIMGUI_API void ImRect_ClipWith(ImRect* self, const ImRect r); -CIMGUI_API void ImRect_ClipWithFull(ImRect* self, const ImRect r); -CIMGUI_API void ImRect_Floor(ImRect* self); -CIMGUI_API bool ImRect_IsInverted(ImRect* self); -CIMGUI_API void ImRect_ToVec4(ImVec4* pOut, ImRect* self); -CIMGUI_API size_t igImBitArrayGetStorageSizeInBytes(int bitcount); -CIMGUI_API void igImBitArrayClearAllBits(ImU32* arr, int bitcount); -CIMGUI_API bool igImBitArrayTestBit(const ImU32* arr, int n); -CIMGUI_API void igImBitArrayClearBit(ImU32* arr, int n); -CIMGUI_API void igImBitArraySetBit(ImU32* arr, int n); -CIMGUI_API void igImBitArraySetBitRange(ImU32* arr, int n, int n2); -CIMGUI_API void ImBitVector_Create(ImBitVector* self, int sz); -CIMGUI_API void ImBitVector_Clear(ImBitVector* self); -CIMGUI_API bool ImBitVector_TestBit(ImBitVector* self, int n); -CIMGUI_API void ImBitVector_SetBit(ImBitVector* self, int n); -CIMGUI_API void ImBitVector_ClearBit(ImBitVector* self, int n); -CIMGUI_API void ImGuiTextIndex_clear(ImGuiTextIndex* self); -CIMGUI_API int ImGuiTextIndex_size(ImGuiTextIndex* self); -CIMGUI_API const char* ImGuiTextIndex_get_line_begin(ImGuiTextIndex* self, - const char* base, - int n); -CIMGUI_API const char* ImGuiTextIndex_get_line_end(ImGuiTextIndex* self, - const char* base, - int n); -CIMGUI_API void ImGuiTextIndex_append(ImGuiTextIndex* self, - const char* base, - int old_size, - int new_size); -CIMGUI_API ImDrawListSharedData* ImDrawListSharedData_ImDrawListSharedData( - void); -CIMGUI_API void ImDrawListSharedData_destroy(ImDrawListSharedData* self); -CIMGUI_API void ImDrawListSharedData_SetCircleTessellationMaxError( - ImDrawListSharedData* self, - float max_error); -CIMGUI_API ImDrawDataBuilder* ImDrawDataBuilder_ImDrawDataBuilder(void); -CIMGUI_API void ImDrawDataBuilder_destroy(ImDrawDataBuilder* self); -CIMGUI_API ImGuiStyleMod* ImGuiStyleMod_ImGuiStyleMod_Int(ImGuiStyleVar idx, - int v); -CIMGUI_API void ImGuiStyleMod_destroy(ImGuiStyleMod* self); -CIMGUI_API ImGuiStyleMod* ImGuiStyleMod_ImGuiStyleMod_Float(ImGuiStyleVar idx, - float v); -CIMGUI_API ImGuiStyleMod* ImGuiStyleMod_ImGuiStyleMod_Vec2(ImGuiStyleVar idx, - ImVec2 v); -CIMGUI_API ImGuiComboPreviewData* ImGuiComboPreviewData_ImGuiComboPreviewData( - void); -CIMGUI_API void ImGuiComboPreviewData_destroy(ImGuiComboPreviewData* self); -CIMGUI_API ImGuiMenuColumns* ImGuiMenuColumns_ImGuiMenuColumns(void); -CIMGUI_API void ImGuiMenuColumns_destroy(ImGuiMenuColumns* self); -CIMGUI_API void ImGuiMenuColumns_Update(ImGuiMenuColumns* self, - float spacing, - bool window_reappearing); -CIMGUI_API float ImGuiMenuColumns_DeclColumns(ImGuiMenuColumns* self, - float w_icon, - float w_label, - float w_shortcut, - float w_mark); -CIMGUI_API void ImGuiMenuColumns_CalcNextTotalWidth(ImGuiMenuColumns* self, - bool update_offsets); -CIMGUI_API ImGuiInputTextDeactivatedState* -ImGuiInputTextDeactivatedState_ImGuiInputTextDeactivatedState(void); -CIMGUI_API void ImGuiInputTextDeactivatedState_destroy( - ImGuiInputTextDeactivatedState* self); -CIMGUI_API void ImGuiInputTextDeactivatedState_ClearFreeMemory( - ImGuiInputTextDeactivatedState* self); -CIMGUI_API ImGuiInputTextState* ImGuiInputTextState_ImGuiInputTextState(void); -CIMGUI_API void ImGuiInputTextState_destroy(ImGuiInputTextState* self); -CIMGUI_API void ImGuiInputTextState_ClearText(ImGuiInputTextState* self); -CIMGUI_API void ImGuiInputTextState_ClearFreeMemory(ImGuiInputTextState* self); -CIMGUI_API int ImGuiInputTextState_GetUndoAvailCount(ImGuiInputTextState* self); -CIMGUI_API int ImGuiInputTextState_GetRedoAvailCount(ImGuiInputTextState* self); -CIMGUI_API void ImGuiInputTextState_OnKeyPressed(ImGuiInputTextState* self, - int key); -CIMGUI_API void ImGuiInputTextState_CursorAnimReset(ImGuiInputTextState* self); -CIMGUI_API void ImGuiInputTextState_CursorClamp(ImGuiInputTextState* self); -CIMGUI_API bool ImGuiInputTextState_HasSelection(ImGuiInputTextState* self); -CIMGUI_API void ImGuiInputTextState_ClearSelection(ImGuiInputTextState* self); -CIMGUI_API int ImGuiInputTextState_GetCursorPos(ImGuiInputTextState* self); -CIMGUI_API int ImGuiInputTextState_GetSelectionStart(ImGuiInputTextState* self); -CIMGUI_API int ImGuiInputTextState_GetSelectionEnd(ImGuiInputTextState* self); -CIMGUI_API void ImGuiInputTextState_SelectAll(ImGuiInputTextState* self); -CIMGUI_API void ImGuiInputTextState_ReloadUserBufAndSelectAll( - ImGuiInputTextState* self); -CIMGUI_API void ImGuiInputTextState_ReloadUserBufAndKeepSelection( - ImGuiInputTextState* self); -CIMGUI_API void ImGuiInputTextState_ReloadUserBufAndMoveToEnd( - ImGuiInputTextState* self); -CIMGUI_API ImGuiNextWindowData* ImGuiNextWindowData_ImGuiNextWindowData(void); -CIMGUI_API void ImGuiNextWindowData_destroy(ImGuiNextWindowData* self); -CIMGUI_API void ImGuiNextWindowData_ClearFlags(ImGuiNextWindowData* self); -CIMGUI_API ImGuiNextItemData* ImGuiNextItemData_ImGuiNextItemData(void); -CIMGUI_API void ImGuiNextItemData_destroy(ImGuiNextItemData* self); -CIMGUI_API void ImGuiNextItemData_ClearFlags(ImGuiNextItemData* self); -CIMGUI_API ImGuiLastItemData* ImGuiLastItemData_ImGuiLastItemData(void); -CIMGUI_API void ImGuiLastItemData_destroy(ImGuiLastItemData* self); -CIMGUI_API ImGuiStackSizes* ImGuiStackSizes_ImGuiStackSizes(void); -CIMGUI_API void ImGuiStackSizes_destroy(ImGuiStackSizes* self); -CIMGUI_API void ImGuiStackSizes_SetToContextState(ImGuiStackSizes* self, - ImGuiContext* ctx); -CIMGUI_API void ImGuiStackSizes_CompareWithContextState(ImGuiStackSizes* self, - ImGuiContext* ctx); -CIMGUI_API ImGuiPtrOrIndex* ImGuiPtrOrIndex_ImGuiPtrOrIndex_Ptr(void* ptr); -CIMGUI_API void ImGuiPtrOrIndex_destroy(ImGuiPtrOrIndex* self); -CIMGUI_API ImGuiPtrOrIndex* ImGuiPtrOrIndex_ImGuiPtrOrIndex_Int(int index); -CIMGUI_API void* ImGuiDataVarInfo_GetVarPtr(ImGuiDataVarInfo* self, - void* parent); -CIMGUI_API ImGuiPopupData* ImGuiPopupData_ImGuiPopupData(void); -CIMGUI_API void ImGuiPopupData_destroy(ImGuiPopupData* self); -CIMGUI_API ImGuiInputEvent* ImGuiInputEvent_ImGuiInputEvent(void); -CIMGUI_API void ImGuiInputEvent_destroy(ImGuiInputEvent* self); -CIMGUI_API ImGuiKeyRoutingData* ImGuiKeyRoutingData_ImGuiKeyRoutingData(void); -CIMGUI_API void ImGuiKeyRoutingData_destroy(ImGuiKeyRoutingData* self); -CIMGUI_API ImGuiKeyRoutingTable* ImGuiKeyRoutingTable_ImGuiKeyRoutingTable( - void); -CIMGUI_API void ImGuiKeyRoutingTable_destroy(ImGuiKeyRoutingTable* self); -CIMGUI_API void ImGuiKeyRoutingTable_Clear(ImGuiKeyRoutingTable* self); -CIMGUI_API ImGuiKeyOwnerData* ImGuiKeyOwnerData_ImGuiKeyOwnerData(void); -CIMGUI_API void ImGuiKeyOwnerData_destroy(ImGuiKeyOwnerData* self); -CIMGUI_API ImGuiListClipperRange ImGuiListClipperRange_FromIndices(int min, - int max); -CIMGUI_API ImGuiListClipperRange -ImGuiListClipperRange_FromPositions(float y1, - float y2, - int off_min, - int off_max); -CIMGUI_API ImGuiListClipperData* ImGuiListClipperData_ImGuiListClipperData( - void); -CIMGUI_API void ImGuiListClipperData_destroy(ImGuiListClipperData* self); -CIMGUI_API void ImGuiListClipperData_Reset(ImGuiListClipperData* self, - ImGuiListClipper* clipper); -CIMGUI_API ImGuiNavItemData* ImGuiNavItemData_ImGuiNavItemData(void); -CIMGUI_API void ImGuiNavItemData_destroy(ImGuiNavItemData* self); -CIMGUI_API void ImGuiNavItemData_Clear(ImGuiNavItemData* self); -CIMGUI_API ImGuiTypingSelectState* -ImGuiTypingSelectState_ImGuiTypingSelectState(void); -CIMGUI_API void ImGuiTypingSelectState_destroy(ImGuiTypingSelectState* self); -CIMGUI_API void ImGuiTypingSelectState_Clear(ImGuiTypingSelectState* self); -CIMGUI_API ImGuiOldColumnData* ImGuiOldColumnData_ImGuiOldColumnData(void); -CIMGUI_API void ImGuiOldColumnData_destroy(ImGuiOldColumnData* self); -CIMGUI_API ImGuiOldColumns* ImGuiOldColumns_ImGuiOldColumns(void); -CIMGUI_API void ImGuiOldColumns_destroy(ImGuiOldColumns* self); -CIMGUI_API ImGuiDockNode* ImGuiDockNode_ImGuiDockNode(ImGuiID id); -CIMGUI_API void ImGuiDockNode_destroy(ImGuiDockNode* self); -CIMGUI_API bool ImGuiDockNode_IsRootNode(ImGuiDockNode* self); -CIMGUI_API bool ImGuiDockNode_IsDockSpace(ImGuiDockNode* self); -CIMGUI_API bool ImGuiDockNode_IsFloatingNode(ImGuiDockNode* self); -CIMGUI_API bool ImGuiDockNode_IsCentralNode(ImGuiDockNode* self); -CIMGUI_API bool ImGuiDockNode_IsHiddenTabBar(ImGuiDockNode* self); -CIMGUI_API bool ImGuiDockNode_IsNoTabBar(ImGuiDockNode* self); -CIMGUI_API bool ImGuiDockNode_IsSplitNode(ImGuiDockNode* self); -CIMGUI_API bool ImGuiDockNode_IsLeafNode(ImGuiDockNode* self); -CIMGUI_API bool ImGuiDockNode_IsEmpty(ImGuiDockNode* self); -CIMGUI_API void ImGuiDockNode_Rect(ImRect* pOut, ImGuiDockNode* self); -CIMGUI_API void ImGuiDockNode_SetLocalFlags(ImGuiDockNode* self, - ImGuiDockNodeFlags flags); -CIMGUI_API void ImGuiDockNode_UpdateMergedFlags(ImGuiDockNode* self); -CIMGUI_API ImGuiDockContext* ImGuiDockContext_ImGuiDockContext(void); -CIMGUI_API void ImGuiDockContext_destroy(ImGuiDockContext* self); -CIMGUI_API ImGuiViewportP* ImGuiViewportP_ImGuiViewportP(void); -CIMGUI_API void ImGuiViewportP_destroy(ImGuiViewportP* self); -CIMGUI_API void ImGuiViewportP_ClearRequestFlags(ImGuiViewportP* self); -CIMGUI_API void ImGuiViewportP_CalcWorkRectPos(ImVec2* pOut, - ImGuiViewportP* self, - const ImVec2 off_min); -CIMGUI_API void ImGuiViewportP_CalcWorkRectSize(ImVec2* pOut, - ImGuiViewportP* self, - const ImVec2 off_min, - const ImVec2 off_max); -CIMGUI_API void ImGuiViewportP_UpdateWorkRect(ImGuiViewportP* self); -CIMGUI_API void ImGuiViewportP_GetMainRect(ImRect* pOut, ImGuiViewportP* self); -CIMGUI_API void ImGuiViewportP_GetWorkRect(ImRect* pOut, ImGuiViewportP* self); -CIMGUI_API void ImGuiViewportP_GetBuildWorkRect(ImRect* pOut, - ImGuiViewportP* self); -CIMGUI_API ImGuiWindowSettings* ImGuiWindowSettings_ImGuiWindowSettings(void); -CIMGUI_API void ImGuiWindowSettings_destroy(ImGuiWindowSettings* self); -CIMGUI_API char* ImGuiWindowSettings_GetName(ImGuiWindowSettings* self); -CIMGUI_API ImGuiSettingsHandler* ImGuiSettingsHandler_ImGuiSettingsHandler( - void); -CIMGUI_API void ImGuiSettingsHandler_destroy(ImGuiSettingsHandler* self); -CIMGUI_API ImGuiDebugAllocInfo* ImGuiDebugAllocInfo_ImGuiDebugAllocInfo(void); -CIMGUI_API void ImGuiDebugAllocInfo_destroy(ImGuiDebugAllocInfo* self); -CIMGUI_API ImGuiStackLevelInfo* ImGuiStackLevelInfo_ImGuiStackLevelInfo(void); -CIMGUI_API void ImGuiStackLevelInfo_destroy(ImGuiStackLevelInfo* self); -CIMGUI_API ImGuiIDStackTool* ImGuiIDStackTool_ImGuiIDStackTool(void); -CIMGUI_API void ImGuiIDStackTool_destroy(ImGuiIDStackTool* self); -CIMGUI_API ImGuiContextHook* ImGuiContextHook_ImGuiContextHook(void); -CIMGUI_API void ImGuiContextHook_destroy(ImGuiContextHook* self); -CIMGUI_API ImGuiContext* ImGuiContext_ImGuiContext( - ImFontAtlas* shared_font_atlas); -CIMGUI_API void ImGuiContext_destroy(ImGuiContext* self); -CIMGUI_API ImGuiWindow* ImGuiWindow_ImGuiWindow(ImGuiContext* context, - const char* name); -CIMGUI_API void ImGuiWindow_destroy(ImGuiWindow* self); -CIMGUI_API ImGuiID ImGuiWindow_GetID_Str(ImGuiWindow* self, - const char* str, - const char* str_end); -CIMGUI_API ImGuiID ImGuiWindow_GetID_Ptr(ImGuiWindow* self, const void* ptr); -CIMGUI_API ImGuiID ImGuiWindow_GetID_Int(ImGuiWindow* self, int n); -CIMGUI_API ImGuiID ImGuiWindow_GetIDFromRectangle(ImGuiWindow* self, - const ImRect r_abs); -CIMGUI_API void ImGuiWindow_Rect(ImRect* pOut, ImGuiWindow* self); -CIMGUI_API float ImGuiWindow_CalcFontSize(ImGuiWindow* self); -CIMGUI_API float ImGuiWindow_TitleBarHeight(ImGuiWindow* self); -CIMGUI_API void ImGuiWindow_TitleBarRect(ImRect* pOut, ImGuiWindow* self); -CIMGUI_API float ImGuiWindow_MenuBarHeight(ImGuiWindow* self); -CIMGUI_API void ImGuiWindow_MenuBarRect(ImRect* pOut, ImGuiWindow* self); -CIMGUI_API ImGuiTabItem* ImGuiTabItem_ImGuiTabItem(void); -CIMGUI_API void ImGuiTabItem_destroy(ImGuiTabItem* self); -CIMGUI_API ImGuiTabBar* ImGuiTabBar_ImGuiTabBar(void); -CIMGUI_API void ImGuiTabBar_destroy(ImGuiTabBar* self); -CIMGUI_API ImGuiTableColumn* ImGuiTableColumn_ImGuiTableColumn(void); -CIMGUI_API void ImGuiTableColumn_destroy(ImGuiTableColumn* self); -CIMGUI_API ImGuiTableInstanceData* -ImGuiTableInstanceData_ImGuiTableInstanceData(void); -CIMGUI_API void ImGuiTableInstanceData_destroy(ImGuiTableInstanceData* self); -CIMGUI_API ImGuiTable* ImGuiTable_ImGuiTable(void); -CIMGUI_API void ImGuiTable_destroy(ImGuiTable* self); -CIMGUI_API ImGuiTableTempData* ImGuiTableTempData_ImGuiTableTempData(void); -CIMGUI_API void ImGuiTableTempData_destroy(ImGuiTableTempData* self); -CIMGUI_API ImGuiTableColumnSettings* -ImGuiTableColumnSettings_ImGuiTableColumnSettings(void); -CIMGUI_API void ImGuiTableColumnSettings_destroy( - ImGuiTableColumnSettings* self); -CIMGUI_API ImGuiTableSettings* ImGuiTableSettings_ImGuiTableSettings(void); -CIMGUI_API void ImGuiTableSettings_destroy(ImGuiTableSettings* self); -CIMGUI_API ImGuiTableColumnSettings* ImGuiTableSettings_GetColumnSettings( - ImGuiTableSettings* self); -CIMGUI_API ImGuiWindow* igGetCurrentWindowRead(void); -CIMGUI_API ImGuiWindow* igGetCurrentWindow(void); -CIMGUI_API ImGuiWindow* igFindWindowByID(ImGuiID id); -CIMGUI_API ImGuiWindow* igFindWindowByName(const char* name); -CIMGUI_API void igUpdateWindowParentAndRootLinks(ImGuiWindow* window, - ImGuiWindowFlags flags, - ImGuiWindow* parent_window); -CIMGUI_API void igUpdateWindowSkipRefresh(ImGuiWindow* window); -CIMGUI_API void igCalcWindowNextAutoFitSize(ImVec2* pOut, ImGuiWindow* window); -CIMGUI_API bool igIsWindowChildOf(ImGuiWindow* window, - ImGuiWindow* potential_parent, - bool popup_hierarchy, - bool dock_hierarchy); -CIMGUI_API bool igIsWindowWithinBeginStackOf(ImGuiWindow* window, - ImGuiWindow* potential_parent); -CIMGUI_API bool igIsWindowAbove(ImGuiWindow* potential_above, - ImGuiWindow* potential_below); -CIMGUI_API bool igIsWindowNavFocusable(ImGuiWindow* window); -CIMGUI_API void igSetWindowPos_WindowPtr(ImGuiWindow* window, - const ImVec2 pos, - ImGuiCond cond); -CIMGUI_API void igSetWindowSize_WindowPtr(ImGuiWindow* window, - const ImVec2 size, - ImGuiCond cond); -CIMGUI_API void igSetWindowCollapsed_WindowPtr(ImGuiWindow* window, - bool collapsed, - ImGuiCond cond); -CIMGUI_API void igSetWindowHitTestHole(ImGuiWindow* window, - const ImVec2 pos, - const ImVec2 size); -CIMGUI_API void igSetWindowHiddenAndSkipItemsForCurrentFrame( - ImGuiWindow* window); -CIMGUI_API void igSetWindowParentWindowForFocusRoute( - ImGuiWindow* window, - ImGuiWindow* parent_window); -CIMGUI_API void igWindowRectAbsToRel(ImRect* pOut, - ImGuiWindow* window, - const ImRect r); -CIMGUI_API void igWindowRectRelToAbs(ImRect* pOut, - ImGuiWindow* window, - const ImRect r); -CIMGUI_API void igWindowPosRelToAbs(ImVec2* pOut, - ImGuiWindow* window, - const ImVec2 p); -CIMGUI_API void igFocusWindow(ImGuiWindow* window, - ImGuiFocusRequestFlags flags); -CIMGUI_API void igFocusTopMostWindowUnderOne(ImGuiWindow* under_this_window, - ImGuiWindow* ignore_window, - ImGuiViewport* filter_viewport, - ImGuiFocusRequestFlags flags); -CIMGUI_API void igBringWindowToFocusFront(ImGuiWindow* window); -CIMGUI_API void igBringWindowToDisplayFront(ImGuiWindow* window); -CIMGUI_API void igBringWindowToDisplayBack(ImGuiWindow* window); -CIMGUI_API void igBringWindowToDisplayBehind(ImGuiWindow* window, - ImGuiWindow* above_window); -CIMGUI_API int igFindWindowDisplayIndex(ImGuiWindow* window); -CIMGUI_API ImGuiWindow* igFindBottomMostVisibleWindowWithinBeginStack( - ImGuiWindow* window); -CIMGUI_API void igSetNextWindowRefreshPolicy(ImGuiWindowRefreshFlags flags); -CIMGUI_API void igSetCurrentFont(ImFont* font); -CIMGUI_API ImFont* igGetDefaultFont(void); -CIMGUI_API ImDrawList* igGetForegroundDrawList_WindowPtr(ImGuiWindow* window); -CIMGUI_API void igAddDrawListToDrawDataEx(ImDrawData* draw_data, - ImVector_ImDrawListPtr* out_list, - ImDrawList* draw_list); -CIMGUI_API void igInitialize(void); -CIMGUI_API void igShutdown(void); -CIMGUI_API void igUpdateInputEvents(bool trickle_fast_inputs); -CIMGUI_API void igUpdateHoveredWindowAndCaptureFlags(void); -CIMGUI_API void igStartMouseMovingWindow(ImGuiWindow* window); -CIMGUI_API void igStartMouseMovingWindowOrNode(ImGuiWindow* window, - ImGuiDockNode* node, - bool undock); -CIMGUI_API void igUpdateMouseMovingWindowNewFrame(void); -CIMGUI_API void igUpdateMouseMovingWindowEndFrame(void); -CIMGUI_API ImGuiID igAddContextHook(ImGuiContext* context, - const ImGuiContextHook* hook); -CIMGUI_API void igRemoveContextHook(ImGuiContext* context, - ImGuiID hook_to_remove); -CIMGUI_API void igCallContextHooks(ImGuiContext* context, - ImGuiContextHookType type); -CIMGUI_API void igTranslateWindowsInViewport(ImGuiViewportP* viewport, - const ImVec2 old_pos, - const ImVec2 new_pos); -CIMGUI_API void igScaleWindowsInViewport(ImGuiViewportP* viewport, float scale); -CIMGUI_API void igDestroyPlatformWindow(ImGuiViewportP* viewport); -CIMGUI_API void igSetWindowViewport(ImGuiWindow* window, - ImGuiViewportP* viewport); -CIMGUI_API void igSetCurrentViewport(ImGuiWindow* window, - ImGuiViewportP* viewport); -CIMGUI_API const ImGuiPlatformMonitor* igGetViewportPlatformMonitor( - ImGuiViewport* viewport); -CIMGUI_API ImGuiViewportP* igFindHoveredViewportFromPlatformWindowStack( - const ImVec2 mouse_platform_pos); -CIMGUI_API void igMarkIniSettingsDirty_Nil(void); -CIMGUI_API void igMarkIniSettingsDirty_WindowPtr(ImGuiWindow* window); -CIMGUI_API void igClearIniSettings(void); -CIMGUI_API void igAddSettingsHandler(const ImGuiSettingsHandler* handler); -CIMGUI_API void igRemoveSettingsHandler(const char* type_name); -CIMGUI_API ImGuiSettingsHandler* igFindSettingsHandler(const char* type_name); -CIMGUI_API ImGuiWindowSettings* igCreateNewWindowSettings(const char* name); -CIMGUI_API ImGuiWindowSettings* igFindWindowSettingsByID(ImGuiID id); -CIMGUI_API ImGuiWindowSettings* igFindWindowSettingsByWindow( - ImGuiWindow* window); -CIMGUI_API void igClearWindowSettings(const char* name); -CIMGUI_API void igLocalizeRegisterEntries(const ImGuiLocEntry* entries, - int count); -CIMGUI_API const char* igLocalizeGetMsg(ImGuiLocKey key); -CIMGUI_API void igSetScrollX_WindowPtr(ImGuiWindow* window, float scroll_x); -CIMGUI_API void igSetScrollY_WindowPtr(ImGuiWindow* window, float scroll_y); -CIMGUI_API void igSetScrollFromPosX_WindowPtr(ImGuiWindow* window, - float local_x, - float center_x_ratio); -CIMGUI_API void igSetScrollFromPosY_WindowPtr(ImGuiWindow* window, - float local_y, - float center_y_ratio); -CIMGUI_API void igScrollToItem(ImGuiScrollFlags flags); -CIMGUI_API void igScrollToRect(ImGuiWindow* window, - const ImRect rect, - ImGuiScrollFlags flags); -CIMGUI_API void igScrollToRectEx(ImVec2* pOut, - ImGuiWindow* window, - const ImRect rect, - ImGuiScrollFlags flags); -CIMGUI_API void igScrollToBringRectIntoView(ImGuiWindow* window, - const ImRect rect); -CIMGUI_API ImGuiItemStatusFlags igGetItemStatusFlags(void); -CIMGUI_API ImGuiItemFlags igGetItemFlags(void); -CIMGUI_API ImGuiID igGetActiveID(void); -CIMGUI_API ImGuiID igGetFocusID(void); -CIMGUI_API void igSetActiveID(ImGuiID id, ImGuiWindow* window); -CIMGUI_API void igSetFocusID(ImGuiID id, ImGuiWindow* window); -CIMGUI_API void igClearActiveID(void); -CIMGUI_API ImGuiID igGetHoveredID(void); -CIMGUI_API void igSetHoveredID(ImGuiID id); -CIMGUI_API void igKeepAliveID(ImGuiID id); -CIMGUI_API void igMarkItemEdited(ImGuiID id); -CIMGUI_API void igPushOverrideID(ImGuiID id); -CIMGUI_API ImGuiID igGetIDWithSeed_Str(const char* str_id_begin, - const char* str_id_end, - ImGuiID seed); -CIMGUI_API ImGuiID igGetIDWithSeed_Int(int n, ImGuiID seed); -CIMGUI_API void igItemSize_Vec2(const ImVec2 size, float text_baseline_y); -CIMGUI_API void igItemSize_Rect(const ImRect bb, float text_baseline_y); -CIMGUI_API bool igItemAdd(const ImRect bb, - ImGuiID id, - const ImRect* nav_bb, - ImGuiItemFlags extra_flags); -CIMGUI_API bool igItemHoverable(const ImRect bb, - ImGuiID id, - ImGuiItemFlags item_flags); -CIMGUI_API bool igIsWindowContentHoverable(ImGuiWindow* window, - ImGuiHoveredFlags flags); -CIMGUI_API bool igIsClippedEx(const ImRect bb, ImGuiID id); -CIMGUI_API void igSetLastItemData(ImGuiID item_id, - ImGuiItemFlags in_flags, - ImGuiItemStatusFlags status_flags, - const ImRect item_rect); -CIMGUI_API void igCalcItemSize(ImVec2* pOut, - ImVec2 size, - float default_w, - float default_h); -CIMGUI_API float igCalcWrapWidthForPos(const ImVec2 pos, float wrap_pos_x); -CIMGUI_API void igPushMultiItemsWidths(int components, float width_full); -CIMGUI_API bool igIsItemToggledSelection(void); -CIMGUI_API void igGetContentRegionMaxAbs(ImVec2* pOut); -CIMGUI_API void igShrinkWidths(ImGuiShrinkWidthItem* items, - int count, - float width_excess); -CIMGUI_API void igPushItemFlag(ImGuiItemFlags option, bool enabled); -CIMGUI_API void igPopItemFlag(void); -CIMGUI_API const ImGuiDataVarInfo* igGetStyleVarInfo(ImGuiStyleVar idx); -CIMGUI_API void igLogBegin(ImGuiLogType type, int auto_open_depth); -CIMGUI_API void igLogToBuffer(int auto_open_depth); -CIMGUI_API void igLogRenderedText(const ImVec2* ref_pos, - const char* text, - const char* text_end); -CIMGUI_API void igLogSetNextTextDecoration(const char* prefix, - const char* suffix); -CIMGUI_API bool igBeginChildEx(const char* name, - ImGuiID id, - const ImVec2 size_arg, - ImGuiChildFlags child_flags, - ImGuiWindowFlags window_flags); -CIMGUI_API void igOpenPopupEx(ImGuiID id, ImGuiPopupFlags popup_flags); -CIMGUI_API void igClosePopupToLevel(int remaining, - bool restore_focus_to_window_under_popup); -CIMGUI_API void igClosePopupsOverWindow( - ImGuiWindow* ref_window, - bool restore_focus_to_window_under_popup); -CIMGUI_API void igClosePopupsExceptModals(void); -CIMGUI_API bool igIsPopupOpen_ID(ImGuiID id, ImGuiPopupFlags popup_flags); -CIMGUI_API bool igBeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_flags); -CIMGUI_API bool igBeginTooltipEx(ImGuiTooltipFlags tooltip_flags, - ImGuiWindowFlags extra_window_flags); -CIMGUI_API bool igBeginTooltipHidden(void); -CIMGUI_API void igGetPopupAllowedExtentRect(ImRect* pOut, ImGuiWindow* window); -CIMGUI_API ImGuiWindow* igGetTopMostPopupModal(void); -CIMGUI_API ImGuiWindow* igGetTopMostAndVisiblePopupModal(void); -CIMGUI_API ImGuiWindow* igFindBlockingModal(ImGuiWindow* window); -CIMGUI_API void igFindBestWindowPosForPopup(ImVec2* pOut, ImGuiWindow* window); -CIMGUI_API void igFindBestWindowPosForPopupEx(ImVec2* pOut, - const ImVec2 ref_pos, - const ImVec2 size, - ImGuiDir* last_dir, - const ImRect r_outer, - const ImRect r_avoid, - ImGuiPopupPositionPolicy policy); -CIMGUI_API bool igBeginViewportSideBar(const char* name, - ImGuiViewport* viewport, - ImGuiDir dir, - float size, - ImGuiWindowFlags window_flags); -CIMGUI_API bool igBeginMenuEx(const char* label, - const char* icon, - bool enabled); -CIMGUI_API bool igMenuItemEx(const char* label, - const char* icon, - const char* shortcut, - bool selected, - bool enabled); -CIMGUI_API bool igBeginComboPopup(ImGuiID popup_id, - const ImRect bb, - ImGuiComboFlags flags); -CIMGUI_API bool igBeginComboPreview(void); -CIMGUI_API void igEndComboPreview(void); -CIMGUI_API void igNavInitWindow(ImGuiWindow* window, bool force_reinit); -CIMGUI_API void igNavInitRequestApplyResult(void); -CIMGUI_API bool igNavMoveRequestButNoResultYet(void); -CIMGUI_API void igNavMoveRequestSubmit(ImGuiDir move_dir, - ImGuiDir clip_dir, - ImGuiNavMoveFlags move_flags, - ImGuiScrollFlags scroll_flags); -CIMGUI_API void igNavMoveRequestForward(ImGuiDir move_dir, - ImGuiDir clip_dir, - ImGuiNavMoveFlags move_flags, - ImGuiScrollFlags scroll_flags); -CIMGUI_API void igNavMoveRequestResolveWithLastItem(ImGuiNavItemData* result); -CIMGUI_API void igNavMoveRequestResolveWithPastTreeNode( - ImGuiNavItemData* result, - ImGuiNavTreeNodeData* tree_node_data); -CIMGUI_API void igNavMoveRequestCancel(void); -CIMGUI_API void igNavMoveRequestApplyResult(void); -CIMGUI_API void igNavMoveRequestTryWrapping(ImGuiWindow* window, - ImGuiNavMoveFlags move_flags); -CIMGUI_API void igNavHighlightActivated(ImGuiID id); -CIMGUI_API void igNavClearPreferredPosForAxis(ImGuiAxis axis); -CIMGUI_API void igNavRestoreHighlightAfterMove(void); -CIMGUI_API void igNavUpdateCurrentWindowIsScrollPushableX(void); -CIMGUI_API void igSetNavWindow(ImGuiWindow* window); -CIMGUI_API void igSetNavID(ImGuiID id, - ImGuiNavLayer nav_layer, - ImGuiID focus_scope_id, - const ImRect rect_rel); -CIMGUI_API void igSetNavFocusScope(ImGuiID focus_scope_id); -CIMGUI_API void igFocusItem(void); -CIMGUI_API void igActivateItemByID(ImGuiID id); -CIMGUI_API bool igIsNamedKey(ImGuiKey key); -CIMGUI_API bool igIsNamedKeyOrModKey(ImGuiKey key); -CIMGUI_API bool igIsLegacyKey(ImGuiKey key); -CIMGUI_API bool igIsKeyboardKey(ImGuiKey key); -CIMGUI_API bool igIsGamepadKey(ImGuiKey key); -CIMGUI_API bool igIsMouseKey(ImGuiKey key); -CIMGUI_API bool igIsAliasKey(ImGuiKey key); -CIMGUI_API bool igIsModKey(ImGuiKey key); -CIMGUI_API ImGuiKeyChord igFixupKeyChord(ImGuiContext* ctx, - ImGuiKeyChord key_chord); -CIMGUI_API ImGuiKey igConvertSingleModFlagToKey(ImGuiContext* ctx, - ImGuiKey key); -CIMGUI_API ImGuiKeyData* igGetKeyData_ContextPtr(ImGuiContext* ctx, - ImGuiKey key); -CIMGUI_API ImGuiKeyData* igGetKeyData_Key(ImGuiKey key); -CIMGUI_API const char* igGetKeyChordName(ImGuiKeyChord key_chord); -CIMGUI_API ImGuiKey igMouseButtonToKey(ImGuiMouseButton button); -CIMGUI_API bool igIsMouseDragPastThreshold(ImGuiMouseButton button, - float lock_threshold); -CIMGUI_API void igGetKeyMagnitude2d(ImVec2* pOut, - ImGuiKey key_left, - ImGuiKey key_right, - ImGuiKey key_up, - ImGuiKey key_down); -CIMGUI_API float igGetNavTweakPressedAmount(ImGuiAxis axis); -CIMGUI_API int igCalcTypematicRepeatAmount(float t0, - float t1, - float repeat_delay, - float repeat_rate); -CIMGUI_API void igGetTypematicRepeatRate(ImGuiInputFlags flags, - float* repeat_delay, - float* repeat_rate); -CIMGUI_API void igTeleportMousePos(const ImVec2 pos); -CIMGUI_API void igSetActiveIdUsingAllKeyboardKeys(void); -CIMGUI_API bool igIsActiveIdUsingNavDir(ImGuiDir dir); -CIMGUI_API ImGuiID igGetKeyOwner(ImGuiKey key); -CIMGUI_API void igSetKeyOwner(ImGuiKey key, - ImGuiID owner_id, - ImGuiInputFlags flags); -CIMGUI_API void igSetKeyOwnersForKeyChord(ImGuiKeyChord key, - ImGuiID owner_id, - ImGuiInputFlags flags); -CIMGUI_API void igSetItemKeyOwner(ImGuiKey key, ImGuiInputFlags flags); -CIMGUI_API bool igTestKeyOwner(ImGuiKey key, ImGuiID owner_id); -CIMGUI_API ImGuiKeyOwnerData* igGetKeyOwnerData(ImGuiContext* ctx, - ImGuiKey key); -CIMGUI_API bool igIsKeyDown_ID(ImGuiKey key, ImGuiID owner_id); -CIMGUI_API bool igIsKeyPressed_ID(ImGuiKey key, - ImGuiID owner_id, - ImGuiInputFlags flags); -CIMGUI_API bool igIsKeyReleased_ID(ImGuiKey key, ImGuiID owner_id); -CIMGUI_API bool igIsMouseDown_ID(ImGuiMouseButton button, ImGuiID owner_id); -CIMGUI_API bool igIsMouseClicked_ID(ImGuiMouseButton button, - ImGuiID owner_id, - ImGuiInputFlags flags); -CIMGUI_API bool igIsMouseReleased_ID(ImGuiMouseButton button, ImGuiID owner_id); -CIMGUI_API bool igIsMouseDoubleClicked_ID(ImGuiMouseButton button, - ImGuiID owner_id); -CIMGUI_API bool igIsKeyChordPressed_ID(ImGuiKeyChord key_chord, - ImGuiID owner_id, - ImGuiInputFlags flags); -CIMGUI_API void igSetNextItemShortcut(ImGuiKeyChord key_chord); -CIMGUI_API bool igShortcut(ImGuiKeyChord key_chord, - ImGuiID owner_id, - ImGuiInputFlags flags); -CIMGUI_API bool igSetShortcutRouting(ImGuiKeyChord key_chord, - ImGuiID owner_id, - ImGuiInputFlags flags); -CIMGUI_API bool igTestShortcutRouting(ImGuiKeyChord key_chord, - ImGuiID owner_id); -CIMGUI_API ImGuiKeyRoutingData* igGetShortcutRoutingData( - ImGuiKeyChord key_chord); -CIMGUI_API void igDockContextInitialize(ImGuiContext* ctx); -CIMGUI_API void igDockContextShutdown(ImGuiContext* ctx); -CIMGUI_API void igDockContextClearNodes(ImGuiContext* ctx, - ImGuiID root_id, - bool clear_settings_refs); -CIMGUI_API void igDockContextRebuildNodes(ImGuiContext* ctx); -CIMGUI_API void igDockContextNewFrameUpdateUndocking(ImGuiContext* ctx); -CIMGUI_API void igDockContextNewFrameUpdateDocking(ImGuiContext* ctx); -CIMGUI_API void igDockContextEndFrame(ImGuiContext* ctx); -CIMGUI_API ImGuiID igDockContextGenNodeID(ImGuiContext* ctx); -CIMGUI_API void igDockContextQueueDock(ImGuiContext* ctx, - ImGuiWindow* target, - ImGuiDockNode* target_node, - ImGuiWindow* payload, - ImGuiDir split_dir, - float split_ratio, - bool split_outer); -CIMGUI_API void igDockContextQueueUndockWindow(ImGuiContext* ctx, - ImGuiWindow* window); -CIMGUI_API void igDockContextQueueUndockNode(ImGuiContext* ctx, - ImGuiDockNode* node); -CIMGUI_API void igDockContextProcessUndockWindow( - ImGuiContext* ctx, - ImGuiWindow* window, - bool clear_persistent_docking_ref); -CIMGUI_API void igDockContextProcessUndockNode(ImGuiContext* ctx, - ImGuiDockNode* node); -CIMGUI_API bool igDockContextCalcDropPosForDocking(ImGuiWindow* target, - ImGuiDockNode* target_node, - ImGuiWindow* payload_window, - ImGuiDockNode* payload_node, - ImGuiDir split_dir, - bool split_outer, - ImVec2* out_pos); -CIMGUI_API ImGuiDockNode* igDockContextFindNodeByID(ImGuiContext* ctx, - ImGuiID id); -CIMGUI_API void igDockNodeWindowMenuHandler_Default(ImGuiContext* ctx, - ImGuiDockNode* node, - ImGuiTabBar* tab_bar); -CIMGUI_API bool igDockNodeBeginAmendTabBar(ImGuiDockNode* node); -CIMGUI_API void igDockNodeEndAmendTabBar(void); -CIMGUI_API ImGuiDockNode* igDockNodeGetRootNode(ImGuiDockNode* node); -CIMGUI_API bool igDockNodeIsInHierarchyOf(ImGuiDockNode* node, - ImGuiDockNode* parent); -CIMGUI_API int igDockNodeGetDepth(const ImGuiDockNode* node); -CIMGUI_API ImGuiID igDockNodeGetWindowMenuButtonId(const ImGuiDockNode* node); -CIMGUI_API ImGuiDockNode* igGetWindowDockNode(void); -CIMGUI_API bool igGetWindowAlwaysWantOwnTabBar(ImGuiWindow* window); -CIMGUI_API void igBeginDocked(ImGuiWindow* window, bool* p_open); -CIMGUI_API void igBeginDockableDragDropSource(ImGuiWindow* window); -CIMGUI_API void igBeginDockableDragDropTarget(ImGuiWindow* window); -CIMGUI_API void igSetWindowDock(ImGuiWindow* window, - ImGuiID dock_id, - ImGuiCond cond); -CIMGUI_API void igDockBuilderDockWindow(const char* window_name, - ImGuiID node_id); -CIMGUI_API ImGuiDockNode* igDockBuilderGetNode(ImGuiID node_id); -CIMGUI_API ImGuiDockNode* igDockBuilderGetCentralNode(ImGuiID node_id); -CIMGUI_API ImGuiID igDockBuilderAddNode(ImGuiID node_id, - ImGuiDockNodeFlags flags); -CIMGUI_API void igDockBuilderRemoveNode(ImGuiID node_id); -CIMGUI_API void igDockBuilderRemoveNodeDockedWindows(ImGuiID node_id, - bool clear_settings_refs); -CIMGUI_API void igDockBuilderRemoveNodeChildNodes(ImGuiID node_id); -CIMGUI_API void igDockBuilderSetNodePos(ImGuiID node_id, ImVec2 pos); -CIMGUI_API void igDockBuilderSetNodeSize(ImGuiID node_id, ImVec2 size); -CIMGUI_API ImGuiID igDockBuilderSplitNode(ImGuiID node_id, - ImGuiDir split_dir, - float size_ratio_for_node_at_dir, - ImGuiID* out_id_at_dir, - ImGuiID* out_id_at_opposite_dir); -CIMGUI_API void igDockBuilderCopyDockSpace( - ImGuiID src_dockspace_id, - ImGuiID dst_dockspace_id, - ImVector_const_charPtr* in_window_remap_pairs); -CIMGUI_API void igDockBuilderCopyNode(ImGuiID src_node_id, - ImGuiID dst_node_id, - ImVector_ImGuiID* out_node_remap_pairs); -CIMGUI_API void igDockBuilderCopyWindowSettings(const char* src_name, - const char* dst_name); -CIMGUI_API void igDockBuilderFinish(ImGuiID node_id); -CIMGUI_API void igPushFocusScope(ImGuiID id); -CIMGUI_API void igPopFocusScope(void); -CIMGUI_API ImGuiID igGetCurrentFocusScope(void); -CIMGUI_API bool igIsDragDropActive(void); -CIMGUI_API bool igBeginDragDropTargetCustom(const ImRect bb, ImGuiID id); -CIMGUI_API void igClearDragDrop(void); -CIMGUI_API bool igIsDragDropPayloadBeingAccepted(void); -CIMGUI_API void igRenderDragDropTargetRect(const ImRect bb, - const ImRect item_clip_rect); -CIMGUI_API ImGuiTypingSelectRequest* igGetTypingSelectRequest( - ImGuiTypingSelectFlags flags); -CIMGUI_API int igTypingSelectFindMatch(ImGuiTypingSelectRequest* req, - int items_count, - const char* (*get_item_name_func)(void*, - int), - void* user_data, - int nav_item_idx); -CIMGUI_API int igTypingSelectFindNextSingleCharMatch( - ImGuiTypingSelectRequest* req, - int items_count, - const char* (*get_item_name_func)(void*, int), - void* user_data, - int nav_item_idx); -CIMGUI_API int igTypingSelectFindBestLeadingMatch( - ImGuiTypingSelectRequest* req, - int items_count, - const char* (*get_item_name_func)(void*, int), - void* user_data); -CIMGUI_API void igSetWindowClipRectBeforeSetChannel(ImGuiWindow* window, - const ImRect clip_rect); -CIMGUI_API void igBeginColumns(const char* str_id, - int count, - ImGuiOldColumnFlags flags); -CIMGUI_API void igEndColumns(void); -CIMGUI_API void igPushColumnClipRect(int column_index); -CIMGUI_API void igPushColumnsBackground(void); -CIMGUI_API void igPopColumnsBackground(void); -CIMGUI_API ImGuiID igGetColumnsID(const char* str_id, int count); -CIMGUI_API ImGuiOldColumns* igFindOrCreateColumns(ImGuiWindow* window, - ImGuiID id); -CIMGUI_API float igGetColumnOffsetFromNorm(const ImGuiOldColumns* columns, - float offset_norm); -CIMGUI_API float igGetColumnNormFromOffset(const ImGuiOldColumns* columns, - float offset); -CIMGUI_API void igTableOpenContextMenu(int column_n); -CIMGUI_API void igTableSetColumnWidth(int column_n, float width); -CIMGUI_API void igTableSetColumnSortDirection(int column_n, - ImGuiSortDirection sort_direction, - bool append_to_sort_specs); -CIMGUI_API int igTableGetHoveredColumn(void); -CIMGUI_API int igTableGetHoveredRow(void); -CIMGUI_API float igTableGetHeaderRowHeight(void); -CIMGUI_API float igTableGetHeaderAngledMaxLabelWidth(void); -CIMGUI_API void igTablePushBackgroundChannel(void); -CIMGUI_API void igTablePopBackgroundChannel(void); -CIMGUI_API void igTableAngledHeadersRowEx(ImGuiID row_id, - float angle, - float max_label_width, - const ImGuiTableHeaderData* data, - int data_count); -CIMGUI_API ImGuiTable* igGetCurrentTable(void); -CIMGUI_API ImGuiTable* igTableFindByID(ImGuiID id); -CIMGUI_API bool igBeginTableEx(const char* name, - ImGuiID id, - int columns_count, - ImGuiTableFlags flags, - const ImVec2 outer_size, - float inner_width); -CIMGUI_API void igTableBeginInitMemory(ImGuiTable* table, int columns_count); -CIMGUI_API void igTableBeginApplyRequests(ImGuiTable* table); -CIMGUI_API void igTableSetupDrawChannels(ImGuiTable* table); -CIMGUI_API void igTableUpdateLayout(ImGuiTable* table); -CIMGUI_API void igTableUpdateBorders(ImGuiTable* table); -CIMGUI_API void igTableUpdateColumnsWeightFromWidth(ImGuiTable* table); -CIMGUI_API void igTableDrawBorders(ImGuiTable* table); -CIMGUI_API void igTableDrawDefaultContextMenu( - ImGuiTable* table, - ImGuiTableFlags flags_for_section_to_display); -CIMGUI_API bool igTableBeginContextMenuPopup(ImGuiTable* table); -CIMGUI_API void igTableMergeDrawChannels(ImGuiTable* table); -CIMGUI_API ImGuiTableInstanceData* igTableGetInstanceData(ImGuiTable* table, - int instance_no); -CIMGUI_API ImGuiID igTableGetInstanceID(ImGuiTable* table, int instance_no); -CIMGUI_API void igTableSortSpecsSanitize(ImGuiTable* table); -CIMGUI_API void igTableSortSpecsBuild(ImGuiTable* table); -CIMGUI_API ImGuiSortDirection -igTableGetColumnNextSortDirection(ImGuiTableColumn* column); -CIMGUI_API void igTableFixColumnSortDirection(ImGuiTable* table, - ImGuiTableColumn* column); -CIMGUI_API float igTableGetColumnWidthAuto(ImGuiTable* table, - ImGuiTableColumn* column); -CIMGUI_API void igTableBeginRow(ImGuiTable* table); -CIMGUI_API void igTableEndRow(ImGuiTable* table); -CIMGUI_API void igTableBeginCell(ImGuiTable* table, int column_n); -CIMGUI_API void igTableEndCell(ImGuiTable* table); -CIMGUI_API void igTableGetCellBgRect(ImRect* pOut, - const ImGuiTable* table, - int column_n); -CIMGUI_API const char* igTableGetColumnName_TablePtr(const ImGuiTable* table, - int column_n); -CIMGUI_API ImGuiID igTableGetColumnResizeID(ImGuiTable* table, - int column_n, - int instance_no); -CIMGUI_API float igTableGetMaxColumnWidth(const ImGuiTable* table, - int column_n); -CIMGUI_API void igTableSetColumnWidthAutoSingle(ImGuiTable* table, - int column_n); -CIMGUI_API void igTableSetColumnWidthAutoAll(ImGuiTable* table); -CIMGUI_API void igTableRemove(ImGuiTable* table); -CIMGUI_API void igTableGcCompactTransientBuffers_TablePtr(ImGuiTable* table); -CIMGUI_API void igTableGcCompactTransientBuffers_TableTempDataPtr( - ImGuiTableTempData* table); -CIMGUI_API void igTableGcCompactSettings(void); -CIMGUI_API void igTableLoadSettings(ImGuiTable* table); -CIMGUI_API void igTableSaveSettings(ImGuiTable* table); -CIMGUI_API void igTableResetSettings(ImGuiTable* table); -CIMGUI_API ImGuiTableSettings* igTableGetBoundSettings(ImGuiTable* table); -CIMGUI_API void igTableSettingsAddSettingsHandler(void); -CIMGUI_API ImGuiTableSettings* igTableSettingsCreate(ImGuiID id, - int columns_count); -CIMGUI_API ImGuiTableSettings* igTableSettingsFindByID(ImGuiID id); -CIMGUI_API ImGuiTabBar* igGetCurrentTabBar(void); -CIMGUI_API bool igBeginTabBarEx(ImGuiTabBar* tab_bar, - const ImRect bb, - ImGuiTabBarFlags flags); -CIMGUI_API ImGuiTabItem* igTabBarFindTabByID(ImGuiTabBar* tab_bar, - ImGuiID tab_id); -CIMGUI_API ImGuiTabItem* igTabBarFindTabByOrder(ImGuiTabBar* tab_bar, - int order); -CIMGUI_API ImGuiTabItem* igTabBarFindMostRecentlySelectedTabForActiveWindow( - ImGuiTabBar* tab_bar); -CIMGUI_API ImGuiTabItem* igTabBarGetCurrentTab(ImGuiTabBar* tab_bar); -CIMGUI_API int igTabBarGetTabOrder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); -CIMGUI_API const char* igTabBarGetTabName(ImGuiTabBar* tab_bar, - ImGuiTabItem* tab); -CIMGUI_API void igTabBarAddTab(ImGuiTabBar* tab_bar, - ImGuiTabItemFlags tab_flags, - ImGuiWindow* window); -CIMGUI_API void igTabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id); -CIMGUI_API void igTabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); -CIMGUI_API void igTabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); -CIMGUI_API void igTabBarQueueReorder(ImGuiTabBar* tab_bar, - ImGuiTabItem* tab, - int offset); -CIMGUI_API void igTabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, - ImGuiTabItem* tab, - ImVec2 mouse_pos); -CIMGUI_API bool igTabBarProcessReorder(ImGuiTabBar* tab_bar); -CIMGUI_API bool igTabItemEx(ImGuiTabBar* tab_bar, - const char* label, - bool* p_open, - ImGuiTabItemFlags flags, - ImGuiWindow* docked_window); -CIMGUI_API void igTabItemCalcSize_Str(ImVec2* pOut, - const char* label, - bool has_close_button_or_unsaved_marker); -CIMGUI_API void igTabItemCalcSize_WindowPtr(ImVec2* pOut, ImGuiWindow* window); -CIMGUI_API void igTabItemBackground(ImDrawList* draw_list, - const ImRect bb, - ImGuiTabItemFlags flags, - ImU32 col); -CIMGUI_API void igTabItemLabelAndCloseButton(ImDrawList* draw_list, - const ImRect bb, - ImGuiTabItemFlags flags, - ImVec2 frame_padding, - const char* label, - ImGuiID tab_id, - ImGuiID close_button_id, - bool is_contents_visible, - bool* out_just_closed, - bool* out_text_clipped); -CIMGUI_API void igRenderText(ImVec2 pos, - const char* text, - const char* text_end, - bool hide_text_after_hash); -CIMGUI_API void igRenderTextWrapped(ImVec2 pos, - const char* text, - const char* text_end, - float wrap_width); -CIMGUI_API void igRenderTextClipped(const ImVec2 pos_min, - const ImVec2 pos_max, - const char* text, - const char* text_end, - const ImVec2* text_size_if_known, - const ImVec2 align, - const ImRect* clip_rect); -CIMGUI_API void igRenderTextClippedEx(ImDrawList* draw_list, - const ImVec2 pos_min, - const ImVec2 pos_max, - const char* text, - const char* text_end, - const ImVec2* text_size_if_known, - const ImVec2 align, - const ImRect* clip_rect); -CIMGUI_API void igRenderTextEllipsis(ImDrawList* draw_list, - const ImVec2 pos_min, - const ImVec2 pos_max, - float clip_max_x, - float ellipsis_max_x, - const char* text, - const char* text_end, - const ImVec2* text_size_if_known); -CIMGUI_API void igRenderFrame(ImVec2 p_min, - ImVec2 p_max, - ImU32 fill_col, - bool border, - float rounding); -CIMGUI_API void igRenderFrameBorder(ImVec2 p_min, ImVec2 p_max, float rounding); -CIMGUI_API void igRenderColorRectWithAlphaCheckerboard(ImDrawList* draw_list, - ImVec2 p_min, - ImVec2 p_max, - ImU32 fill_col, - float grid_step, - ImVec2 grid_off, - float rounding, - ImDrawFlags flags); -CIMGUI_API void igRenderNavHighlight(const ImRect bb, - ImGuiID id, - ImGuiNavHighlightFlags flags); -CIMGUI_API const char* igFindRenderedTextEnd(const char* text, - const char* text_end); -CIMGUI_API void igRenderMouseCursor(ImVec2 pos, - float scale, - ImGuiMouseCursor mouse_cursor, - ImU32 col_fill, - ImU32 col_border, - ImU32 col_shadow); -CIMGUI_API void igRenderArrow(ImDrawList* draw_list, - ImVec2 pos, - ImU32 col, - ImGuiDir dir, - float scale); -CIMGUI_API void igRenderBullet(ImDrawList* draw_list, ImVec2 pos, ImU32 col); -CIMGUI_API void igRenderCheckMark(ImDrawList* draw_list, - ImVec2 pos, - ImU32 col, - float sz); -CIMGUI_API void igRenderArrowPointingAt(ImDrawList* draw_list, - ImVec2 pos, - ImVec2 half_sz, - ImGuiDir direction, - ImU32 col); -CIMGUI_API void igRenderArrowDockMenu(ImDrawList* draw_list, - ImVec2 p_min, - float sz, - ImU32 col); -CIMGUI_API void igRenderRectFilledRangeH(ImDrawList* draw_list, - const ImRect rect, - ImU32 col, - float x_start_norm, - float x_end_norm, - float rounding); -CIMGUI_API void igRenderRectFilledWithHole(ImDrawList* draw_list, - const ImRect outer, - const ImRect inner, - ImU32 col, - float rounding); -CIMGUI_API ImDrawFlags igCalcRoundingFlagsForRectInRect(const ImRect r_in, - const ImRect r_outer, - float threshold); -CIMGUI_API void igTextEx(const char* text, - const char* text_end, - ImGuiTextFlags flags); -CIMGUI_API bool igButtonEx(const char* label, - const ImVec2 size_arg, - ImGuiButtonFlags flags); -CIMGUI_API bool igArrowButtonEx(const char* str_id, - ImGuiDir dir, - ImVec2 size_arg, - ImGuiButtonFlags flags); -CIMGUI_API bool igImageButtonEx(ImGuiID id, - ImTextureID texture_id, - const ImVec2 image_size, - const ImVec2 uv0, - const ImVec2 uv1, - const ImVec4 bg_col, - const ImVec4 tint_col, - ImGuiButtonFlags flags); -CIMGUI_API void igSeparatorEx(ImGuiSeparatorFlags flags, float thickness); -CIMGUI_API void igSeparatorTextEx(ImGuiID id, - const char* label, - const char* label_end, - float extra_width); -CIMGUI_API bool igCheckboxFlags_S64Ptr(const char* label, - ImS64* flags, - ImS64 flags_value); -CIMGUI_API bool igCheckboxFlags_U64Ptr(const char* label, - ImU64* flags, - ImU64 flags_value); -CIMGUI_API bool igCloseButton(ImGuiID id, const ImVec2 pos); -CIMGUI_API bool igCollapseButton(ImGuiID id, - const ImVec2 pos, - ImGuiDockNode* dock_node); -CIMGUI_API void igScrollbar(ImGuiAxis axis); -CIMGUI_API bool igScrollbarEx(const ImRect bb, - ImGuiID id, - ImGuiAxis axis, - ImS64* p_scroll_v, - ImS64 avail_v, - ImS64 contents_v, - ImDrawFlags flags); -CIMGUI_API void igGetWindowScrollbarRect(ImRect* pOut, - ImGuiWindow* window, - ImGuiAxis axis); -CIMGUI_API ImGuiID igGetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis); -CIMGUI_API ImGuiID igGetWindowResizeCornerID(ImGuiWindow* window, int n); -CIMGUI_API ImGuiID igGetWindowResizeBorderID(ImGuiWindow* window, ImGuiDir dir); -CIMGUI_API bool igButtonBehavior(const ImRect bb, - ImGuiID id, - bool* out_hovered, - bool* out_held, - ImGuiButtonFlags flags); -CIMGUI_API bool igDragBehavior(ImGuiID id, - ImGuiDataType data_type, - void* p_v, - float v_speed, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderBehavior(const ImRect bb, - ImGuiID id, - ImGuiDataType data_type, - void* p_v, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags, - ImRect* out_grab_bb); -CIMGUI_API bool igSplitterBehavior(const ImRect bb, - ImGuiID id, - ImGuiAxis axis, - float* size1, - float* size2, - float min_size1, - float min_size2, - float hover_extend, - float hover_visibility_delay, - ImU32 bg_col); -CIMGUI_API bool igTreeNodeBehavior(ImGuiID id, - ImGuiTreeNodeFlags flags, - const char* label, - const char* label_end); -CIMGUI_API void igTreePushOverrideID(ImGuiID id); -CIMGUI_API void igTreeNodeSetOpen(ImGuiID id, bool open); -CIMGUI_API bool igTreeNodeUpdateNextOpen(ImGuiID id, ImGuiTreeNodeFlags flags); -CIMGUI_API void igSetNextItemSelectionUserData( - ImGuiSelectionUserData selection_user_data); -CIMGUI_API const ImGuiDataTypeInfo* igDataTypeGetInfo(ImGuiDataType data_type); -CIMGUI_API int igDataTypeFormatString(char* buf, - int buf_size, - ImGuiDataType data_type, - const void* p_data, - const char* format); -CIMGUI_API void igDataTypeApplyOp(ImGuiDataType data_type, - int op, - void* output, - const void* arg_1, - const void* arg_2); -CIMGUI_API bool igDataTypeApplyFromText(const char* buf, - ImGuiDataType data_type, - void* p_data, - const char* format); -CIMGUI_API int igDataTypeCompare(ImGuiDataType data_type, - const void* arg_1, - const void* arg_2); -CIMGUI_API bool igDataTypeClamp(ImGuiDataType data_type, - void* p_data, - const void* p_min, - const void* p_max); -CIMGUI_API bool igInputTextEx(const char* label, - const char* hint, - char* buf, - int buf_size, - const ImVec2 size_arg, - ImGuiInputTextFlags flags, - ImGuiInputTextCallback callback, - void* user_data); -CIMGUI_API void igInputTextDeactivateHook(ImGuiID id); -CIMGUI_API bool igTempInputText(const ImRect bb, - ImGuiID id, - const char* label, - char* buf, - int buf_size, - ImGuiInputTextFlags flags); -CIMGUI_API bool igTempInputScalar(const ImRect bb, - ImGuiID id, - const char* label, - ImGuiDataType data_type, - void* p_data, - const char* format, - const void* p_clamp_min, - const void* p_clamp_max); -CIMGUI_API bool igTempInputIsActive(ImGuiID id); -CIMGUI_API ImGuiInputTextState* igGetInputTextState(ImGuiID id); -CIMGUI_API void igColorTooltip(const char* text, - const float* col, - ImGuiColorEditFlags flags); -CIMGUI_API void igColorEditOptionsPopup(const float* col, - ImGuiColorEditFlags flags); -CIMGUI_API void igColorPickerOptionsPopup(const float* ref_col, - ImGuiColorEditFlags flags); -CIMGUI_API int igPlotEx(ImGuiPlotType plot_type, - const char* label, - float (*values_getter)(void* data, int idx), - void* data, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - const ImVec2 size_arg); -CIMGUI_API void igShadeVertsLinearColorGradientKeepAlpha(ImDrawList* draw_list, - int vert_start_idx, - int vert_end_idx, - ImVec2 gradient_p0, - ImVec2 gradient_p1, - ImU32 col0, - ImU32 col1); -CIMGUI_API void igShadeVertsLinearUV(ImDrawList* draw_list, - int vert_start_idx, - int vert_end_idx, - const ImVec2 a, - const ImVec2 b, - const ImVec2 uv_a, - const ImVec2 uv_b, - bool clamp); -CIMGUI_API void igShadeVertsTransformPos(ImDrawList* draw_list, - int vert_start_idx, - int vert_end_idx, - const ImVec2 pivot_in, - float cos_a, - float sin_a, - const ImVec2 pivot_out); -CIMGUI_API void igGcCompactTransientMiscBuffers(void); -CIMGUI_API void igGcCompactTransientWindowBuffers(ImGuiWindow* window); -CIMGUI_API void igGcAwakeTransientWindowBuffers(ImGuiWindow* window); -CIMGUI_API void igDebugLog(const char* fmt, ...); -CIMGUI_API void igDebugLogV(const char* fmt, va_list args); -CIMGUI_API void igDebugAllocHook(ImGuiDebugAllocInfo* info, - int frame_count, - void* ptr, - size_t size); -CIMGUI_API void igErrorCheckEndFrameRecover(ImGuiErrorLogCallback log_callback, - void* user_data); -CIMGUI_API void igErrorCheckEndWindowRecover(ImGuiErrorLogCallback log_callback, - void* user_data); -CIMGUI_API void igErrorCheckUsingSetCursorPosToExtendParentBoundaries(void); -CIMGUI_API void igDebugDrawCursorPos(ImU32 col); -CIMGUI_API void igDebugDrawLineExtents(ImU32 col); -CIMGUI_API void igDebugDrawItemRect(ImU32 col); -CIMGUI_API void igDebugLocateItem(ImGuiID target_id); -CIMGUI_API void igDebugLocateItemOnHover(ImGuiID target_id); -CIMGUI_API void igDebugLocateItemResolveWithLastItem(void); -CIMGUI_API void igDebugBreakClearData(void); -CIMGUI_API bool igDebugBreakButton(const char* label, - const char* description_of_location); -CIMGUI_API void igDebugBreakButtonTooltip(bool keyboard_only, - const char* description_of_location); -CIMGUI_API void igShowFontAtlas(ImFontAtlas* atlas); -CIMGUI_API void igDebugHookIdInfo(ImGuiID id, - ImGuiDataType data_type, - const void* data_id, - const void* data_id_end); -CIMGUI_API void igDebugNodeColumns(ImGuiOldColumns* columns); -CIMGUI_API void igDebugNodeDockNode(ImGuiDockNode* node, const char* label); -CIMGUI_API void igDebugNodeDrawList(ImGuiWindow* window, - ImGuiViewportP* viewport, - const ImDrawList* draw_list, - const char* label); -CIMGUI_API void igDebugNodeDrawCmdShowMeshAndBoundingBox( - ImDrawList* out_draw_list, - const ImDrawList* draw_list, - const ImDrawCmd* draw_cmd, - bool show_mesh, - bool show_aabb); -CIMGUI_API void igDebugNodeFont(ImFont* font); -CIMGUI_API void igDebugNodeFontGlyph(ImFont* font, const ImFontGlyph* glyph); -CIMGUI_API void igDebugNodeStorage(ImGuiStorage* storage, const char* label); -CIMGUI_API void igDebugNodeTabBar(ImGuiTabBar* tab_bar, const char* label); -CIMGUI_API void igDebugNodeTable(ImGuiTable* table); -CIMGUI_API void igDebugNodeTableSettings(ImGuiTableSettings* settings); -CIMGUI_API void igDebugNodeInputTextState(ImGuiInputTextState* state); -CIMGUI_API void igDebugNodeTypingSelectState(ImGuiTypingSelectState* state); -CIMGUI_API void igDebugNodeWindow(ImGuiWindow* window, const char* label); -CIMGUI_API void igDebugNodeWindowSettings(ImGuiWindowSettings* settings); -CIMGUI_API void igDebugNodeWindowsList(ImVector_ImGuiWindowPtr* windows, - const char* label); -CIMGUI_API void igDebugNodeWindowsListByBeginStackParent( - ImGuiWindow** windows, - int windows_size, - ImGuiWindow* parent_in_begin_stack); -CIMGUI_API void igDebugNodeViewport(ImGuiViewportP* viewport); -CIMGUI_API void igDebugRenderKeyboardPreview(ImDrawList* draw_list); -CIMGUI_API void igDebugRenderViewportThumbnail(ImDrawList* draw_list, - ImGuiViewportP* viewport, - const ImRect bb); -CIMGUI_API void igImFontAtlasUpdateConfigDataPointers(ImFontAtlas* atlas); -CIMGUI_API void igImFontAtlasBuildInit(ImFontAtlas* atlas); -CIMGUI_API void igImFontAtlasBuildSetupFont(ImFontAtlas* atlas, - ImFont* font, - ImFontConfig* font_config, - float ascent, - float descent); -CIMGUI_API void igImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, - void* stbrp_context_opaque); -CIMGUI_API void igImFontAtlasBuildFinish(ImFontAtlas* atlas); -CIMGUI_API void igImFontAtlasBuildRender8bppRectFromString( - ImFontAtlas* atlas, - int x, - int y, - int w, - int h, - const char* in_str, - char in_marker_char, - unsigned char in_marker_pixel_value); -CIMGUI_API void igImFontAtlasBuildRender32bppRectFromString( - ImFontAtlas* atlas, - int x, - int y, - int w, - int h, - const char* in_str, - char in_marker_char, - unsigned int in_marker_pixel_value); -CIMGUI_API void igImFontAtlasBuildMultiplyCalcLookupTable( - unsigned char out_table[256], - float in_multiply_factor); -CIMGUI_API void igImFontAtlasBuildMultiplyRectAlpha8( - const unsigned char table[256], - unsigned char* pixels, - int x, - int y, - int w, - int h, - int stride); - -/////////////////////////hand written functions -// no LogTextV -CIMGUI_API void igLogText(CONST char* fmt, ...); -// no appendfV -CIMGUI_API void ImGuiTextBuffer_appendf(struct ImGuiTextBuffer* buffer, - const char* fmt, - ...); -// for getting FLT_MAX in bindings -CIMGUI_API float igGET_FLT_MAX(void); -// for getting FLT_MIN in bindings -CIMGUI_API float igGET_FLT_MIN(void); - -CIMGUI_API ImVector_ImWchar* ImVector_ImWchar_create(void); -CIMGUI_API void ImVector_ImWchar_destroy(ImVector_ImWchar* self); -CIMGUI_API void ImVector_ImWchar_Init(ImVector_ImWchar* p); -CIMGUI_API void ImVector_ImWchar_UnInit(ImVector_ImWchar* p); - -#endif // CIMGUI_INCLUDED diff --git a/pkg/dcimgui/build.zig b/pkg/dcimgui/build.zig new file mode 100644 index 000000000..95c4af303 --- /dev/null +++ b/pkg/dcimgui/build.zig @@ -0,0 +1,199 @@ +const std = @import("std"); +const NativeTargetInfo = std.zig.system.NativeTargetInfo; + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + const freetype = b.option(bool, "freetype", "Use Freetype") orelse false; + const backend_opengl3 = b.option(bool, "backend-opengl3", "OpenGL3 backend") orelse false; + const backend_metal = b.option(bool, "backend-metal", "Metal backend") orelse false; + const backend_osx = b.option(bool, "backend-osx", "OSX backend") orelse false; + + // Build options + const options = b.addOptions(); + options.addOption(bool, "freetype", freetype); + options.addOption(bool, "backend_opengl3", backend_opengl3); + options.addOption(bool, "backend_metal", backend_metal); + options.addOption(bool, "backend_osx", backend_osx); + + // Main static lib + const lib = b.addLibrary(.{ + .name = "dcimgui", + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + }), + .linkage = .static, + }); + lib.linkLibC(); + lib.linkLibCpp(); + b.installArtifact(lib); + + // Zig module + const mod = b.addModule("dcimgui", .{ + .root_source_file = b.path("main.zig"), + .target = target, + .optimize = optimize, + }); + mod.addOptions("build_options", options); + mod.linkLibrary(lib); + + // We need to add proper Apple SDKs to find stdlib headers + if (target.result.os.tag.isDarwin()) { + if (!target.query.isNative()) { + try @import("apple_sdk").addPaths(b, lib); + } + } + + // Flags for C compilation, common to all. + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ + "-DIMGUI_USE_WCHAR32=1", + "-DIMGUI_DISABLE_OBSOLETE_FUNCTIONS=1", + }); + if (freetype) try flags.appendSlice(b.allocator, &.{ + "-DIMGUI_ENABLE_FREETYPE=1", + }); + if (target.result.os.tag == .windows) { + try flags.appendSlice(b.allocator, &.{ + "-DIMGUI_IMPL_API=extern\t\"C\"\t__declspec(dllexport)", + }); + } else { + try flags.appendSlice(b.allocator, &.{ + "-DIMGUI_IMPL_API=extern\t\"C\"", + }); + } + if (target.result.os.tag == .freebsd or target.result.abi == .musl) { + try flags.append(b.allocator, "-fPIC"); + } + + // Add the core Dear Imgui source files + if (b.lazyDependency("imgui", .{})) |upstream| { + lib.addIncludePath(upstream.path("")); + lib.addCSourceFiles(.{ + .root = upstream.path(""), + .files = &.{ + "imgui_demo.cpp", + "imgui_draw.cpp", + "imgui_tables.cpp", + "imgui_widgets.cpp", + "imgui.cpp", + }, + .flags = flags.items, + }); + + lib.installHeadersDirectory( + upstream.path(""), + "", + .{ .include_extensions = &.{".h"} }, + ); + + if (freetype) { + lib.addCSourceFile(.{ + .file = upstream.path("misc/freetype/imgui_freetype.cpp"), + .flags = flags.items, + }); + + if (b.systemIntegrationOption("freetype", .{})) { + lib.linkSystemLibrary2("freetype2", dynamic_link_opts); + } else { + const freetype_dep = b.dependency("freetype", .{ + .target = target, + .optimize = optimize, + .@"enable-libpng" = true, + }); + lib.linkLibrary(freetype_dep.artifact("freetype")); + if (freetype_dep.builder.lazyDependency( + "freetype", + .{}, + )) |freetype_upstream| { + mod.addIncludePath(freetype_upstream.path("include")); + } + } + } + + if (backend_metal) { + lib.addCSourceFiles(.{ + .root = upstream.path("backends"), + .files = &.{"imgui_impl_metal.mm"}, + .flags = flags.items, + }); + lib.installHeadersDirectory( + upstream.path("backends"), + "", + .{ .include_extensions = &.{"imgui_impl_metal.h"} }, + ); + } + if (backend_osx) { + lib.addCSourceFiles(.{ + .root = upstream.path("backends"), + .files = &.{"imgui_impl_osx.mm"}, + .flags = flags.items, + }); + lib.installHeadersDirectory( + upstream.path("backends"), + "", + .{ .include_extensions = &.{"imgui_impl_osx.h"} }, + ); + } + if (backend_opengl3) { + lib.addCSourceFiles(.{ + .root = upstream.path("backends"), + .files = &.{"imgui_impl_opengl3.cpp"}, + .flags = flags.items, + }); + lib.installHeadersDirectory( + upstream.path("backends"), + "", + .{ .include_extensions = &.{"imgui_impl_opengl3.h"} }, + ); + } + } + + // Add the C bindings + if (b.lazyDependency("bindings", .{})) |upstream| { + lib.addIncludePath(upstream.path("")); + lib.addCSourceFiles(.{ + .root = upstream.path(""), + .files = &.{ + "dcimgui.cpp", + "dcimgui_internal.cpp", + }, + .flags = flags.items, + }); + lib.addCSourceFiles(.{ + .root = b.path(""), + .files = &.{"ext.cpp"}, + .flags = flags.items, + }); + + lib.installHeadersDirectory( + upstream.path(""), + "", + .{ .include_extensions = &.{".h"} }, + ); + } + + const test_exe = b.addTest(.{ + .name = "test", + .root_module = b.createModule(.{ + .root_source_file = b.path("main.zig"), + .target = target, + .optimize = optimize, + }), + }); + test_exe.root_module.addOptions("build_options", options); + test_exe.linkLibrary(lib); + const tests_run = b.addRunArtifact(test_exe); + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&tests_run.step); +} + +// For dynamic linking, we prefer dynamic linking and to search by +// mode first. Mode first will search all paths for a dynamic library +// before falling back to static. +const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{ + .preferred_link_mode = .dynamic, + .search_strategy = .mode_first, +}; diff --git a/pkg/dcimgui/build.zig.zon b/pkg/dcimgui/build.zig.zon new file mode 100644 index 000000000..95d0120e1 --- /dev/null +++ b/pkg/dcimgui/build.zig.zon @@ -0,0 +1,26 @@ +.{ + .name = .dcimgui, + .version = "1.92.5", // -docking branch + .fingerprint = 0x1a25797442c6324f, + .paths = .{""}, + .dependencies = .{ + // The bindings and imgui versions below must match exactly. + + .bindings = .{ + // https://github.com/dearimgui/dear_bindings + .url = "https://deps.files.ghostty.org/DearBindings_v0.17_ImGui_v1.92.5-docking.tar.gz", + .hash = "N-V-__8AANT61wB--nJ95Gj_ctmzAtcjloZ__hRqNw5lC1Kr", + .lazy = true, + }, + + .imgui = .{ + // https://github.com/ocornut/imgui + .url = "https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz", + .hash = "N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI", + .lazy = true, + }, + + .apple_sdk = .{ .path = "../apple-sdk" }, + .freetype = .{ .path = "../freetype" }, + }, +} diff --git a/pkg/dcimgui/ext.cpp b/pkg/dcimgui/ext.cpp new file mode 100644 index 000000000..d4732e0fa --- /dev/null +++ b/pkg/dcimgui/ext.cpp @@ -0,0 +1,30 @@ +#include "imgui.h" + +// This file contains custom extensions for functionality that isn't +// properly supported by Dear Bindings yet. Namely: +// https://github.com/dearimgui/dear_bindings/issues/55 + +// Wrap this in a namespace to keep it separate from the C++ API +namespace cimgui +{ +#include "dcimgui.h" +} + +extern "C" +{ +CIMGUI_API void ImFontConfig_ImFontConfig(cimgui::ImFontConfig* self) +{ + static_assert(sizeof(cimgui::ImFontConfig) == sizeof(::ImFontConfig), "ImFontConfig size mismatch"); + static_assert(alignof(cimgui::ImFontConfig) == alignof(::ImFontConfig), "ImFontConfig alignment mismatch"); + ::ImFontConfig defaults; + *reinterpret_cast<::ImFontConfig*>(self) = defaults; +} + +CIMGUI_API void ImGuiStyle_ImGuiStyle(cimgui::ImGuiStyle* self) +{ + static_assert(sizeof(cimgui::ImGuiStyle) == sizeof(::ImGuiStyle), "ImGuiStyle size mismatch"); + static_assert(alignof(cimgui::ImGuiStyle) == alignof(::ImGuiStyle), "ImGuiStyle alignment mismatch"); + ::ImGuiStyle defaults; + *reinterpret_cast<::ImGuiStyle*>(self) = defaults; +} +} diff --git a/pkg/dcimgui/main.zig b/pkg/dcimgui/main.zig new file mode 100644 index 000000000..e709158f5 --- /dev/null +++ b/pkg/dcimgui/main.zig @@ -0,0 +1,43 @@ +pub const build_options = @import("build_options"); + +pub const c = @cImport({ + // This is set during the build so it also has to be set + // during import time to get the right types. Without this + // you get stack size mismatches on some structs. + @cDefine("IMGUI_USE_WCHAR32", "1"); + @cInclude("dcimgui.h"); +}); + +// OpenGL3 backend +pub extern fn ImGui_ImplOpenGL3_Init(glsl_version: ?[*:0]const u8) callconv(.c) bool; +pub extern fn ImGui_ImplOpenGL3_Shutdown() callconv(.c) void; +pub extern fn ImGui_ImplOpenGL3_NewFrame() callconv(.c) void; +pub extern fn ImGui_ImplOpenGL3_RenderDrawData(draw_data: *c.ImDrawData) callconv(.c) void; + +// Metal backend +pub extern fn ImGui_ImplMetal_Init(device: *anyopaque) callconv(.c) bool; +pub extern fn ImGui_ImplMetal_Shutdown() callconv(.c) void; +pub extern fn ImGui_ImplMetal_NewFrame(render_pass_descriptor: *anyopaque) callconv(.c) void; +pub extern fn ImGui_ImplMetal_RenderDrawData(draw_data: *c.ImDrawData, command_buffer: *anyopaque, command_encoder: *anyopaque) callconv(.c) void; + +// OSX +pub extern fn ImGui_ImplOSX_Init(*anyopaque) callconv(.c) bool; +pub extern fn ImGui_ImplOSX_Shutdown() callconv(.c) void; +pub extern fn ImGui_ImplOSX_NewFrame(*anyopaque) callconv(.c) void; + +// Internal API functions from dcimgui_internal.h +// We declare these manually because the internal header contains bitfields +// that Zig's cImport cannot translate. +pub extern fn ImGui_DockBuilderDockWindow(window_name: [*:0]const u8, node_id: c.ImGuiID) callconv(.c) void; +pub extern fn ImGui_DockBuilderSplitNode(node_id: c.ImGuiID, split_dir: c.ImGuiDir, size_ratio_for_node_at_dir: f32, out_id_at_dir: *c.ImGuiID, out_id_at_opposite_dir: *c.ImGuiID) callconv(.c) c.ImGuiID; +pub extern fn ImGui_DockBuilderFinish(node_id: c.ImGuiID) callconv(.c) void; + +// Extension functions from ext.cpp +pub const ext = struct { + pub extern fn ImFontConfig_ImFontConfig(self: *c.ImFontConfig) callconv(.c) void; + pub extern fn ImGuiStyle_ImGuiStyle(self: *c.ImGuiStyle) callconv(.c) void; +}; + +test { + _ = c; +} diff --git a/pkg/freetype/build.zig b/pkg/freetype/build.zig index a25dc18da..ecb22cb6c 100644 --- a/pkg/freetype/build.zig +++ b/pkg/freetype/build.zig @@ -90,6 +90,10 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu "-fno-sanitize=undefined", }); + if (target.result.os.tag == .freebsd or target.result.abi == .musl) { + try flags.append(b.allocator, "-fPIC"); + } + const dynamic_link_opts = options.dynamic_link_opts; // Zlib diff --git a/pkg/glslang/build.zig b/pkg/glslang/build.zig index 746a41497..c41e05217 100644 --- a/pkg/glslang/build.zig +++ b/pkg/glslang/build.zig @@ -66,6 +66,10 @@ fn buildGlslang( "-fno-sanitize-trap=undefined", }); + if (target.result.os.tag == .freebsd or target.result.abi == .musl) { + try flags.append(b.allocator, "-fPIC"); + } + if (upstream_) |upstream| { lib.addCSourceFiles(.{ .root = upstream.path(""), diff --git a/pkg/harfbuzz/buffer.zig b/pkg/harfbuzz/buffer.zig index 035120f1a..b97c1bef4 100644 --- a/pkg/harfbuzz/buffer.zig +++ b/pkg/harfbuzz/buffer.zig @@ -238,6 +238,12 @@ pub const Buffer = struct { pub fn guessSegmentProperties(self: Buffer) void { c.hb_buffer_guess_segment_properties(self.handle); } + + /// Sets the cluster level of a buffer. The `ClusterLevel` dictates one + /// aspect of how HarfBuzz will treat non-base characters during shaping. + pub fn setClusterLevel(self: Buffer, level: ClusterLevel) void { + c.hb_buffer_set_cluster_level(self.handle, @intFromEnum(level)); + } }; /// The type of hb_buffer_t contents. @@ -252,6 +258,40 @@ pub const ContentType = enum(u2) { glyphs = c.HB_BUFFER_CONTENT_TYPE_GLYPHS, }; +/// Data type for holding HarfBuzz's clustering behavior options. The cluster +/// level dictates one aspect of how HarfBuzz will treat non-base characters +/// during shaping. +pub const ClusterLevel = enum(u2) { + /// In `monotone_graphemes`, non-base characters are merged into the + /// cluster of the base character that precedes them. There is also cluster + /// merging every time the clusters will otherwise become non-monotone. + /// This is the default cluster level. + monotone_graphemes = c.HB_BUFFER_CLUSTER_LEVEL_MONOTONE_GRAPHEMES, + + /// In `monotone_characters`, non-base characters are initially assigned + /// their own cluster values, which are not merged into preceding base + /// clusters. This allows HarfBuzz to perform additional operations like + /// reorder sequences of adjacent marks. The output is still monotone, but + /// the cluster values are more granular. + monotone_characters = c.HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS, + + /// In `characters`, non-base characters are assigned their own cluster + /// values, which are not merged into preceding base clusters. Moreover, + /// the cluster values are not merged into monotone order. This is the most + /// granular cluster level, and it is useful for clients that need to know + /// the exact cluster values of each character, but is harder to use for + /// clients, since clusters might appear in any order. + characters = c.HB_BUFFER_CLUSTER_LEVEL_CHARACTERS, + + /// In `graphemes`, non-base characters are merged into the cluster of the + /// base character that precedes them. This is similar to the Unicode + /// Grapheme Cluster algorithm, but it is not exactly the same. The output + /// is not forced to be monotone. This is useful for clients that want to + /// use HarfBuzz as a cheap implementation of the Unicode Grapheme Cluster + /// algorithm. + graphemes = c.HB_BUFFER_CLUSTER_LEVEL_GRAPHEMES, +}; + /// The hb_glyph_info_t is the structure that holds information about the /// glyphs and their relation to input text. pub const GlyphInfo = extern struct { diff --git a/pkg/harfbuzz/main.zig b/pkg/harfbuzz/main.zig index d0e8ac2f3..08a4f9c2a 100644 --- a/pkg/harfbuzz/main.zig +++ b/pkg/harfbuzz/main.zig @@ -13,6 +13,7 @@ pub const coretext = @import("coretext.zig"); pub const MemoryMode = blob.MemoryMode; pub const Blob = blob.Blob; pub const Buffer = buffer.Buffer; +pub const GlyphPosition = buffer.GlyphPosition; pub const Direction = common.Direction; pub const Script = common.Script; pub const Language = common.Language; diff --git a/pkg/highway/build.zig b/pkg/highway/build.zig index fd93675e6..3715baf4a 100644 --- a/pkg/highway/build.zig +++ b/pkg/highway/build.zig @@ -72,6 +72,11 @@ pub fn build(b: *std.Build) !void { "-fno-sanitize=undefined", "-fno-sanitize-trap=undefined", }); + + if (target.result.os.tag == .freebsd or target.result.abi == .musl) { + try flags.append(b.allocator, "-fPIC"); + } + if (target.result.os.tag != .windows) { try flags.appendSlice(b.allocator, &.{ "-fmath-errno", diff --git a/pkg/simdutf/build.zig b/pkg/simdutf/build.zig index 0d827c1cc..3123cab21 100644 --- a/pkg/simdutf/build.zig +++ b/pkg/simdutf/build.zig @@ -32,6 +32,10 @@ pub fn build(b: *std.Build) !void { "-fno-sanitize-trap=undefined", }); + if (target.result.os.tag == .freebsd or target.result.abi == .musl) { + try flags.append(b.allocator, "-fPIC"); + } + lib.addCSourceFiles(.{ .flags = flags.items, .files = &.{ diff --git a/pkg/spirv-cross/build.zig b/pkg/spirv-cross/build.zig index 003ec43cf..f85e74adf 100644 --- a/pkg/spirv-cross/build.zig +++ b/pkg/spirv-cross/build.zig @@ -74,6 +74,10 @@ fn buildSpirvCross( "-fno-sanitize-trap=undefined", }); + if (target.result.os.tag == .freebsd or target.result.abi == .musl) { + try flags.append(b.allocator, "-fPIC"); + } + if (b.lazyDependency("spirv_cross", .{})) |upstream| { lib.addIncludePath(upstream.path("")); module.addIncludePath(upstream.path("")); diff --git a/po/README_CONTRIBUTORS.md b/po/README_CONTRIBUTORS.md index 2c405acf3..e232c0620 100644 --- a/po/README_CONTRIBUTORS.md +++ b/po/README_CONTRIBUTORS.md @@ -9,7 +9,7 @@ for any localization that they may add. ## GTK -In the GTK app runtime, translable strings are mainly sourced from Blueprint +In the GTK app runtime, translatable strings are mainly sourced from Blueprint files (located under `src/apprt/gtk/ui`). Blueprints have a native syntax for translatable strings, which look like this: diff --git a/po/README_TRANSLATORS.md b/po/README_TRANSLATORS.md index 582d5037c..25b7cab5b 100644 --- a/po/README_TRANSLATORS.md +++ b/po/README_TRANSLATORS.md @@ -44,9 +44,9 @@ intended to be regenerated by code contributors. If there is a problem with the template file, please reach out to a code contributor. Instead, only edit the translation file corresponding to your language/locale, -identified via the its _locale name_: for example, `de_DE.UTF-8.po` would be -the translation file for German (language code `de`) as spoken in Germany -(country code `DE`). The GNU `gettext` manual contains +identified via its _locale name_: for example, `de_DE.UTF-8.po` would be the +translation file for German (language code `de`) as spoken in Germany (country +code `DE`). The GNU `gettext` manual contains [further information about locale names](https://www.gnu.org/software/gettext/manual/gettext.html#Locale-Names-1), including a list of language and country codes. diff --git a/po/de_DE.UTF-8.po b/po/de_DE.UTF-8.po index c7fc6643f..f73f1c251 100644 --- a/po/de_DE.UTF-8.po +++ b/po/de_DE.UTF-8.po @@ -3,14 +3,15 @@ # Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Robin Pfäffle , 2025. +# Jan Klass , 2026. # msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-08-25 19:38+0100\n" -"Last-Translator: Robin \n" +"PO-Revision-Date: 2026-01-06 10:25+0100\n" +"Last-Translator: Jan Klass \n" "Language-Team: German \n" "Language: de\n" "MIME-Version: 1.0\n" @@ -320,4 +321,4 @@ msgstr "Ghostty-Entwickler" #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" -msgstr "" +msgstr "Ghostty: Terminalinspektor" diff --git a/snap/local/launcher b/snap/local/launcher index 89e0d1709..71b92f5bb 100755 --- a/snap/local/launcher +++ b/snap/local/launcher @@ -14,7 +14,14 @@ if [ -z "$XDG_DATA_HOME" ]; then export XDG_DATA_HOME="$SNAP_REAL_HOME/.local/share" fi -source "$SNAP_USER_DATA/.last_revision" 2>/dev/null || true +if [ -f "$SNAP_USER_DATA/.last_revision" ]; then + if ! source "$SNAP_USER_DATA/.last_revision" 2>/dev/null; then + # file exist but sourcing it fails, so it's likely + # not good anyway + rm -f "$SNAP_USER_DATA/.last_revision" + fi +fi + if [ "$LAST_REVISION" = "$SNAP_REVISION" ]; then needs_update=false else diff --git a/src/App.zig b/src/App.zig index 99d03399c..00be56f49 100644 --- a/src/App.zig +++ b/src/App.zig @@ -357,15 +357,17 @@ pub fn keyEvent( // Get the keybind entry for this event. We don't support key sequences // so we can look directly in the top-level set. const entry = rt_app.config.keybind.set.getEvent(event) orelse return false; - const leaf: input.Binding.Set.Leaf = switch (entry.value_ptr.*) { + const leaf: input.Binding.Set.GenericLeaf = switch (entry.value_ptr.*) { // Sequences aren't supported. Our configuration parser verifies // this for global keybinds but we may still get an entry for // a non-global keybind. .leader => return false, // Leaf entries are good - .leaf => |leaf| leaf, + inline .leaf, .leaf_chained => |leaf| leaf.generic(), }; + const actions: []const input.Binding.Action = leaf.actionsSlice(); + assert(actions.len > 0); // If we aren't focused, then we only process global keybinds. if (!self.focused and !leaf.flags.global) return false; @@ -373,13 +375,7 @@ pub fn keyEvent( // Global keybinds are done using performAll so that they // can target all surfaces too. if (leaf.flags.global) { - self.performAllAction(rt_app, leaf.action) catch |err| { - log.warn("error performing global keybind action action={s} err={}", .{ - @tagName(leaf.action), - err, - }); - }; - + self.performAllChainedAction(rt_app, actions); return true; } @@ -389,14 +385,20 @@ pub fn keyEvent( // If we are focused, then we process keybinds only if they are // app-scoped. Otherwise, we do nothing. Surface-scoped should - // be processed by Surface.keyEvent. - const app_action = leaf.action.scoped(.app) orelse return false; - self.performAction(rt_app, app_action) catch |err| { - log.warn("error performing app keybind action action={s} err={}", .{ - @tagName(app_action), - err, - }); - }; + // be processed by Surface.keyEvent. For chained actions, all + // actions must be app-scoped. + for (actions) |action| if (action.scoped(.app) == null) return false; + for (actions) |action| { + self.performAction( + rt_app, + action.scoped(.app).?, + ) catch |err| { + log.warn("error performing app keybind action action={s} err={}", .{ + @tagName(action), + err, + }); + }; + } return true; } @@ -454,6 +456,23 @@ pub fn performAction( } } +/// Performs a chained action. We will continue executing each action +/// even if there is a failure in a prior action. +pub fn performAllChainedAction( + self: *App, + rt_app: *apprt.App, + actions: []const input.Binding.Action, +) void { + for (actions) |action| { + self.performAllAction(rt_app, action) catch |err| { + log.warn("error performing chained action action={s} err={}", .{ + @tagName(action), + err, + }); + }; + } +} + /// Perform an app-wide binding action. If the action is surface-specific /// then it will be performed on all surfaces. To perform only app-scoped /// actions, use performAction. diff --git a/src/Surface.zig b/src/Surface.zig index 653178bdc..0bf3aa008 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -49,6 +49,10 @@ const Renderer = rendererpkg.Renderer; const min_window_width_cells: u32 = 10; const min_window_height_cells: u32 = 4; +/// The maximum number of key tables that can be active at any +/// given time. `activate_key_table` calls after this are ignored. +const max_active_key_tables = 8; + /// Allocator alloc: Allocator, @@ -145,6 +149,12 @@ focused: bool = true, /// Used to determine whether to continuously scroll. selection_scroll_active: bool = false, +/// True if the surface is in read-only mode. When read-only, no input +/// is sent to the PTY but terminal-level operations like selections, +/// (native) scrolling, and copy keybinds still work. Warn before quit is +/// always enabled in this state. +readonly: bool = false, + /// Used to send notifications that long running commands have finished. /// Requires that shell integration be active. Should represent a nanosecond /// precision timestamp. It does not necessarily need to correspond to the @@ -247,18 +257,9 @@ const Mouse = struct { /// Keyboard state for the surface. pub const Keyboard = struct { - /// The currently active keybindings for the surface. This is used to - /// implement sequences: as leader keys are pressed, the active bindings - /// set is updated to reflect the current leader key sequence. If this is - /// null then the root bindings are used. - bindings: ?*const input.Binding.Set = null, - - /// The last handled binding. This is used to prevent encoding release - /// events for handled bindings. We only need to keep track of one because - /// at least at the time of writing this, its impossible for two keys of - /// a combination to be handled by different bindings before the release - /// of the prior (namely since you can't bind modifier-only). - last_trigger: ?u64 = null, + /// The currently active key sequence for the surface. If this is null + /// then we're not currently in a key sequence. + sequence_set: ?*const input.Binding.Set = null, /// The queued keys when we're in the middle of a sequenced binding. /// These are flushed when the sequence is completed and unconsumed or @@ -266,7 +267,23 @@ pub const Keyboard = struct { /// /// This is naturally bounded due to the configuration maximum /// length of a sequence. - queued: std.ArrayListUnmanaged(termio.Message.WriteReq) = .{}, + sequence_queued: std.ArrayListUnmanaged(termio.Message.WriteReq) = .empty, + + /// The stack of tables that is currently active. The first value + /// in this is the first activated table (NOT the default keybinding set). + /// + /// This is bounded by `max_active_key_tables`. + table_stack: std.ArrayListUnmanaged(struct { + set: *const input.Binding.Set, + once: bool, + }) = .empty, + + /// The last handled binding. This is used to prevent encoding release + /// events for handled bindings. We only need to keep track of one because + /// at least at the time of writing this, its impossible for two keys of + /// a combination to be handled by different bindings before the release + /// of the prior (namely since you can't bind modifier-only). + last_trigger: ?u64 = null, }; /// The configuration that a surface has, this is copied from the main @@ -299,6 +316,7 @@ const DerivedConfig = struct { macos_option_as_alt: ?input.OptionAsAlt, selection_clear_on_copy: bool, selection_clear_on_typing: bool, + selection_word_chars: []const u21, vt_kam_allowed: bool, wait_after_command: bool, window_padding_top: u32, @@ -310,12 +328,13 @@ const DerivedConfig = struct { window_width: u32, title: ?[:0]const u8, title_report: bool, - links: []Link, + links: []DerivedConfig.Link, link_previews: configpkg.LinkPreviews, scroll_to_bottom: configpkg.Config.ScrollToBottom, notify_on_command_finish: configpkg.Config.NotifyOnCommandFinish, notify_on_command_finish_action: configpkg.Config.NotifyOnCommandFinishAction, notify_on_command_finish_after: Duration, + key_remaps: input.KeyRemapSet, const Link = struct { regex: oni.Regex, @@ -330,7 +349,7 @@ const DerivedConfig = struct { // Build all of our links const links = links: { - var links: std.ArrayList(Link) = .empty; + var links: std.ArrayList(DerivedConfig.Link) = .empty; defer links.deinit(alloc); for (config.link.links.items) |link| { var regex = try link.oniRegex(); @@ -374,6 +393,7 @@ const DerivedConfig = struct { .macos_option_as_alt = config.@"macos-option-as-alt", .selection_clear_on_copy = config.@"selection-clear-on-copy", .selection_clear_on_typing = config.@"selection-clear-on-typing", + .selection_word_chars = try alloc.dupe(u21, config.@"selection-word-chars".codepoints), .vt_kam_allowed = config.@"vt-kam-allowed", .wait_after_command = config.@"wait-after-command", .window_padding_top = config.@"window-padding-y".top_left, @@ -391,6 +411,7 @@ const DerivedConfig = struct { .notify_on_command_finish = config.@"notify-on-command-finish", .notify_on_command_finish_action = config.@"notify-on-command-finish-action", .notify_on_command_finish_after = config.@"notify-on-command-finish-after", + .key_remaps = try config.@"key-remap".clone(alloc), // Assignments happen sequentially so we have to do this last // so that the memory is captured from allocs above. @@ -601,6 +622,9 @@ pub fn init( }; errdefer env.deinit(); + // don't leak GHOSTTY_LOG to any subprocesses + env.remove("GHOSTTY_LOG"); + // Initialize our IO backend var io_exec = try termio.Exec.init(alloc, .{ .command = command, @@ -784,8 +808,9 @@ pub fn deinit(self: *Surface) void { } // Clean up our keyboard state - for (self.keyboard.queued.items) |req| req.deinit(); - self.keyboard.queued.deinit(self.alloc); + for (self.keyboard.sequence_queued.items) |req| req.deinit(); + self.keyboard.sequence_queued.deinit(self.alloc); + self.keyboard.table_stack.deinit(self.alloc); // Clean up our font grid self.app.font_grid_set.deref(self.font_grid_key); @@ -812,6 +837,30 @@ inline fn surfaceMailbox(self: *Surface) Mailbox { }; } +/// Queue a message for the IO thread. +/// +/// We centralize all our logic into this spot so we can intercept +/// messages for example in readonly mode. +fn queueIo( + self: *Surface, + msg: termio.Message, + mutex: termio.Termio.MutexState, +) void { + // In readonly mode, we don't allow any writes through to the pty. + if (self.readonly) { + switch (msg) { + .write_small, + .write_stable, + .write_alloc, + => return, + + else => {}, + } + } + + self.io.queueMessage(msg, mutex); +} + /// Forces the surface to render. This is useful for when the surface /// is in the middle of animation (such as a resize, etc.) or when /// the render timer is managed manually by the apprt. @@ -843,7 +892,7 @@ pub fn activateInspector(self: *Surface) !void { // Notify our components we have an inspector active _ = self.renderer_thread.mailbox.push(.{ .inspector = true }, .{ .forever = {} }); - self.io.queueMessage(.{ .inspector = true }, .unlocked); + self.queueIo(.{ .inspector = true }, .unlocked); } /// Deactivate the inspector and stop collecting any information. @@ -860,7 +909,7 @@ pub fn deactivateInspector(self: *Surface) void { // Notify our components we have deactivated inspector _ = self.renderer_thread.mailbox.push(.{ .inspector = false }, .{ .forever = {} }); - self.io.queueMessage(.{ .inspector = false }, .unlocked); + self.queueIo(.{ .inspector = false }, .unlocked); // Deinit the inspector insp.deinit(); @@ -871,6 +920,9 @@ pub fn deactivateInspector(self: *Surface) void { /// True if the surface requires confirmation to quit. This should be called /// by apprt to determine if the surface should confirm before quitting. pub fn needsConfirmQuit(self: *Surface) bool { + // If the surface is in read-only mode, always require confirmation + if (self.readonly) return true; + // If the child has exited, then our process is certainly not alive. // We check this first to avoid the locking overhead below. if (self.child_exited) return false; @@ -929,7 +981,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { // We always use an allocating message because we don't know // the length of the title and this isn't a performance critical // path. - self.io.queueMessage(.{ + self.queueIo(.{ .write_alloc = .{ .alloc = self.alloc, .data = data, @@ -978,7 +1030,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { return; } - try self.startClipboardRequest(.standard, .{ .osc_52_read = clipboard }); + _ = try self.startClipboardRequest(.standard, .{ .osc_52_read = clipboard }); }, .clipboard_write => |w| switch (w.req) { @@ -1023,8 +1075,6 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { .scrollbar => |scrollbar| self.updateScrollbar(scrollbar), - .report_color_scheme => |force| self.reportColorScheme(force), - .present_surface => try self.presentSurface(), .password_input => |v| try self.passwordInput(v), @@ -1121,7 +1171,7 @@ fn selectionScrollTick(self: *Surface) !void { // If our screen changed while this is happening, we stop our // selection scroll. if (self.mouse.left_click_screen != t.screens.active_key) { - self.io.queueMessage( + self.queueIo( .{ .selection_scroll = false }, .locked, ); @@ -1174,7 +1224,7 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void { break :gui false; }) return; - // If a native GUI notification was not showm. update our terminal to + // If a native GUI notification was not shown, update our terminal to // note the abnormal exit. self.childExitedAbnormally(info) catch |err| { log.err("error handling abnormal child exit err={}", .{err}); @@ -1184,7 +1234,7 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void { return; } - // We output a message so that the user knows whats going on and + // We output a message so that the user knows what's going on and // doesn't think their terminal just froze. We show this unconditionally // on close even if `wait_after_command` is false and the surface closes // immediately because if a user does an `undo` to restore a closed @@ -1336,26 +1386,6 @@ fn passwordInput(self: *Surface, v: bool) !void { try self.queueRender(); } -/// Sends a DSR response for the current color scheme to the pty. If -/// force is false then we only send the response if the terminal mode -/// 2031 is enabled. -fn reportColorScheme(self: *Surface, force: bool) void { - if (!force) { - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); - if (!self.renderer_state.terminal.modes.get(.report_color_scheme)) { - return; - } - } - - const output = switch (self.config_conditional_state.theme) { - .light => "\x1B[?997;2n", - .dark => "\x1B[?997;1n", - }; - - self.io.queueMessage(.{ .write_stable = output }, .unlocked); -} - fn searchCallback(event: terminal.search.Thread.Event, ud: ?*anyopaque) void { // IMPORTANT: This function is run on the SEARCH THREAD! It is NOT SAFE // to access anything other than values that never change on the surface. @@ -1551,10 +1581,10 @@ fn mouseRefreshLinks( } const link = (try self.linkAtPos(pos)) orelse break :link .{ null, false }; - switch (link[0]) { + switch (link.action) { .open => { const str = try self.io.terminal.screens.active.selectionString(alloc, .{ - .sel = link[1], + .sel = link.selection, .trim = false, }); break :link .{ @@ -1565,7 +1595,7 @@ fn mouseRefreshLinks( ._open_osc8 => { // Show the URL in the status bar - const pin = link[1].start(); + const pin = link.selection.start(); const uri = self.osc8URI(pin) orelse { log.warn("failed to get URI for OSC8 hyperlink", .{}); break :link .{ null, false }; @@ -1695,6 +1725,14 @@ pub fn updateConfig( // If we are in the middle of a key sequence, clear it. self.endKeySequence(.drop, .free); + // Deactivate all key tables since they may have changed. Importantly, + // we store pointers into the config as part of our table stack so + // we can't keep them active across config changes. But this behavior + // also matches key sequences. + _ = self.deactivateAllKeyTables() catch |err| { + log.warn("failed to deactivate key tables err={}", .{err}); + }; + // Before sending any other config changes, we give the renderer a new font // grid. We could check to see if there was an actual change to the font, // but this is easier and pretty rare so it's not a performance concern. @@ -1726,7 +1764,7 @@ pub fn updateConfig( errdefer termio_config_ptr.deinit(); _ = self.renderer_thread.mailbox.push(renderer_message, .{ .forever = {} }); - self.io.queueMessage(.{ + self.queueIo(.{ .change_config = .{ .alloc = self.alloc, .ptr = termio_config_ptr, @@ -2001,6 +2039,29 @@ pub fn pwd( return try alloc.dupe(u8, terminal_pwd); } +/// Resolves a relative file path to an absolute path using the terminal's pwd. +fn resolvePathForOpening( + self: *Surface, + path: []const u8, +) Allocator.Error!?[]const u8 { + if (!std.fs.path.isAbsolute(path)) { + const terminal_pwd = self.io.terminal.getPwd() orelse { + return null; + }; + + const resolved = try std.fs.path.resolve(self.alloc, &.{ terminal_pwd, path }); + + std.fs.accessAbsolute(resolved, .{}) catch { + self.alloc.free(resolved); + return null; + }; + + return resolved; + } + + return null; +} + /// Returns the x/y coordinate of where the IME (Input Method Editor) /// keyboard should be rendered. pub fn imePoint(self: *const Surface) apprt.IMEPos { @@ -2292,7 +2353,7 @@ fn setCellSize(self: *Surface, size: rendererpkg.CellSize) !void { self.balancePaddingIfNeeded(); // Notify the terminal - self.io.queueMessage(.{ .resize = self.size }, .unlocked); + self.queueIo(.{ .resize = self.size }, .unlocked); // Update our terminal default size if necessary. self.recomputeInitialSize() catch |err| { @@ -2395,7 +2456,7 @@ fn resize(self: *Surface, size: rendererpkg.ScreenSize) !void { } // Mail the IO thread - self.io.queueMessage(.{ .resize = self.size }, .unlocked); + self.queueIo(.{ .resize = self.size }, .unlocked); } /// Recalculate the balanced padding if needed. @@ -2497,30 +2558,60 @@ pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void { /// then Ghosty will act as though the binding does not exist. pub fn keyEventIsBinding( self: *Surface, - event: input.KeyEvent, -) bool { + event_orig: input.KeyEvent, +) ?input.Binding.Flags { + // Apply key remappings for consistency with keyCallback + var event = event_orig; + if (self.config.key_remaps.isRemapped(event_orig.mods)) { + event.mods = self.config.key_remaps.apply(event_orig.mods); + } + switch (event.action) { - .release => return false, + .release => return null, .press, .repeat => {}, } - // Our keybinding set is either our current nested set (for - // sequences) or the root set. - const set = self.keyboard.bindings orelse &self.config.keybind.set; + // Look up our entry + const entry: input.Binding.Set.Entry = entry: { + // If we're in a sequence, check the sequence set + if (self.keyboard.sequence_set) |set| { + break :entry set.getEvent(event) orelse return null; + } - // log.warn("text keyEventIsBinding event={} match={}", .{ event, set.getEvent(event) != null }); + // Check active key tables (inner-most to outer-most) + const table_items = self.keyboard.table_stack.items; + for (0..table_items.len) |i| { + const rev_i: usize = table_items.len - 1 - i; + if (table_items[rev_i].set.getEvent(event)) |entry| { + break :entry entry; + } + } - // If we have a keybinding for this event then we return true. - return set.getEvent(event) != null; + // Check the root set + break :entry self.config.keybind.set.getEvent(event) orelse return null; + }; + + // Return flags based on the + return switch (entry.value_ptr.*) { + .leader => .{}, + inline .leaf, .leaf_chained => |v| v.flags, + }; } /// Called for any key events. This handles keybindings, encoding and /// sending to the terminal, etc. pub fn keyCallback( self: *Surface, - event: input.KeyEvent, + event_orig: input.KeyEvent, ) !InputEffect { - // log.warn("text keyCallback event={}", .{event}); + // log.warn("text keyCallback event={}", .{event_orig}); + + // Apply key remappings to transform modifiers before any processing. + // This allows users to remap modifier keys at the app level. + var event = event_orig; + if (self.config.key_remaps.isRemapped(event_orig.mods)) { + event.mods = self.config.key_remaps.apply(event_orig.mods); + } // Crash metadata in case we crash in here crash.sentry.thread_state = self.crashThreadState(); @@ -2558,7 +2649,6 @@ pub fn keyCallback( event, if (insp_ev) |*ev| ev else null, )) |v| return v; - // If we allow KAM and KAM is enabled then we do nothing. if (self.config.vt_kam_allowed) { self.renderer_state.mutex.lock(); @@ -2671,7 +2761,7 @@ pub fn keyCallback( } errdefer write_req.deinit(); - self.io.queueMessage(switch (write_req) { + self.queueIo(switch (write_req) { .small => |v| .{ .write_small = v }, .stable => |v| .{ .write_stable = v }, .alloc => |v| .{ .write_alloc = v }, @@ -2732,38 +2822,70 @@ fn maybeHandleBinding( // Find an entry in the keybind set that matches our event. const entry: input.Binding.Set.Entry = entry: { - const set = self.keyboard.bindings orelse &self.config.keybind.set; + // Handle key sequences first. + if (self.keyboard.sequence_set) |set| { + // Get our entry from the set for the given event. + if (set.getEvent(event)) |v| break :entry v; - // Get our entry from the set for the given event. - if (set.getEvent(event)) |v| break :entry v; + // No entry found. We need to encode everything up to this + // point and send to the pty since we're in a sequence. + + // We ignore modifiers so that nested sequences such as + // ctrl+a>ctrl+b>c work. + if (event.key.modifier()) return null; + + // If we have a catch-all of ignore, then we special case our + // invalid sequence handling to ignore it. + if (self.catchAllIsIgnore()) { + self.endKeySequence(.drop, .retain); + return .ignored; + } - // No entry found. If we're not looking at the root set of the - // bindings we need to encode everything up to this point and - // send to the pty. - // - // We also ignore modifiers so that nested sequences such as - // ctrl+a>ctrl+b>c work. - if (self.keyboard.bindings != null and - !event.key.modifier()) - { // Encode everything up to this point self.endKeySequence(.flush, .retain); + + return null; } - return null; + // No currently active sequence, move on to tables. For tables, + // we search inner-most table to outer-most. The table stack does + // NOT include the root set. + const table_items = self.keyboard.table_stack.items; + if (table_items.len > 0) { + for (0..table_items.len) |i| { + const rev_i: usize = table_items.len - 1 - i; + const table = table_items[rev_i]; + if (table.set.getEvent(event)) |v| { + // If this is a one-shot activation AND its the currently + // active table, then we deactivate it after this. + // Note: we may want to change the semantics here to + // remove this table no matter where it is in the stack, + // maybe. + if (table.once and i == 0) _ = try self.performBindingAction( + .deactivate_key_table, + ); + + break :entry v; + } + } + } + + // No table, use our default set + break :entry self.config.keybind.set.getEvent(event) orelse + return null; }; // Determine if this entry has an action or if its a leader key. - const leaf: input.Binding.Set.Leaf = switch (entry.value_ptr.*) { + const leaf: input.Binding.Set.GenericLeaf = switch (entry.value_ptr.*) { .leader => |set| { // Setup the next set we'll look at. - self.keyboard.bindings = set; + self.keyboard.sequence_set = set; // Store this event so that we can drain and encode on invalid. // We don't need to cap this because it is naturally capped by // the config validation. if (try self.encodeKey(event, insp_ev)) |req| { - try self.keyboard.queued.append(self.alloc, req); + try self.keyboard.sequence_queued.append(self.alloc, req); } // Start or continue our key sequence @@ -2781,9 +2903,8 @@ fn maybeHandleBinding( return .consumed; }, - .leaf => |leaf| leaf, + inline .leaf, .leaf_chained => |leaf| leaf.generic(), }; - const action = leaf.action; // consumed determines if the input is consumed or if we continue // encoding the key (if we have a key to encode). @@ -2802,39 +2923,61 @@ fn maybeHandleBinding( // perform an action (below) self.keyboard.last_trigger = null; - // An action also always resets the binding set. - self.keyboard.bindings = null; + // An action also always resets the sequence set. + self.keyboard.sequence_set = null; + + // Setup our actions + const actions = leaf.actionsSlice(); // Attempt to perform the action - log.debug("key event binding flags={} action={f}", .{ + log.debug("key event binding flags={} action={any}", .{ leaf.flags, - action, + actions, }); const performed = performed: { // If this is a global or all action, then we perform it on // the app and it applies to every surface. if (leaf.flags.global or leaf.flags.all) { - try self.app.performAllAction(self.rt_app, action); + self.app.performAllChainedAction( + self.rt_app, + actions, + ); // "All" actions are always performed since they are global. break :performed true; } - break :performed try self.performBindingAction(action); + // Perform each action. We are performed if ANY of the chained + // actions perform. + var performed: bool = false; + for (actions) |action| { + if (self.performBindingAction(action)) |v| { + performed = performed or v; + } else |err| { + log.info( + "key binding action failed action={t} err={}", + .{ action, err }, + ); + } + } + + break :performed performed; }; if (performed) { // If we performed an action and it was a closing action, // our "self" pointer is not safe to use anymore so we need to // just exit immediately. - if (closingAction(action)) { + for (actions) |action| if (closingAction(action)) { log.debug("key binding is a closing binding, halting key event processing", .{}); return .closed; - } + }; // If our action was "ignore" then we return the special input // effect of "ignored". - if (action == .ignore) return .ignored; + for (actions) |action| if (action == .ignore) { + return .ignored; + }; } // If we have the performable flag and the action was not performed, @@ -2858,7 +3001,18 @@ fn maybeHandleBinding( // Store our last trigger so we don't encode the release event self.keyboard.last_trigger = event.bindingHash(); - if (insp_ev) |ev| ev.binding = action; + if (insp_ev) |ev| { + ev.binding = self.alloc.dupe( + input.Binding.Action, + actions, + ) catch |err| binding: { + log.warn( + "error allocating binding action for inspector err={}", + .{err}, + ); + break :binding &.{}; + }; + } return .consumed; } @@ -2869,6 +3023,58 @@ fn maybeHandleBinding( return null; } +fn deactivateAllKeyTables(self: *Surface) !bool { + switch (self.keyboard.table_stack.items.len) { + // No key table active. This does nothing. + 0 => return false, + + // Clear the entire table stack. + else => self.keyboard.table_stack.clearAndFree(self.alloc), + } + + // Notify the UI. + _ = self.rt_app.performAction( + .{ .surface = self }, + .key_table, + .deactivate_all, + ) catch |err| { + log.warn( + "failed to notify app of key table err={}", + .{err}, + ); + }; + + return true; +} + +/// This checks if the current keybinding sets have a catch_all binding +/// with `ignore`. This is used to determine some special input cases. +fn catchAllIsIgnore(self: *Surface) bool { + // Get our catch all + const entry: input.Binding.Set.Entry = entry: { + const trigger: input.Binding.Trigger = .{ .key = .catch_all }; + + const table_items = self.keyboard.table_stack.items; + for (0..table_items.len) |i| { + const rev_i: usize = table_items.len - 1 - i; + const entry = table_items[rev_i].set.get(trigger) orelse continue; + break :entry entry; + } + + break :entry self.config.keybind.set.get(trigger) orelse + return false; + }; + + // We have a catch-all entry, see if its an ignore + return switch (entry.value_ptr.*) { + .leader => false, + .leaf => |leaf| leaf.action == .ignore, + .leaf_chained => |leaf| chained: for (leaf.actions.items) |action| { + if (action == .ignore) break :chained true; + } else false, + }; +} + const KeySequenceQueued = enum { flush, drop }; const KeySequenceMemory = enum { retain, free }; @@ -2893,27 +3099,30 @@ fn endKeySequence( ); }; - // No matter what we clear our current binding set. This restores + // No matter what we clear our current sequence set. This restores // the set we look at to the root set. - self.keyboard.bindings = null; + self.keyboard.sequence_set = null; - if (self.keyboard.queued.items.len > 0) { - switch (action) { - .flush => for (self.keyboard.queued.items) |write_req| { - self.io.queueMessage(switch (write_req) { - .small => |v| .{ .write_small = v }, - .stable => |v| .{ .write_stable = v }, - .alloc => |v| .{ .write_alloc = v }, - }, .unlocked); - }, + // If we have no queued data, there is nothing else to do. + if (self.keyboard.sequence_queued.items.len == 0) return; - .drop => for (self.keyboard.queued.items) |req| req.deinit(), - } + // Run the proper action first + switch (action) { + .flush => for (self.keyboard.sequence_queued.items) |write_req| { + self.queueIo(switch (write_req) { + .small => |v| .{ .write_small = v }, + .stable => |v| .{ .write_stable = v }, + .alloc => |v| .{ .write_alloc = v }, + }, .unlocked); + }, - switch (mem) { - .free => self.keyboard.queued.clearAndFree(self.alloc), - .retain => self.keyboard.queued.clearRetainingCapacity(), - } + .drop => for (self.keyboard.sequence_queued.items) |req| req.deinit(), + } + + // Memory handling of the sequence after the action + switch (mem) { + .free => self.keyboard.sequence_queued.clearAndFree(self.alloc), + .retain => self.keyboard.sequence_queued.clearRetainingCapacity(), } } @@ -3126,7 +3335,7 @@ pub fn focusCallback(self: *Surface, focused: bool) !void { self.renderer_state.mutex.lock(); self.io.terminal.flags.focused = focused; self.renderer_state.mutex.unlock(); - self.io.queueMessage(.{ .focused = focused }, .unlocked); + self.queueIo(.{ .focused = focused }, .unlocked); } } @@ -3290,7 +3499,7 @@ pub fn scrollCallback( }; }; for (0..y.magnitude()) |_| { - self.io.queueMessage(.{ .write_stable = seq }, .locked); + self.queueIo(.{ .write_stable = seq }, .locked); } } @@ -3511,7 +3720,7 @@ fn mouseReport( data[5] = 32 + @as(u8, @intCast(viewport_point.y)) + 1; // Ask our IO thread to write the data - self.io.queueMessage(.{ .write_small = .{ + self.queueIo(.{ .write_small = .{ .data = data, .len = 6, } }, .locked); @@ -3534,7 +3743,7 @@ fn mouseReport( i += try std.unicode.utf8Encode(@intCast(32 + viewport_point.y + 1), data[i..]); // Ask our IO thread to write the data - self.io.queueMessage(.{ .write_small = .{ + self.queueIo(.{ .write_small = .{ .data = data, .len = @intCast(i), } }, .locked); @@ -3555,7 +3764,7 @@ fn mouseReport( }); // Ask our IO thread to write the data - self.io.queueMessage(.{ .write_small = .{ + self.queueIo(.{ .write_small = .{ .data = data, .len = @intCast(resp.len), } }, .locked); @@ -3572,7 +3781,7 @@ fn mouseReport( }); // Ask our IO thread to write the data - self.io.queueMessage(.{ .write_small = .{ + self.queueIo(.{ .write_small = .{ .data = data, .len = @intCast(resp.len), } }, .locked); @@ -3601,7 +3810,7 @@ fn mouseReport( }); // Ask our IO thread to write the data - self.io.queueMessage(.{ .write_small = .{ + self.queueIo(.{ .write_small = .{ .data = data, .len = @intCast(resp.len), } }, .locked); @@ -3753,7 +3962,7 @@ pub fn mouseButtonCallback( // Stop selection scrolling when releasing the left mouse button // but only when selection scrolling is active. if (self.selection_scroll_active) { - self.io.queueMessage( + self.queueIo( .{ .selection_scroll = false }, .unlocked, ); @@ -3935,9 +4144,24 @@ pub fn mouseButtonCallback( } }, - // Double click, select the word under our mouse + // Double click, select the word under our mouse. + // First try to detect if we're clicking on a URL to select the entire URL. 2 => { - const sel_ = self.io.terminal.screens.active.selectWord(pin.*); + const sel_ = sel: { + // Try link detection without requiring modifier keys + if (self.linkAtPin( + pin.*, + null, + )) |result_| { + if (result_) |result| { + break :sel result.selection; + } + } else |_| { + // Ignore any errors, likely regex errors. + } + + break :sel self.io.terminal.screens.active.selectWord(pin.*, self.config.selection_word_chars); + }; if (sel_) |sel| { try self.io.terminal.screens.active.select(sel); try self.queueRender(); @@ -3967,7 +4191,7 @@ pub fn mouseButtonCallback( .selection else .standard; - try self.startClipboardRequest(clipboard, .{ .paste = {} }); + _ = try self.startClipboardRequest(clipboard, .{ .paste = {} }); } // Right-click down selects word for context menus. If the apprt @@ -3981,8 +4205,8 @@ pub fn mouseButtonCallback( // Get our viewport pin const screen: *terminal.Screen = self.renderer_state.terminal.screens.active; + const pos = try self.rt_surface.getCursorPos(); const pin = pin: { - const pos = try self.rt_surface.getCursorPos(); const pt_viewport = self.posToViewport(pos.x, pos.y); const pin = screen.pages.pin(.{ .viewport = .{ @@ -4013,8 +4237,17 @@ pub fn mouseButtonCallback( // word selection where we clicked. } - const sel = screen.selectWord(pin) orelse break :sel; - try self.setSelection(sel); + // If there is a link at this position, we want to + // select the link. Otherwise, select the word. + if (try self.linkAtPos(pos)) |link| { + try self.setSelection(link.selection); + } else { + const sel = screen.selectWord( + pin, + self.config.selection_word_chars, + ) orelse break :sel; + try self.setSelection(sel); + } try self.queueRender(); // Don't consume so that we show the context menu in apprt. @@ -4045,7 +4278,7 @@ pub fn mouseButtonCallback( // request so we need to unlock. self.renderer_state.mutex.unlock(); defer self.renderer_state.mutex.lock(); - try self.startClipboardRequest(.standard, .paste); + _ = try self.startClipboardRequest(.standard, .paste); // We don't need to clear selection because we didn't have // one to begin with. @@ -4060,7 +4293,7 @@ pub fn mouseButtonCallback( // request so we need to unlock. self.renderer_state.mutex.unlock(); defer self.renderer_state.mutex.lock(); - try self.startClipboardRequest(.standard, .paste); + _ = try self.startClipboardRequest(.standard, .paste); }, } @@ -4110,7 +4343,7 @@ fn clickMoveCursor(self: *Surface, to: terminal.Pin) !void { break :arrow if (t.modes.get(.cursor_keys)) "\x1bOB" else "\x1b[B"; }; for (0..@abs(path.y)) |_| { - self.io.queueMessage(.{ .write_stable = arrow }, .locked); + self.queueIo(.{ .write_stable = arrow }, .locked); } } if (path.x != 0) { @@ -4120,21 +4353,23 @@ fn clickMoveCursor(self: *Surface, to: terminal.Pin) !void { break :arrow if (t.modes.get(.cursor_keys)) "\x1bOC" else "\x1b[C"; }; for (0..@abs(path.x)) |_| { - self.io.queueMessage(.{ .write_stable = arrow }, .locked); + self.queueIo(.{ .write_stable = arrow }, .locked); } } } +const Link = struct { + action: input.Link.Action, + selection: terminal.Selection, +}; + /// Returns the link at the given cursor position, if any. /// /// Requires the renderer mutex is held. fn linkAtPos( self: *Surface, pos: apprt.CursorPos, -) !?struct { - input.Link.Action, - terminal.Selection, -} { +) !?Link { // Convert our cursor position to a screen point. const screen: *terminal.Screen = self.renderer_state.terminal.screens.active; const mouse_pin: terminal.Pin = mouse_pin: { @@ -4155,14 +4390,27 @@ fn linkAtPos( const cell = rac.cell; if (!cell.hyperlink) break :hyperlink; const sel = terminal.Selection.init(mouse_pin, mouse_pin, false); - return .{ ._open_osc8, sel }; + return .{ .action = ._open_osc8, .selection = sel }; } - // If we have no OSC8 links then we fallback to regex-based URL detection. - // If we have no configured links we can save a lot of work going forward. + // Fall back to configured links + return try self.linkAtPin(mouse_pin, mouse_mods); +} + +/// Detects if a link is present at the given pin. +/// +/// If mouse mods is null then mouse mod requirements are ignored (all +/// configured links are checked). +/// +/// Requires the renderer state mutex is held. +fn linkAtPin( + self: *Surface, + mouse_pin: terminal.Pin, + mouse_mods: ?input.Mods, +) !?Link { if (self.config.links.len == 0) return null; - // Get the line we're hovering over. + const screen: *terminal.Screen = self.renderer_state.terminal.screens.active; const line = screen.selectLine(.{ .pin = mouse_pin, .whitespace = null, @@ -4177,12 +4425,12 @@ fn linkAtPos( })); defer strmap.deinit(self.alloc); - // Go through each link and see if we clicked it for (self.config.links) |link| { - switch (link.highlight) { + // Skip highlight/mods check when mouse_mods is null (double-click mode) + if (mouse_mods) |mods| switch (link.highlight) { .always, .hover => {}, - .always_mods, .hover_mods => |v| if (!v.equal(mouse_mods)) continue, - } + .always_mods, .hover_mods => |v| if (!v.equal(mods)) continue, + }; var it = strmap.searchIterator(link.regex); while (true) { @@ -4190,7 +4438,10 @@ fn linkAtPos( defer match.deinit(); const sel = match.selection(); if (!sel.contains(screen, mouse_pin)) continue; - return .{ link.action, sel }; + return .{ + .action = link.action, + .selection = sel, + }; } } @@ -4221,19 +4472,24 @@ fn mouseModsWithCapture(self: *Surface, mods: input.Mods) input.Mods { /// /// Requires the renderer state mutex is held. fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool { - const action, const sel = try self.linkAtPos(pos) orelse return false; - switch (action) { + const link = try self.linkAtPos(pos) orelse return false; + switch (link.action) { .open => { const str = try self.io.terminal.screens.active.selectionString(self.alloc, .{ - .sel = sel, + .sel = link.selection, .trim = false, }); defer self.alloc.free(str); - try self.openUrl(.{ .kind = .unknown, .url = str }); + + const resolved_path = try self.resolvePathForOpening(str); + defer if (resolved_path) |p| self.alloc.free(p); + + const url_to_open = resolved_path orelse str; + try self.openUrl(.{ .kind = .unknown, .url = url_to_open }); }, ._open_osc8 => { - const uri = self.osc8URI(sel.start()) orelse { + const uri = self.osc8URI(link.selection.start()) orelse { log.warn("failed to get URI for OSC8 hyperlink", .{}); return false; }; @@ -4310,7 +4566,10 @@ pub fn mousePressureCallback( // This should always be set in this state but we don't want // to handle state inconsistency here. const pin = self.mouse.left_click_pin orelse break :select; - const sel = self.io.terminal.screens.active.selectWord(pin.*) orelse break :select; + const sel = self.io.terminal.screens.active.selectWord( + pin.*, + self.config.selection_word_chars, + ) orelse break :select; try self.io.terminal.screens.active.select(sel); try self.queueRender(); } @@ -4393,7 +4652,7 @@ pub fn cursorPosCallback( // Stop selection scrolling when inside the viewport within a 1px buffer // for fullscreen windows, but only when selection scrolling is active. if (pos.y >= 1 and self.selection_scroll_active) { - self.io.queueMessage( + self.queueIo( .{ .selection_scroll = false }, .locked, ); @@ -4493,7 +4752,7 @@ pub fn cursorPosCallback( if ((pos.y <= 1 or pos.y > max_y - 1) and !self.selection_scroll_active) { - self.io.queueMessage( + self.queueIo( .{ .selection_scroll = true }, .locked, ); @@ -4533,7 +4792,11 @@ fn dragLeftClickDouble( const click_pin = self.mouse.left_click_pin.?.*; // Get the word closest to our starting click. - const word_start = screen.selectWordBetween(click_pin, drag_pin) orelse { + const word_start = screen.selectWordBetween( + click_pin, + drag_pin, + self.config.selection_word_chars, + ) orelse { try self.setSelection(null); return; }; @@ -4542,6 +4805,7 @@ fn dragLeftClickDouble( const word_current = screen.selectWordBetween( drag_pin, click_pin, + self.config.selection_word_chars, ) orelse { try self.setSelection(null); return; @@ -4772,7 +5036,7 @@ pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) !void { self.notifyConfigConditionalState(); // If mode 2031 is on, then we report the change live. - self.reportColorScheme(false); + self.queueIo(.{ .color_scheme_report = .{ .force = false } }, .unlocked); } pub fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Coordinate { @@ -4869,7 +5133,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .esc => try std.fmt.bufPrint(&buf, "\x1b{s}", .{data}), else => unreachable, }; - self.io.queueMessage(try termio.Message.writeReq( + self.queueIo(try termio.Message.writeReq( self.alloc, full_data, ), .unlocked); @@ -4896,7 +5160,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool ); return true; }; - self.io.queueMessage(try termio.Message.writeReq( + self.queueIo(try termio.Message.writeReq( self.alloc, text, ), .unlocked); @@ -4929,9 +5193,9 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool }; if (normal) { - self.io.queueMessage(.{ .write_stable = ck.normal }, .unlocked); + self.queueIo(.{ .write_stable = ck.normal }, .unlocked); } else { - self.io.queueMessage(.{ .write_stable = ck.application }, .unlocked); + self.queueIo(.{ .write_stable = ck.application }, .unlocked); } }, @@ -4952,6 +5216,16 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool ); }, + .search_selection => { + const selection = try self.selectionString(self.alloc) orelse return false; + defer self.alloc.free(selection); + return try self.rt_app.performAction( + .{ .surface = self }, + .start_search, + .{ .needle = selection }, + ); + }, + .end_search => { // We only return that this was performed if we actually // stopped a search, but we also send the apprt end_search so @@ -5067,11 +5341,11 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); if (try self.linkAtPos(pos)) |link_info| { - const url_text = switch (link_info[0]) { + const url_text = switch (link_info.action) { .open => url_text: { // For regex links, get the text from selection break :url_text (self.io.terminal.screens.active.selectionString(self.alloc, .{ - .sel = link_info[1], + .sel = link_info.selection, .trim = self.config.clipboard_trim_trailing_spaces, })) catch |err| { log.err("error reading url string err={}", .{err}); @@ -5081,7 +5355,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool ._open_osc8 => url_text: { // For OSC8 links, get the URI directly from hyperlink data - const uri = self.osc8URI(link_info[1].start()) orelse { + const uri = self.osc8URI(link_info.selection.start()) orelse { log.warn("failed to get URI for OSC8 hyperlink", .{}); return false; }; @@ -5119,12 +5393,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool return true; }, - .paste_from_clipboard => try self.startClipboardRequest( + .paste_from_clipboard => return try self.startClipboardRequest( .standard, .{ .paste = {} }, ), - .paste_from_selection => try self.startClipboardRequest( + .paste_from_selection => return try self.startClipboardRequest( .selection, .{ .paste = {} }, ), @@ -5183,7 +5457,13 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .prompt_surface_title => return try self.rt_app.performAction( .{ .surface = self }, .prompt_title, - {}, + .surface, + ), + + .prompt_tab_title => return try self.rt_app.performAction( + .{ .surface = self }, + .prompt_title, + .tab, ), .clear_screen => { @@ -5198,19 +5478,19 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool if (self.io.terminal.screens.active_key == .alternate) return false; } - self.io.queueMessage(.{ + self.queueIo(.{ .clear_screen = .{ .history = true }, }, .unlocked); }, .scroll_to_top => { - self.io.queueMessage(.{ + self.queueIo(.{ .scroll_viewport = .{ .top = {} }, }, .unlocked); }, .scroll_to_bottom => { - self.io.queueMessage(.{ + self.queueIo(.{ .scroll_viewport = .{ .bottom = {} }, }, .unlocked); }, @@ -5240,14 +5520,14 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .scroll_page_up => { const rows: isize = @intCast(self.size.grid().rows); - self.io.queueMessage(.{ + self.queueIo(.{ .scroll_viewport = .{ .delta = -1 * rows }, }, .unlocked); }, .scroll_page_down => { const rows: isize = @intCast(self.size.grid().rows); - self.io.queueMessage(.{ + self.queueIo(.{ .scroll_viewport = .{ .delta = rows }, }, .unlocked); }, @@ -5255,19 +5535,19 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .scroll_page_fractional => |fraction| { const rows: f32 = @floatFromInt(self.size.grid().rows); const delta: isize = @intFromFloat(@trunc(fraction * rows)); - self.io.queueMessage(.{ + self.queueIo(.{ .scroll_viewport = .{ .delta = delta }, }, .unlocked); }, .scroll_page_lines => |lines| { - self.io.queueMessage(.{ + self.queueIo(.{ .scroll_viewport = .{ .delta = lines }, }, .unlocked); }, .jump_to_prompt => |delta| { - self.io.queueMessage(.{ + self.queueIo(.{ .jump_to_prompt = @intCast(delta), }, .unlocked); }, @@ -5299,6 +5579,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool switch (v) { .this => .this, .other => .other, + .right => .right, }, ), @@ -5350,6 +5631,15 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool }, ), + .goto_window => |direction| return try self.rt_app.performAction( + .{ .surface = self }, + .goto_window, + switch (direction) { + .previous => .previous, + .next => .next, + }, + ), + .resize_split => |value| return try self.rt_app.performAction( .{ .surface = self }, .resize_split, @@ -5376,6 +5666,16 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool {}, ), + .toggle_readonly => { + self.readonly = !self.readonly; + _ = try self.rt_app.performAction( + .{ .surface = self }, + .readonly, + if (self.readonly) .on else .off, + ); + return true; + }, + .reset_window_size => return try self.rt_app.performAction( .{ .surface = self }, .reset_window_size, @@ -5434,6 +5734,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool {}, ), + .toggle_background_opacity => return try self.rt_app.performAction( + .{ .surface = self }, + .toggle_background_opacity, + {}, + ), + .show_on_screen_keyboard => return try self.rt_app.performAction( .{ .surface = self }, .show_on_screen_keyboard, @@ -5470,6 +5776,95 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool {}, ), + inline .activate_key_table, + .activate_key_table_once, + => |name, tag| { + // Look up the table in our config + const set = self.config.keybind.tables.getPtr(name) orelse { + log.debug("key table not found: {s}", .{name}); + return false; + }; + + // If this is the same table as is currently active, then + // do nothing. + if (self.keyboard.table_stack.items.len > 0) { + const items = self.keyboard.table_stack.items; + const active = items[items.len - 1].set; + if (active == set) { + log.debug("ignoring duplicate activate table: {s}", .{name}); + return false; + } + } + + // If we're already at the max, ignore it. + if (self.keyboard.table_stack.items.len >= max_active_key_tables) { + log.info( + "ignoring activate table, max depth reached: {s}", + .{name}, + ); + return false; + } + + // Add the table to the stack. + try self.keyboard.table_stack.append(self.alloc, .{ + .set = set, + .once = tag == .activate_key_table_once, + }); + + // Notify the UI. + _ = self.rt_app.performAction( + .{ .surface = self }, + .key_table, + .{ .activate = name }, + ) catch |err| { + log.warn( + "failed to notify app of key table err={}", + .{err}, + ); + }; + + log.debug("key table activated: {s}", .{name}); + }, + + .deactivate_key_table => { + switch (self.keyboard.table_stack.items.len) { + // No key table active. This does nothing. + 0 => return false, + + // Final key table active, clear our state. + 1 => self.keyboard.table_stack.clearAndFree(self.alloc), + + // Restore the prior key table. We don't free any memory in + // this case because we assume it will be freed later when + // we finish our key table. + else => _ = self.keyboard.table_stack.pop(), + } + + // Notify the UI. + _ = self.rt_app.performAction( + .{ .surface = self }, + .key_table, + .deactivate, + ) catch |err| { + log.warn( + "failed to notify app of key table err={}", + .{err}, + ); + }; + }, + + .deactivate_all_key_tables => { + return try self.deactivateAllKeyTables(); + }, + + .end_key_sequence => { + // End the key sequence and flush queued keys to the terminal, + // but don't encode the key that triggered this action. This + // will do that because leaf keys (keys with bindings) aren't + // in the queued encoding list. + self.endKeySequence(.flush, .retain); + }, + .crash => |location| switch (location) { .main => @panic("crash binding action, crashing intentionally"), @@ -5481,7 +5876,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool }; }, - .io => self.io.queueMessage(.{ .crash = {} }, .unlocked), + .io => self.queueIo(.{ .crash = {} }, .unlocked), }, .adjust_selection => |direction| { @@ -5679,7 +6074,7 @@ fn writeScreenFile( }, .url = path, }), - .paste => self.io.queueMessage(try termio.Message.writeReq( + .paste => self.queueIo(try termio.Message.writeReq( self.alloc, path, ), .unlocked), @@ -5725,11 +6120,15 @@ pub fn completeClipboardRequest( /// This starts a clipboard request, with some basic validation. For example, /// an OSC 52 request is not actually requested if OSC 52 is disabled. +/// +/// Returns true if the request was started, false if it was not (e.g., clipboard +/// doesn't contain text for paste requests). This allows performable keybinds +/// to pass through when the action cannot be performed. fn startClipboardRequest( self: *Surface, loc: apprt.Clipboard, req: apprt.ClipboardRequest, -) !void { +) !bool { switch (req) { .paste => {}, // always allowed .osc_52_read => if (self.config.clipboard_read == .deny) { @@ -5737,14 +6136,14 @@ fn startClipboardRequest( "application attempted to read clipboard, but 'clipboard-read' is set to deny", .{}, ); - return; + return false; }, // No clipboard write code paths travel through this function .osc_52_write => unreachable, } - try self.rt_surface.clipboardRequest(loc, req); + return try self.rt_surface.clipboardRequest(loc, req); } fn completeClipboardPaste( @@ -5819,7 +6218,7 @@ fn completeClipboardPaste( }; for (vecs) |vec| if (vec.len > 0) { - self.io.queueMessage(try termio.Message.writeReq( + self.queueIo(try termio.Message.writeReq( self.alloc, vec, ), .unlocked); @@ -5865,7 +6264,7 @@ fn completeClipboardReadOSC52( const encoded = enc.encode(buf[prefix.len..], data); assert(encoded.len == size); - self.io.queueMessage(try termio.Message.writeReq( + self.queueIo(try termio.Message.writeReq( self.alloc, buf, ), .unlocked); diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 00bf8685a..78f4bef54 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -115,6 +115,11 @@ pub const Action = union(Key) { /// Toggle the visibility of all Ghostty terminal windows. toggle_visibility, + /// Toggle the window background opacity. This only has an effect + /// if the window started as transparent (non-opaque), and toggles + /// it between fully opaque and the configured background opacity. + toggle_background_opacity, + /// Moves a tab by a relative offset. /// /// Adjusts the tab position based on `offset` (e.g., -1 for left, +1 @@ -129,6 +134,9 @@ pub const Action = union(Key) { /// Jump to a specific split. goto_split: GotoSplit, + /// Jump to next/previous window. + goto_window: GotoWindow, + /// Resize the split in the given direction. resize_split: ResizeSplit, @@ -189,8 +197,9 @@ pub const Action = union(Key) { set_title: SetTitle, /// Set the title of the target to a prompted value. It is up to - /// the apprt to prompt. - prompt_title, + /// the apprt to prompt. The value specifies whether to prompt for the + /// surface title or the tab title. + prompt_title: PromptTitle, /// The current working directory has changed for the target terminal. pwd: Pwd, @@ -241,6 +250,9 @@ pub const Action = union(Key) { /// key mode because other input may be ignored. key_sequence: KeySequence, + /// A key table has been activated or deactivated. + key_table: KeyTable, + /// A terminal color was changed programmatically through things /// such as OSC 10/11. color_change: ColorChange, @@ -301,7 +313,9 @@ pub const Action = union(Key) { /// A command has finished, command_finished: CommandFinished, - /// Start the search overlay with an optional initial needle. + /// Start the search overlay with an optional initial needle. If the + /// search is already active and the needle is non-empty, update the + /// current search needle and focus the search input. start_search: StartSearch, /// End the search overlay, clearing the search state and hiding it. @@ -313,6 +327,9 @@ pub const Action = union(Key) { /// The currently selected search match index (1-based). search_selected: SearchSelected, + /// The readonly state of the surface has changed. + readonly: Readonly, + /// Sync with: ghostty_action_tag_e pub const Key = enum(c_int) { quit, @@ -328,9 +345,11 @@ pub const Action = union(Key) { toggle_quick_terminal, toggle_command_palette, toggle_visibility, + toggle_background_opacity, move_tab, goto_tab, goto_split, + goto_window, resize_split, equalize_splits, toggle_split_zoom, @@ -357,6 +376,7 @@ pub const Action = union(Key) { float_window, secure_input, key_sequence, + key_table, color_change, reload_config, config_change, @@ -374,6 +394,7 @@ pub const Action = union(Key) { end_search, search_total, search_selected, + readonly, }; /// Sync with: ghostty_action_u @@ -469,6 +490,13 @@ pub const GotoSplit = enum(c_int) { right, }; +// This is made extern (c_int) to make interop easier with our embedded +// runtime. The small size cost doesn't make a difference in our union. +pub const GotoWindow = enum(c_int) { + previous, + next, +}; + /// The amount to resize the split by and the direction to resize it in. pub const ResizeSplit = extern struct { amount: u16, @@ -531,11 +559,22 @@ pub const QuitTimer = enum(c_int) { stop, }; +pub const Readonly = enum(c_int) { + off, + on, +}; + pub const MouseVisibility = enum(c_int) { visible, hidden, }; +/// Whether to prompt for the surface title or tab title. +pub const PromptTitle = enum(c_int) { + surface, + tab, +}; + pub const MouseOverLink = struct { url: [:0]const u8, @@ -678,6 +717,50 @@ pub const KeySequence = union(enum) { } }; +pub const KeyTable = union(enum) { + activate: []const u8, + deactivate, + deactivate_all, + + // Sync with: ghostty_action_key_table_tag_e + pub const Tag = enum(c_int) { + activate, + deactivate, + deactivate_all, + }; + + // Sync with: ghostty_action_key_table_u + pub const CValue = extern union { + activate: extern struct { + name: [*]const u8, + len: usize, + }, + }; + + // Sync with: ghostty_action_key_table_s + pub const C = extern struct { + tag: Tag, + value: CValue, + }; + + pub fn cval(self: KeyTable) C { + return switch (self) { + .activate => |name| .{ + .tag = .activate, + .value = .{ .activate = .{ .name = name.ptr, .len = name.len } }, + }, + .deactivate => .{ + .tag = .deactivate, + .value = undefined, + }, + .deactivate_all => .{ + .tag = .deactivate_all, + .value = undefined, + }, + }; + } +}; + pub const ColorChange = extern struct { kind: ColorKind, r: u8, @@ -767,6 +850,8 @@ pub const CloseTabMode = enum(c_int) { this, /// Close all other tabs. other, + /// Close all tabs to the right of the current tab. + right, }; pub const CommandFinished = struct { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index da7a585a5..b4ad7f885 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -155,7 +155,7 @@ pub const App = struct { while (it.next()) |entry| { switch (entry.value_ptr.*) { .leader => {}, - .leaf => |leaf| if (leaf.flags.global) return true, + inline .leaf, .leaf_chained => |leaf| if (leaf.flags.global) return true, } } @@ -456,6 +456,9 @@ pub const Surface = struct { /// Wait after the command exits wait_after_command: bool = false, + + /// Context for the new surface + context: apprt.surface.NewSurfaceContext = .window, }; pub fn init(self: *Surface, app: *App, opts: Options) !void { @@ -477,7 +480,7 @@ pub const Surface = struct { errdefer app.core_app.deleteSurface(self); // Shallow copy the config so that we can modify it. - var config = try apprt.surface.newConfig(app.core_app, &app.config); + var config = try apprt.surface.newConfig(app.core_app, &app.config, opts.context); defer config.deinit(); // If we have a working directory from the options then we set it. @@ -539,13 +542,20 @@ pub const Surface = struct { // If we have an initial input then we set it. if (opts.initial_input) |c_input| { const alloc = config.arenaAlloc(); + + // We need to escape the string because the "raw" field + // expects a Zig string. + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try std.zig.stringEscape( + std.mem.sliceTo(c_input, 0), + &buf.writer, + ); + config.input.list.clearRetainingCapacity(); try config.input.list.append( alloc, - .{ .raw = try alloc.dupeZ(u8, std.mem.sliceTo( - c_input, - 0, - )) }, + .{ .raw = try buf.toOwnedSliceSentinel(0) }, ); } @@ -652,7 +662,7 @@ pub const Surface = struct { self: *Surface, clipboard_type: apprt.Clipboard, state: apprt.ClipboardRequest, - ) !void { + ) !bool { // We need to allocate to get a pointer to store our clipboard request // so that it is stable until the read_clipboard callback and call // complete_clipboard_request. This sucks but clipboard requests aren't @@ -667,6 +677,10 @@ pub const Surface = struct { @intCast(@intFromEnum(clipboard_type)), state_ptr, ); + + // Embedded apprt can't synchronously check clipboard content types, + // so we always return true to indicate the request was started. + return true; } fn completeClipboardRequest( @@ -890,14 +904,23 @@ pub const Surface = struct { }; } - pub fn newSurfaceOptions(self: *const Surface) apprt.Surface.Options { + pub fn newSurfaceOptions(self: *const Surface, context: apprt.surface.NewSurfaceContext) apprt.Surface.Options { const font_size: f32 = font_size: { if (!self.app.config.@"window-inherit-font-size") break :font_size 0; break :font_size self.core_surface.font_size.points; }; + const working_directory: ?[*:0]const u8 = wd: { + if (!apprt.surface.shouldInheritWorkingDirectory(context, &self.app.config)) break :wd null; + const cwd = self.core_surface.pwd(self.app.core_app.alloc) catch null orelse break :wd null; + defer self.app.core_app.alloc.free(cwd); + break :wd self.app.core_app.alloc.dupeZ(u8, cwd) catch null; + }; + return .{ .font_size = font_size, + .working_directory = working_directory, + .context = context, }; } @@ -943,7 +966,7 @@ pub const Surface = struct { /// Inspector is the state required for the terminal inspector. A terminal /// inspector is 1:1 with a Surface. pub const Inspector = struct { - const cimgui = @import("cimgui"); + const cimgui = @import("dcimgui"); surface: *Surface, ig_ctx: *cimgui.c.ImGuiContext, @@ -964,10 +987,10 @@ pub const Inspector = struct { }; pub fn init(surface: *Surface) !Inspector { - const ig_ctx = cimgui.c.igCreateContext(null) orelse return error.OutOfMemory; - errdefer cimgui.c.igDestroyContext(ig_ctx); - cimgui.c.igSetCurrentContext(ig_ctx); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const ig_ctx = cimgui.c.ImGui_CreateContext(null) orelse return error.OutOfMemory; + errdefer cimgui.c.ImGui_DestroyContext(ig_ctx); + cimgui.c.ImGui_SetCurrentContext(ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); io.BackendPlatformName = "ghostty_embedded"; // Setup our core inspector @@ -984,9 +1007,9 @@ pub const Inspector = struct { pub fn deinit(self: *Inspector) void { self.surface.core_surface.deactivateInspector(); - cimgui.c.igSetCurrentContext(self.ig_ctx); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); if (self.backend) |v| v.deinit(); - cimgui.c.igDestroyContext(self.ig_ctx); + cimgui.c.ImGui_DestroyContext(self.ig_ctx); } /// Queue a render for the next frame. @@ -997,7 +1020,7 @@ pub const Inspector = struct { /// Initialize the inspector for a metal backend. pub fn initMetal(self: *Inspector, device: objc.Object) bool { defer device.msgSend(void, objc.sel("release"), .{}); - cimgui.c.igSetCurrentContext(self.ig_ctx); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); if (self.backend) |v| { v.deinit(); @@ -1032,7 +1055,7 @@ pub const Inspector = struct { for (0..2) |_| { cimgui.ImGui_ImplMetal_NewFrame(desc.value); try self.newFrame(); - cimgui.c.igNewFrame(); + cimgui.c.ImGui_NewFrame(); // Build our UI render: { @@ -1042,7 +1065,7 @@ pub const Inspector = struct { } // Render - cimgui.c.igRender(); + cimgui.c.ImGui_Render(); } // MTLRenderCommandEncoder @@ -1053,7 +1076,7 @@ pub const Inspector = struct { ); defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); cimgui.ImGui_ImplMetal_RenderDrawData( - cimgui.c.igGetDrawData(), + cimgui.c.ImGui_GetDrawData(), command_buffer.value, encoder.value, ); @@ -1061,22 +1084,24 @@ pub const Inspector = struct { pub fn updateContentScale(self: *Inspector, x: f64, y: f64) void { _ = y; - cimgui.c.igSetCurrentContext(self.ig_ctx); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); // Cache our scale because we use it for cursor position calculations. self.content_scale = x; - // Setup a new style and scale it appropriately. - const style = cimgui.c.ImGuiStyle_ImGuiStyle(); - defer cimgui.c.ImGuiStyle_destroy(style); - cimgui.c.ImGuiStyle_ScaleAllSizes(style, @floatCast(x)); - const active_style = cimgui.c.igGetStyle(); - active_style.* = style.*; + // Setup a new style and scale it appropriately. We must use the + // ImGuiStyle constructor to get proper default values (e.g., + // CurveTessellationTol) rather than zero-initialized values. + var style: cimgui.c.ImGuiStyle = undefined; + cimgui.ext.ImGuiStyle_ImGuiStyle(&style); + cimgui.c.ImGuiStyle_ScaleAllSizes(&style, @floatCast(x)); + const active_style = cimgui.c.ImGui_GetStyle(); + active_style.* = style; } pub fn updateSize(self: *Inspector, width: u32, height: u32) void { - cimgui.c.igSetCurrentContext(self.ig_ctx); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); io.DisplaySize = .{ .x = @floatFromInt(width), .y = @floatFromInt(height) }; } @@ -1089,8 +1114,8 @@ pub const Inspector = struct { _ = mods; self.queueRender(); - cimgui.c.igSetCurrentContext(self.ig_ctx); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); const imgui_button = switch (button) { .left => cimgui.c.ImGuiMouseButton_Left, @@ -1111,8 +1136,8 @@ pub const Inspector = struct { _ = mods; self.queueRender(); - cimgui.c.igSetCurrentContext(self.ig_ctx); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); cimgui.c.ImGuiIO_AddMouseWheelEvent( io, @floatCast(xoff), @@ -1122,8 +1147,8 @@ pub const Inspector = struct { pub fn cursorPosCallback(self: *Inspector, x: f64, y: f64) void { self.queueRender(); - cimgui.c.igSetCurrentContext(self.ig_ctx); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); cimgui.c.ImGuiIO_AddMousePosEvent( io, @floatCast(x * self.content_scale), @@ -1133,15 +1158,15 @@ pub const Inspector = struct { pub fn focusCallback(self: *Inspector, focused: bool) void { self.queueRender(); - cimgui.c.igSetCurrentContext(self.ig_ctx); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); cimgui.c.ImGuiIO_AddFocusEvent(io, focused); } pub fn textCallback(self: *Inspector, text: [:0]const u8) void { self.queueRender(); - cimgui.c.igSetCurrentContext(self.ig_ctx); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, text.ptr); } @@ -1152,8 +1177,8 @@ pub const Inspector = struct { mods: input.Mods, ) !void { self.queueRender(); - cimgui.c.igSetCurrentContext(self.ig_ctx); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); // Update all our modifiers cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftShift, mods.shift); @@ -1172,7 +1197,7 @@ pub const Inspector = struct { } fn newFrame(self: *Inspector) !void { - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); // Determine our delta time const now = try std.time.Instant.now(); @@ -1517,8 +1542,11 @@ pub const CAPI = struct { } /// Returns the config to use for surfaces that inherit from this one. - export fn ghostty_surface_inherited_config(surface: *Surface) Surface.Options { - return surface.newSurfaceOptions(); + export fn ghostty_surface_inherited_config( + surface: *Surface, + source: apprt.surface.NewSurfaceContext, + ) Surface.Options { + return surface.newSurfaceOptions(source); } /// Update the configuration to the provided config for only this surface. @@ -1700,23 +1728,6 @@ pub const CAPI = struct { return @intCast(@as(input.Mods.Backing, @bitCast(result))); } - /// Returns the current possible commands for a surface - /// in the output parameter. The memory is owned by libghostty - /// and doesn't need to be freed. - export fn ghostty_surface_commands( - surface: *Surface, - out: *[*]const input.Command.C, - len: *usize, - ) void { - // In the future we may use this information to filter - // some commands. - _ = surface; - - const commands = input.command.defaultsC; - out.* = commands.ptr; - len.* = commands.len; - } - /// Send this for raw keypresses (i.e. the keyDown event on macOS). /// This will handle the keymap translation and send the appropriate /// key and char events. @@ -1740,13 +1751,18 @@ pub const CAPI = struct { export fn ghostty_surface_key_is_binding( surface: *Surface, event: KeyEvent, + c_flags: ?*input.Binding.Flags.C, ) bool { const core_event = event.keyEvent().core() orelse { log.warn("error processing key event", .{}); return false; }; - return surface.core_surface.keyEventIsBinding(core_event); + const flags = surface.core_surface.keyEventIsBinding( + core_event, + ) orelse return false; + if (c_flags) |ptr| ptr.* = flags.cval(); + return true; } /// Send raw text to the terminal. This is treated like a paste @@ -2149,7 +2165,10 @@ pub const CAPI = struct { if (comptime std.debug.runtime_safety) unreachable; return false; }; - break :sel surface.io.terminal.screens.active.selectWord(pin) orelse return false; + break :sel surface.io.terminal.screens.active.selectWord( + pin, + surface.config.selection_word_chars, + ) orelse return false; }; // Read the selection diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 415d3773d..07b4eb0e7 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -10,4 +10,5 @@ pub const WeakRef = @import("gtk/weak_ref.zig").WeakRef; test { @import("std").testing.refAllDecls(@This()); _ = @import("gtk/ext.zig"); + _ = @import("gtk/key.zig"); } diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 009ce018d..918e77146 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -73,8 +73,8 @@ pub fn clipboardRequest( self: *Self, clipboard_type: apprt.Clipboard, state: apprt.ClipboardRequest, -) !void { - try self.surface.clipboardRequest( +) !bool { + return try self.surface.clipboardRequest( clipboard_type, state, ); diff --git a/src/apprt/gtk/build/gresource.zig b/src/apprt/gtk/build/gresource.zig index c77579aab..d3684c171 100644 --- a/src/apprt/gtk/build/gresource.zig +++ b/src/apprt/gtk/build/gresource.zig @@ -44,6 +44,7 @@ pub const blueprints: []const Blueprint = &.{ .{ .major = 1, .minor = 5, .name = "inspector-window" }, .{ .major = 1, .minor = 2, .name = "resize-overlay" }, .{ .major = 1, .minor = 2, .name = "search-overlay" }, + .{ .major = 1, .minor = 2, .name = "key-state-overlay" }, .{ .major = 1, .minor = 5, .name = "split-tree" }, .{ .major = 1, .minor = 5, .name = "split-tree-split" }, .{ .major = 1, .minor = 2, .name = "surface" }, diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 69576bf00..403f94599 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -8,6 +8,8 @@ const glib = @import("glib"); const gobject = @import("gobject"); const gtk = @import("gtk"); +const build_config = @import("../../../build_config.zig"); +const state = &@import("../../../global.zig").state; const i18n = @import("../../../os/main.zig").i18n; const apprt = @import("../../../apprt.zig"); const cgroup = @import("../cgroup.zig"); @@ -659,12 +661,17 @@ pub const Application = extern struct { .goto_split => return Action.gotoSplit(target, value), + .goto_window => return Action.gotoWindow(value), + .goto_tab => return Action.gotoTab(target, value), .initial_size => return Action.initialSize(target, value), .inspector => return Action.controlInspector(target, value), + .key_sequence => return Action.keySequence(target, value), + .key_table => return Action.keyTable(target, value), + .mouse_over_link => Action.mouseOverLink(target, value), .mouse_shape => Action.mouseShape(target, value), .mouse_visibility => Action.mouseVisibility(target, value), @@ -693,7 +700,7 @@ pub const Application = extern struct { .progress_report => return Action.progressReport(target, value), - .prompt_title => return Action.promptTitle(target), + .prompt_title => return Action.promptTitle(target, value), .quit => self.quit(), @@ -726,8 +733,9 @@ pub const Application = extern struct { .toggle_split_zoom => return Action.toggleSplitZoom(target), .show_on_screen_keyboard => return Action.showOnScreenKeyboard(target), .command_finished => return Action.commandFinished(target, value), + .readonly => return Action.setReadonly(target, value), - .start_search => Action.startSearch(target), + .start_search => Action.startSearch(target, value), .end_search => Action.endSearch(target), .search_total => Action.searchTotal(target, value), .search_selected => Action.searchSelected(target, value), @@ -737,8 +745,8 @@ pub const Application = extern struct { .close_all_windows, .float_window, .toggle_visibility, + .toggle_background_opacity, .cell_size, - .key_sequence, .render_inspector, .renderer_health, .color_change, @@ -1583,7 +1591,7 @@ pub const Application = extern struct { .dark; log.debug("style manager changed scheme={}", .{scheme}); - const priv = self.private(); + const priv: *Private = self.private(); const core_app = priv.core_app; core_app.colorSchemeEvent(self.rt(), scheme) catch |err| { log.warn("error updating app color scheme err={}", .{err}); @@ -1596,6 +1604,26 @@ pub const Application = extern struct { ); }; } + + if (gtk_version.atLeast(4, 20, 0)) { + const gtk_scheme: gtk.InterfaceColorScheme = switch (scheme) { + .light => gtk.InterfaceColorScheme.light, + .dark => gtk.InterfaceColorScheme.dark, + }; + var value = gobject.ext.Value.newFrom(gtk_scheme); + gobject.Object.setProperty( + priv.css_provider.as(gobject.Object), + "prefers-color-scheme", + &value, + ); + for (priv.custom_css_providers.items) |css_provider| { + gobject.Object.setProperty( + css_provider.as(gobject.Object), + "prefers-color-scheme", + &value, + ); + } + } } fn handleReloadConfig( @@ -1993,6 +2021,69 @@ const Action = struct { } } + pub fn gotoWindow(direction: apprt.action.GotoWindow) bool { + const glist = gtk.Window.listToplevels(); + defer glist.free(); + + // The window we're starting from is typically our active window. + const starting: *glib.List = @as(?*glib.List, glist.findCustom( + null, + findActiveWindow, + )) orelse glist; + + // Go forward or backwards in the list until we find a valid + // window that is visible. + var current_: ?*glib.List = starting; + while (current_) |node| : (current_ = switch (direction) { + .next => node.f_next, + .previous => node.f_prev, + }) { + const data = node.f_data orelse continue; + const gtk_window: *gtk.Window = @ptrCast(@alignCast(data)); + if (gotoWindowMaybe(gtk_window)) return true; + } + + // If we reached here, we didn't find a valid window to focus. + // Wrap around. + current_ = switch (direction) { + .next => glist, + .previous => last: { + var end: *glib.List = glist; + while (end.f_next) |next| end = next; + break :last end; + }, + }; + while (current_) |node| : (current_ = switch (direction) { + .next => node.f_next, + .previous => node.f_prev, + }) { + if (current_ == starting) break; + const data = node.f_data orelse continue; + const gtk_window: *gtk.Window = @ptrCast(@alignCast(data)); + if (gotoWindowMaybe(gtk_window)) return true; + } + + return false; + } + + fn gotoWindowMaybe(gtk_window: *gtk.Window) bool { + // If it is already active skip it. + if (gtk_window.isActive() != 0) return false; + // If it is hidden, skip it. + if (gtk_window.as(gtk.Widget).isVisible() == 0) return false; + // If it isn't a Ghostty window, skip it. + const window = gobject.ext.cast( + Window, + gtk_window, + ) orelse return false; + + // Focus our active surface + const surface = window.getActiveSurface() orelse return false; + gtk.Window.present(gtk_window); + surface.grabFocus(); + return true; + } + pub fn initialSize( target: apprt.Target, value: apprt.action.InitialSize, @@ -2147,8 +2238,8 @@ const Action = struct { .{}, ); - // Create a new tab - win.newTab(parent); + // Create a new tab with window context (first tab in new window) + win.newTabForWindow(parent); // Show the window gtk.Window.present(win.as(gtk.Window)); @@ -2230,12 +2321,18 @@ const Action = struct { }; } - pub fn promptTitle(target: apprt.Target) bool { - switch (target) { - .app => return false, - .surface => |v| { - v.rt_surface.surface.promptTitle(); - return true; + pub fn promptTitle(target: apprt.Target, value: apprt.action.PromptTitle) bool { + switch (value) { + .surface => switch (target) { + .app => return false, + .surface => |v| { + v.rt_surface.surface.promptTitle(); + return true; + }, + }, + .tab => { + // GTK does not yet support tab title prompting + return false; }, } } @@ -2303,10 +2400,14 @@ const Action = struct { SplitTree, surface.as(gtk.Widget), ) orelse { - log.warn("surface is not in a split tree, ignoring goto_split", .{}); + log.warn("surface is not in a split tree, ignoring resize_split", .{}); return false; }; + // If the tree has no splits (only one leaf), this action is not performable. + // This allows the key event to pass through to the terminal. + if (!tree.getIsSplit()) return false; + return tree.resize( switch (value.direction) { .up => .up, @@ -2342,17 +2443,17 @@ const Action = struct { } } - pub fn startSearch(target: apprt.Target) void { + pub fn startSearch(target: apprt.Target, value: apprt.action.StartSearch) void { switch (target) { .app => {}, - .surface => |v| v.rt_surface.surface.setSearchActive(true), + .surface => |v| v.rt_surface.surface.setSearchActive(true, value.needle), } } pub fn endSearch(target: apprt.Target) void { switch (target) { .app => {}, - .surface => |v| v.rt_surface.surface.setSearchActive(false), + .surface => |v| v.rt_surface.surface.setSearchActive(false, ""), } } @@ -2453,6 +2554,18 @@ const Action = struct { .surface => |core| { // TODO: pass surface ID when we have that const surface = core.rt_surface.surface; + const tree = ext.getAncestor( + SplitTree, + surface.as(gtk.Widget), + ) orelse { + log.warn("surface is not in a split tree, ignoring toggle_split_zoom", .{}); + return false; + }; + + // If the tree has no splits (only one leaf), this action is not performable. + // This allows the key event to pass through to the terminal. + if (!tree.getIsSplit()) return false; + return surface.as(gtk.Widget).activateAction("split-tree.zoom", null) != 0; }, } @@ -2564,6 +2677,45 @@ const Action = struct { }, } } + + pub fn setReadonly(target: apprt.Target, value: apprt.Action.Value(.readonly)) bool { + switch (target) { + .app => return false, + .surface => |surface| { + return surface.rt_surface.gobj().setReadonly(value); + }, + } + } + + pub fn keySequence(target: apprt.Target, value: apprt.Action.Value(.key_sequence)) bool { + switch (target) { + .app => { + log.warn("key_sequence action to app is unexpected", .{}); + return false; + }, + .surface => |core| { + core.rt_surface.gobj().keySequenceAction(value) catch |err| { + log.warn("error handling key_sequence action: {}", .{err}); + }; + return true; + }, + } + } + + pub fn keyTable(target: apprt.Target, value: apprt.Action.Value(.key_table)) bool { + switch (target) { + .app => { + log.warn("key_table action to app is unexpected", .{}); + return false; + }, + .surface => |core| { + core.rt_surface.gobj().keyTableAction(value) catch |err| { + log.warn("error handling key_table action: {}", .{err}); + }; + return true; + }, + } + } }; /// This sets various GTK-related environment variables as necessary @@ -2585,7 +2737,9 @@ fn setGtkEnv(config: *const CoreConfig) error{NoSpaceLeft}!void { /// disable it. @"vulkan-disable": bool = false, } = .{ - .opengl = config.@"gtk-opengl-debug", + // `gtk-opengl-debug` dumps logs directly to stderr so both must be true + // to enable OpenGL debugging. + .opengl = state.logging.stderr and config.@"gtk-opengl-debug", }; var gdk_disable: struct { diff --git a/src/apprt/gtk/class/command_palette.zig b/src/apprt/gtk/class/command_palette.zig index 6da49115e..0d91c43b2 100644 --- a/src/apprt/gtk/class/command_palette.zig +++ b/src/apprt/gtk/class/command_palette.zig @@ -10,9 +10,12 @@ const gtk = @import("gtk"); const input = @import("../../../input.zig"); const gresource = @import("../build/gresource.zig"); const key = @import("../key.zig"); +const WeakRef = @import("../weak_ref.zig").WeakRef; const Common = @import("../class.zig").Common; const Application = @import("application.zig").Application; const Window = @import("window.zig").Window; +const Surface = @import("surface.zig").Surface; +const Tab = @import("tab.zig").Tab; const Config = @import("config.zig").Config; const log = std.log.scoped(.gtk_ghostty_command_palette); @@ -146,34 +149,138 @@ pub const CommandPalette = extern struct { return; }; - const cfg = config.get(); - // Clear existing binds priv.source.removeAll(); + const alloc = Application.default().allocator(); + var commands: std.ArrayList(*Command) = .{}; + defer { + for (commands.items) |cmd| cmd.unref(); + commands.deinit(alloc); + } + + self.collectJumpCommands(config, &commands) catch |err| { + log.warn("failed to collect jump commands: {}", .{err}); + }; + + self.collectRegularCommands(config, &commands, alloc); + + // Sort commands + std.mem.sort(*Command, commands.items, {}, struct { + fn lessThan(_: void, a: *Command, b: *Command) bool { + return compareCommands(a, b); + } + }.lessThan); + + for (commands.items) |cmd| { + const cmd_ref = cmd.as(gobject.Object); + priv.source.append(cmd_ref); + } + } + + /// Collect regular commands from configuration, filtering out unsupported actions. + fn collectRegularCommands( + self: *CommandPalette, + config: *Config, + commands: *std.ArrayList(*Command), + alloc: std.mem.Allocator, + ) void { + _ = self; + const cfg = config.get(); + for (cfg.@"command-palette-entry".value.items) |command| { // Filter out actions that are not implemented or don't make sense // for GTK. - switch (command.action) { - .close_all_windows, - .toggle_secure_input, - .check_for_updates, - .redo, - .undo, - .reset_window_size, - .toggle_window_float_on_top, - => continue, + if (!isActionSupportedOnGtk(command.action)) continue; - else => {}, - } + const cmd = Command.new(config, command) catch |err| { + log.warn("failed to create command: {}", .{err}); + continue; + }; + errdefer cmd.unref(); - const cmd = Command.new(config, command); - const cmd_ref = cmd.as(gobject.Object); - priv.source.append(cmd_ref); - cmd_ref.unref(); + commands.append(alloc, cmd) catch |err| { + log.warn("failed to add command to list: {}", .{err}); + continue; + }; } } + /// Check if an action is supported on GTK. + fn isActionSupportedOnGtk(action: input.Binding.Action) bool { + return switch (action) { + .close_all_windows, + .toggle_secure_input, + .check_for_updates, + .redo, + .undo, + .reset_window_size, + .toggle_window_float_on_top, + => false, + + else => true, + }; + } + + /// Collect jump commands for all surfaces across all windows. + fn collectJumpCommands( + self: *CommandPalette, + config: *Config, + commands: *std.ArrayList(*Command), + ) !void { + _ = self; + const app = Application.default(); + const alloc = app.allocator(); + + // Get all surfaces from the core app + const core_app = app.core(); + for (core_app.surfaces.items) |apprt_surface| { + const surface = apprt_surface.gobj(); + const cmd = Command.newJump(config, surface); + errdefer cmd.unref(); + try commands.append(alloc, cmd); + } + } + + /// Compare two commands for sorting. + /// Sorts alphabetically by title (case-insensitive), with colon normalization + /// so "Foo:" sorts before "Foo Bar:". Uses sort_key as tie-breaker. + fn compareCommands(a: *Command, b: *Command) bool { + const a_title = a.propGetTitle() orelse return false; + const b_title = b.propGetTitle() orelse return true; + + // Compare case-insensitively with colon normalization + for (0..@min(a_title.len, b_title.len)) |i| { + // Get characters, replacing ':' with '\t' + const a_char = if (a_title[i] == ':') '\t' else a_title[i]; + const b_char = if (b_title[i] == ':') '\t' else b_title[i]; + + const a_lower = std.ascii.toLower(a_char); + const b_lower = std.ascii.toLower(b_char); + + if (a_lower != b_lower) { + return a_lower < b_lower; + } + } + + // If one title is a prefix of the other, shorter one comes first + if (a_title.len != b_title.len) { + return a_title.len < b_title.len; + } + + // Titles are equal - use sort_key as tie-breaker if both are jump commands + const a_sort_key = switch (a.private().data) { + .regular => return false, + .jump => |*ja| ja.sort_key, + }; + const b_sort_key = switch (b.private().data) { + .regular => return false, + .jump => |*jb| jb.sort_key, + }; + + return a_sort_key < b_sort_key; + } + fn close(self: *CommandPalette) void { const priv = self.private(); _ = priv.dialog.close(); @@ -234,6 +341,16 @@ pub const CommandPalette = extern struct { self.close(); const cmd = gobject.ext.cast(Command, object_ orelse return) orelse return; + + // Handle jump commands differently + if (cmd.isJump()) { + const surface = cmd.getJumpSurface() orelse return; + defer surface.unref(); + surface.present(); + return; + } + + // Regular command - emit trigger signal const action = cmd.getAction() orelse return; // Signal that an an action has been selected. Signals are synchronous @@ -413,31 +530,63 @@ const Command = extern struct { }; pub const Private = struct { - /// The configuration we should use to get keybindings. config: ?*Config = null, - - /// Arena used to manage our allocations. arena: ArenaAllocator, - - /// The command. - command: ?input.Command = null, - - /// Cache the formatted action. - action: ?[:0]const u8 = null, - - /// Cache the formatted action_key. - action_key: ?[:0]const u8 = null, + data: CommandData, pub var offset: c_int = 0; + + pub const CommandData = union(enum) { + regular: RegularData, + jump: JumpData, + }; + + pub const RegularData = struct { + command: input.Command, + action: ?[:0]const u8 = null, + action_key: ?[:0]const u8 = null, + }; + + pub const JumpData = struct { + surface: WeakRef(Surface) = .empty, + title: ?[:0]const u8 = null, + description: ?[:0]const u8 = null, + sort_key: usize, + }; }; - pub fn new(config: *Config, command: input.Command) *Self { + pub fn new(config: *Config, command: input.Command) Allocator.Error!*Self { + const self = gobject.ext.newInstance(Self, .{ + .config = config, + }); + errdefer self.unref(); + + const priv = self.private(); + const cloned = try command.clone(priv.arena.allocator()); + + priv.data = .{ + .regular = .{ + .command = cloned, + }, + }; + + return self; + } + + /// Create a new jump command that focuses a specific surface. + pub fn newJump(config: *Config, surface: *Surface) *Self { const self = gobject.ext.newInstance(Self, .{ .config = config, }); const priv = self.private(); - priv.command = command.clone(priv.arena.allocator()) catch null; + priv.data = .{ + .jump = .{ + // TODO: Replace with surface id whenever Ghostty adds one + .sort_key = @intFromPtr(surface), + }, + }; + priv.data.jump.surface.set(surface); return self; } @@ -459,6 +608,13 @@ const Command = extern struct { priv.config = null; } + switch (priv.data) { + .regular => {}, + .jump => |*j| { + j.surface.set(null); + }, + } + gobject.Object.virtual_methods.dispose.call( Class.parent, self.as(Parent), @@ -481,52 +637,99 @@ const Command = extern struct { fn propGetActionKey(self: *Self) ?[:0]const u8 { const priv = self.private(); - if (priv.action_key) |action_key| return action_key; + const regular = switch (priv.data) { + .regular => |*r| r, + .jump => return null, + }; - const command = priv.command orelse return null; + if (regular.action_key) |action_key| return action_key; - priv.action_key = std.fmt.allocPrintSentinel( + regular.action_key = std.fmt.allocPrintSentinel( priv.arena.allocator(), "{f}", - .{command.action}, + .{regular.command.action}, 0, ) catch null; - return priv.action_key; + return regular.action_key; } fn propGetAction(self: *Self) ?[:0]const u8 { const priv = self.private(); - if (priv.action) |action| return action; + const regular = switch (priv.data) { + .regular => |*r| r, + .jump => return null, + }; - const command = priv.command orelse return null; + if (regular.action) |action| return action; const cfg = if (priv.config) |config| config.get() else return null; const keybinds = cfg.keybind.set; const alloc = priv.arena.allocator(); - priv.action = action: { + regular.action = action: { var buf: [64]u8 = undefined; - const trigger = keybinds.getTrigger(command.action) orelse break :action null; + const trigger = keybinds.getTrigger(regular.command.action) orelse break :action null; const accel = (key.accelFromTrigger(&buf, trigger) catch break :action null) orelse break :action null; break :action alloc.dupeZ(u8, accel) catch return null; }; - return priv.action; + return regular.action; } fn propGetTitle(self: *Self) ?[:0]const u8 { const priv = self.private(); - const command = priv.command orelse return null; - return command.title; + + switch (priv.data) { + .regular => |*r| return r.command.title, + .jump => |*j| { + if (j.title) |title| return title; + + const surface = j.surface.get() orelse return null; + defer surface.unref(); + + const alloc = priv.arena.allocator(); + const surface_title = surface.getTitle() orelse "Untitled"; + + j.title = std.fmt.allocPrintSentinel( + alloc, + "Focus: {s}", + .{surface_title}, + 0, + ) catch null; + + return j.title; + }, + } } fn propGetDescription(self: *Self) ?[:0]const u8 { const priv = self.private(); - const command = priv.command orelse return null; - return command.description; + + switch (priv.data) { + .regular => |*r| return r.command.description, + .jump => |*j| { + if (j.description) |desc| return desc; + + const surface = j.surface.get() orelse return null; + defer surface.unref(); + + const alloc = priv.arena.allocator(); + + const title = surface.getTitle() orelse "Untitled"; + const pwd = surface.getPwd(); + + if (pwd) |p| { + if (std.mem.indexOf(u8, title, p) == null) { + j.description = alloc.dupeZ(u8, p) catch null; + } + } + + return j.description; + }, + } } //--------------------------------------------------------------- @@ -536,8 +739,26 @@ const Command = extern struct { /// allocated data that will be freed when this object is. pub fn getAction(self: *Self) ?input.Binding.Action { const priv = self.private(); - const command = priv.command orelse return null; - return command.action; + return switch (priv.data) { + .regular => |*r| r.command.action, + .jump => null, + }; + } + + /// Check if this is a jump command. + pub fn isJump(self: *Self) bool { + const priv = self.private(); + return priv.data == .jump; + } + + /// Get the jump surface. Returns a strong reference that the caller + /// must unref when done, or null if the surface has been destroyed. + pub fn getJumpSurface(self: *Self) ?*Surface { + const priv = self.private(); + return switch (priv.data) { + .regular => null, + .jump => |*j| j.surface.get(), + }; } //--------------------------------------------------------------- diff --git a/src/apprt/gtk/class/global_shortcuts.zig b/src/apprt/gtk/class/global_shortcuts.zig index 57652916a..cf0f31a6e 100644 --- a/src/apprt/gtk/class/global_shortcuts.zig +++ b/src/apprt/gtk/class/global_shortcuts.zig @@ -169,13 +169,17 @@ pub const GlobalShortcuts = extern struct { var trigger_buf: [1024]u8 = undefined; var it = config.keybind.set.bindings.iterator(); while (it.next()) |entry| { - const leaf = switch (entry.value_ptr.*) { - // Global shortcuts can't have leaders + const leaf: Binding.Set.GenericLeaf = switch (entry.value_ptr.*) { .leader => continue, - .leaf => |leaf| leaf, + inline .leaf, .leaf_chained => |leaf| leaf.generic(), }; if (!leaf.flags.global) continue; + // We only allow global keybinds that map to exactly a single + // action for now. TODO: remove this restriction + const actions = leaf.actionsSlice(); + if (actions.len != 1) continue; + const trigger = if (key.xdgShortcutFromTrigger( &trigger_buf, entry.key_ptr.*, @@ -197,7 +201,7 @@ pub const GlobalShortcuts = extern struct { try priv.map.put( alloc, try alloc.dupeZ(u8, trigger), - leaf.action, + actions[0], ); } diff --git a/src/apprt/gtk/class/imgui_widget.zig b/src/apprt/gtk/class/imgui_widget.zig index ef1ca05c9..79e85fad2 100644 --- a/src/apprt/gtk/class/imgui_widget.zig +++ b/src/apprt/gtk/class/imgui_widget.zig @@ -1,7 +1,7 @@ const std = @import("std"); const assert = @import("../../../quirks.zig").inlineAssert; -const cimgui = @import("cimgui"); +const cimgui = @import("dcimgui"); const gl = @import("opengl"); const adw = @import("adw"); const gdk = @import("gdk"); @@ -126,7 +126,7 @@ pub const ImguiWidget = extern struct { log.warn("Dear ImGui context not initialized", .{}); return error.ContextNotInitialized; }; - cimgui.c.igSetCurrentContext(ig_context); + cimgui.c.ImGui_SetCurrentContext(ig_context); } /// Initialize the frame. Expects that the context is already current. @@ -137,7 +137,7 @@ pub const ImguiWidget = extern struct { const priv = self.private(); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); // Determine our delta time const now = std.time.Instant.now() catch unreachable; @@ -163,7 +163,7 @@ pub const ImguiWidget = extern struct { self.setCurrentContext() catch return false; - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); const mods = key.translateMods(gtk_mods); cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftShift, mods.shift); @@ -219,14 +219,14 @@ pub const ImguiWidget = extern struct { return; } - priv.ig_context = cimgui.c.igCreateContext(null) orelse { + priv.ig_context = cimgui.c.ImGui_CreateContext(null) orelse { log.warn("unable to initialize Dear ImGui context", .{}); return; }; self.setCurrentContext() catch return; // Setup some basic config - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); io.BackendPlatformName = "ghostty_gtk"; // Realize means that our OpenGL context is ready, so we can now @@ -247,7 +247,7 @@ pub const ImguiWidget = extern struct { /// Handle a request to resize the GLArea fn glAreaResize(area: *gtk.GLArea, width: c_int, height: c_int, self: *Self) callconv(.c) void { self.setCurrentContext() catch return; - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); const scale_factor = area.as(gtk.Widget).getScaleFactor(); // Our display size is always unscaled. We'll do the scaling in the @@ -255,12 +255,14 @@ pub const ImguiWidget = extern struct { io.DisplaySize = .{ .x = @floatFromInt(width), .y = @floatFromInt(height) }; io.DisplayFramebufferScale = .{ .x = 1, .y = 1 }; - // Setup a new style and scale it appropriately. - const style = cimgui.c.ImGuiStyle_ImGuiStyle(); - defer cimgui.c.ImGuiStyle_destroy(style); - cimgui.c.ImGuiStyle_ScaleAllSizes(style, @floatFromInt(scale_factor)); - const active_style = cimgui.c.igGetStyle(); - active_style.* = style.*; + // Setup a new style and scale it appropriately. We must use the + // ImGuiStyle constructor to get proper default values (e.g., + // CurveTessellationTol) rather than zero-initialized values. + var style: cimgui.c.ImGuiStyle = undefined; + cimgui.ext.ImGuiStyle_ImGuiStyle(&style); + cimgui.c.ImGuiStyle_ScaleAllSizes(&style, @floatFromInt(scale_factor)); + const active_style = cimgui.c.ImGui_GetStyle(); + active_style.* = style; } /// Handle a request to render the contents of our GLArea @@ -273,33 +275,33 @@ pub const ImguiWidget = extern struct { for (0..2) |_| { cimgui.ImGui_ImplOpenGL3_NewFrame(); self.newFrame(); - cimgui.c.igNewFrame(); + cimgui.c.ImGui_NewFrame(); // Call the virtual method to draw the UI. self.render(); // Render - cimgui.c.igRender(); + cimgui.c.ImGui_Render(); } // OpenGL final render gl.clearColor(0x28 / 0xFF, 0x2C / 0xFF, 0x34 / 0xFF, 1.0); gl.clear(gl.c.GL_COLOR_BUFFER_BIT); - cimgui.ImGui_ImplOpenGL3_RenderDrawData(cimgui.c.igGetDrawData()); + cimgui.ImGui_ImplOpenGL3_RenderDrawData(cimgui.c.ImGui_GetDrawData()); return @intFromBool(true); } fn ecFocusEnter(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void { self.setCurrentContext() catch return; - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); cimgui.c.ImGuiIO_AddFocusEvent(io, true); self.queueRender(); } fn ecFocusLeave(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void { self.setCurrentContext() catch return; - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); cimgui.c.ImGuiIO_AddFocusEvent(io, false); self.queueRender(); } @@ -345,7 +347,7 @@ pub const ImguiWidget = extern struct { ) callconv(.c) void { self.queueRender(); self.setCurrentContext() catch return; - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton(); if (translateMouseButton(gdk_button)) |button| { cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, true); @@ -361,7 +363,7 @@ pub const ImguiWidget = extern struct { ) callconv(.c) void { self.queueRender(); self.setCurrentContext() catch return; - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton(); if (translateMouseButton(gdk_button)) |button| { cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, false); @@ -376,7 +378,7 @@ pub const ImguiWidget = extern struct { ) callconv(.c) void { self.queueRender(); self.setCurrentContext() catch return; - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); const scale_factor = self.getScaleFactor(); cimgui.c.ImGuiIO_AddMousePosEvent( io, @@ -393,7 +395,7 @@ pub const ImguiWidget = extern struct { ) callconv(.c) c_int { self.queueRender(); self.setCurrentContext() catch return @intFromBool(false); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); cimgui.c.ImGuiIO_AddMouseWheelEvent( io, @floatCast(x), @@ -409,7 +411,7 @@ pub const ImguiWidget = extern struct { ) callconv(.c) void { self.queueRender(); self.setCurrentContext() catch return; - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, bytes); } diff --git a/src/apprt/gtk/class/key_state_overlay.zig b/src/apprt/gtk/class/key_state_overlay.zig new file mode 100644 index 000000000..7aca8f01d --- /dev/null +++ b/src/apprt/gtk/class/key_state_overlay.zig @@ -0,0 +1,342 @@ +const std = @import("std"); +const adw = @import("adw"); +const glib = @import("glib"); +const gobject = @import("gobject"); +const gtk = @import("gtk"); + +const ext = @import("../ext.zig"); +const gresource = @import("../build/gresource.zig"); +const Application = @import("application.zig").Application; +const Common = @import("../class.zig").Common; + +const log = std.log.scoped(.gtk_ghostty_key_state_overlay); + +/// An overlay that displays the current key table stack and pending key sequence. +/// This helps users understand what key bindings are active and what keys they've +/// pressed in a multi-key sequence. +pub const KeyStateOverlay = extern struct { + const Self = @This(); + parent_instance: Parent, + pub const Parent = adw.Bin; + pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .name = "GhosttyKeyStateOverlay", + .instanceInit = &init, + .classInit = &Class.init, + .parent_class = &Class.parent, + .private = .{ .Type = Private, .offset = &Private.offset }, + }); + + pub const properties = struct { + pub const tables = struct { + pub const name = "tables"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*ext.StringList, + .{ + .accessor = gobject.ext.typedAccessor( + Self, + ?*ext.StringList, + .{ + .getter = getTables, + .getter_transfer = .none, + .setter = setTables, + .setter_transfer = .full, + }, + ), + }, + ); + }; + + pub const @"has-tables" = struct { + pub const name = "has-tables"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .default = false, + .accessor = gobject.ext.typedAccessor( + Self, + bool, + .{ .getter = getHasTables }, + ), + }, + ); + }; + + pub const sequence = struct { + pub const name = "sequence"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*ext.StringList, + .{ + .accessor = gobject.ext.typedAccessor( + Self, + ?*ext.StringList, + .{ + .getter = getSequence, + .getter_transfer = .none, + .setter = setSequence, + .setter_transfer = .full, + }, + ), + }, + ); + }; + + pub const @"has-sequence" = struct { + pub const name = "has-sequence"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .default = false, + .accessor = gobject.ext.typedAccessor( + Self, + bool, + .{ .getter = getHasSequence }, + ), + }, + ); + }; + + pub const @"valign-target" = struct { + pub const name = "valign-target"; + const impl = gobject.ext.defineProperty( + name, + Self, + gtk.Align, + .{ + .default = .end, + .accessor = C.privateShallowFieldAccessor("valign_target"), + }, + ); + }; + }; + + const Private = struct { + /// The key table stack. + tables: ?*ext.StringList = null, + + /// The key sequence. + sequence: ?*ext.StringList = null, + + /// Target vertical alignment for the overlay. + valign_target: gtk.Align = .end, + + pub var offset: c_int = 0; + }; + + fn init(self: *Self, _: *Class) callconv(.c) void { + gtk.Widget.initTemplate(self.as(gtk.Widget)); + } + + fn getTables(self: *Self) ?*ext.StringList { + return self.private().tables; + } + + fn getSequence(self: *Self) ?*ext.StringList { + return self.private().sequence; + } + + fn setTables(self: *Self, value: ?*ext.StringList) void { + const priv = self.private(); + if (priv.tables) |old| { + old.destroy(); + priv.tables = null; + } + if (value) |v| { + priv.tables = v; + } + + self.as(gobject.Object).notifyByPspec(properties.tables.impl.param_spec); + self.as(gobject.Object).notifyByPspec(properties.@"has-tables".impl.param_spec); + } + + fn setSequence(self: *Self, value: ?*ext.StringList) void { + const priv = self.private(); + if (priv.sequence) |old| { + old.destroy(); + priv.sequence = null; + } + if (value) |v| { + priv.sequence = v; + } + + self.as(gobject.Object).notifyByPspec(properties.sequence.impl.param_spec); + self.as(gobject.Object).notifyByPspec(properties.@"has-sequence".impl.param_spec); + } + + fn getHasTables(self: *Self) bool { + const v = self.private().tables orelse return false; + return v.strings.len > 0; + } + + fn getHasSequence(self: *Self) bool { + const v = self.private().sequence orelse return false; + return v.strings.len > 0; + } + + fn closureShowChevron( + _: *Self, + has_tables: bool, + has_sequence: bool, + ) callconv(.c) c_int { + return if (has_tables and has_sequence) 1 else 0; + } + + fn closureHasState( + _: *Self, + has_tables: bool, + has_sequence: bool, + ) callconv(.c) c_int { + return if (has_tables or has_sequence) 1 else 0; + } + + fn closureTablesText( + _: *Self, + tables: ?*ext.StringList, + ) callconv(.c) ?[*:0]const u8 { + const list = tables orelse return null; + if (list.strings.len == 0) return null; + + var buf: std.Io.Writer.Allocating = .init(Application.default().allocator()); + defer buf.deinit(); + + for (list.strings, 0..) |s, i| { + if (i > 0) buf.writer.writeAll(" > ") catch return null; + buf.writer.writeAll(s) catch return null; + } + + return glib.ext.dupeZ(u8, buf.written()); + } + + fn closureSequenceText( + _: *Self, + sequence: ?*ext.StringList, + ) callconv(.c) ?[*:0]const u8 { + const list = sequence orelse return null; + if (list.strings.len == 0) return null; + + var buf: std.Io.Writer.Allocating = .init(Application.default().allocator()); + defer buf.deinit(); + + for (list.strings, 0..) |s, i| { + if (i > 0) buf.writer.writeAll(" ") catch return null; + buf.writer.writeAll(s) catch return null; + } + + return glib.ext.dupeZ(u8, buf.written()); + } + + //--------------------------------------------------------------- + // Template callbacks + + fn onDragEnd( + _: *gtk.GestureDrag, + _: f64, + offset_y: f64, + self: *Self, + ) callconv(.c) void { + // Key state overlay only moves between top-center and bottom-center. + // Horizontal alignment is always center. + const priv = self.private(); + const widget = self.as(gtk.Widget); + const parent = widget.getParent() orelse return; + + const parent_height: f64 = @floatFromInt(parent.getAllocatedHeight()); + const self_height: f64 = @floatFromInt(widget.getAllocatedHeight()); + + const self_y: f64 = if (priv.valign_target == .start) 0 else parent_height - self_height; + const new_y = self_y + offset_y + (self_height / 2); + + const new_valign: gtk.Align = if (new_y > parent_height / 2) .end else .start; + + if (new_valign != priv.valign_target) { + priv.valign_target = new_valign; + self.as(gobject.Object).notifyByPspec(properties.@"valign-target".impl.param_spec); + self.as(gtk.Widget).queueResize(); + } + } + + //--------------------------------------------------------------- + // Virtual methods + + fn dispose(self: *Self) callconv(.c) void { + gtk.Widget.disposeTemplate( + self.as(gtk.Widget), + getGObjectType(), + ); + + gobject.Object.virtual_methods.dispose.call( + Class.parent, + self.as(Parent), + ); + } + + fn finalize(self: *Self) callconv(.c) void { + const priv = self.private(); + + if (priv.tables) |v| { + v.destroy(); + } + if (priv.sequence) |v| { + v.destroy(); + } + + gobject.Object.virtual_methods.finalize.call( + Class.parent, + self.as(Parent), + ); + } + + const C = Common(Self, Private); + pub const as = C.as; + pub const ref = C.ref; + pub const unref = C.unref; + const private = C.private; + + pub const Class = extern struct { + parent_class: Parent.Class, + var parent: *Parent.Class = undefined; + pub const Instance = Self; + + fn init(class: *Class) callconv(.c) void { + gtk.Widget.Class.setTemplateFromResource( + class.as(gtk.Widget.Class), + comptime gresource.blueprint(.{ + .major = 1, + .minor = 2, + .name = "key-state-overlay", + }), + ); + + // Template Callbacks + class.bindTemplateCallback("on_drag_end", &onDragEnd); + class.bindTemplateCallback("show_chevron", &closureShowChevron); + class.bindTemplateCallback("has_state", &closureHasState); + class.bindTemplateCallback("tables_text", &closureTablesText); + class.bindTemplateCallback("sequence_text", &closureSequenceText); + + // Properties + gobject.ext.registerProperties(class, &.{ + properties.tables.impl, + properties.@"has-tables".impl, + properties.sequence.impl, + properties.@"has-sequence".impl, + properties.@"valign-target".impl, + }); + + // Virtual methods + gobject.Object.virtual_methods.dispose.implement(class, &dispose); + gobject.Object.virtual_methods.finalize.implement(class, &finalize); + } + + pub const as = C.Class.as; + pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate; + pub const bindTemplateCallback = C.Class.bindTemplateCallback; + }; +}; diff --git a/src/apprt/gtk/class/search_overlay.zig b/src/apprt/gtk/class/search_overlay.zig index 4936cd967..a81115462 100644 --- a/src/apprt/gtk/class/search_overlay.zig +++ b/src/apprt/gtk/class/search_overlay.zig @@ -250,6 +250,13 @@ pub const SearchOverlay = extern struct { priv.active = active; } + // Set contents of search + pub fn setSearchContents(self: *Self, content: [:0]const u8) void { + const priv = self.private(); + priv.search_entry.as(gtk.Editable).setText(content); + signals.@"search-changed".impl.emit(self, null, .{content}, null); + } + /// Set the total number of search matches. pub fn setSearchTotal(self: *Self, total: ?usize) void { const priv = self.private(); diff --git a/src/apprt/gtk/class/split_tree.zig b/src/apprt/gtk/class/split_tree.zig index 48656c951..0ff7e6044 100644 --- a/src/apprt/gtk/class/split_tree.zig +++ b/src/apprt/gtk/class/split_tree.zig @@ -219,7 +219,7 @@ pub const SplitTree = extern struct { // Inherit properly if we were asked to. if (parent_) |p| { if (p.core()) |core| { - surface.setParent(core); + surface.setParent(core, .split); } } @@ -340,6 +340,35 @@ pub const SplitTree = extern struct { const surface = tree.nodes[target.idx()].leaf; surface.grabFocus(); + // We also need to setup our last_focused to this because if we + // trigger a tree change like below, the grab focus above never + // actually triggers in time to set this and this ensures we + // grab focus to the right thing. + const old_last_focused = self.private().last_focused.get(); + defer if (old_last_focused) |v| v.unref(); // unref strong ref from get + self.private().last_focused.set(surface); + errdefer self.private().last_focused.set(old_last_focused); + + if (tree.zoomed != null) { + const app = Application.default(); + const config_obj = app.getConfig(); + defer config_obj.unref(); + const config = config_obj.get(); + + if (!config.@"split-preserve-zoom".navigation) { + tree.zoomed = null; + } else { + tree.zoom(target); + } + + // When the zoom state changes our tree state changes and + // we need to send the proper notifications to trigger + // relayout. + const object = self.as(gobject.Object); + object.notifyByPspec(properties.tree.impl.param_spec); + object.notifyByPspec(properties.@"is-zoomed".impl.param_spec); + } + return true; } @@ -532,7 +561,7 @@ pub const SplitTree = extern struct { )); } - fn getIsSplit(self: *Self) bool { + pub fn getIsSplit(self: *Self) bool { const tree: *const Surface.Tree = self.private().tree orelse &.empty; if (tree.isEmpty()) return false; diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 548ae1a6a..7a1aa4326 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -19,6 +19,7 @@ const terminal = @import("../../../terminal/main.zig"); const CoreSurface = @import("../../../Surface.zig"); const gresource = @import("../build/gresource.zig"); const ext = @import("../ext.zig"); +const gsettings = @import("../gsettings.zig"); const gtk_key = @import("../key.zig"); const ApprtSurface = @import("../Surface.zig"); const Common = @import("../class.zig").Common; @@ -26,6 +27,7 @@ const Application = @import("application.zig").Application; const Config = @import("config.zig").Config; const ResizeOverlay = @import("resize_overlay.zig").ResizeOverlay; const SearchOverlay = @import("search_overlay.zig").SearchOverlay; +const KeyStateOverlay = @import("key_state_overlay.zig").KeyStateOverlay; const ChildExited = @import("surface_child_exited.zig").SurfaceChildExited; const ClipboardConfirmationDialog = @import("clipboard_confirmation_dialog.zig").ClipboardConfirmationDialog; const TitleDialog = @import("surface_title_dialog.zig").SurfaceTitleDialog; @@ -360,6 +362,63 @@ pub const Surface = extern struct { }, ); }; + + pub const @"key-sequence" = struct { + pub const name = "key-sequence"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*ext.StringList, + .{ + .accessor = gobject.ext.typedAccessor( + Self, + ?*ext.StringList, + .{ + .getter = getKeySequence, + .getter_transfer = .full, + }, + ), + }, + ); + }; + + pub const @"key-table" = struct { + pub const name = "key-table"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*ext.StringList, + .{ + .accessor = gobject.ext.typedAccessor( + Self, + ?*ext.StringList, + .{ + .getter = getKeyTable, + .getter_transfer = .full, + }, + ), + }, + ); + }; + + pub const readonly = struct { + pub const name = "readonly"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .default = false, + .accessor = gobject.ext.typedAccessor( + Self, + bool, + .{ + .getter = getReadonly, + }, + ), + }, + ); + }; }; pub const signals = struct { @@ -553,6 +612,9 @@ pub const Surface = extern struct { /// The search overlay search_overlay: *SearchOverlay, + /// The key state overlay + key_state_overlay: *KeyStateOverlay, + /// The apprt Surface. rt_surface: ApprtSurface = undefined, @@ -617,6 +679,10 @@ pub const Surface = extern struct { vscroll_policy: gtk.ScrollablePolicy = .natural, vadj_signal_group: ?*gobject.SignalGroup = null, + // Key state tracking for key sequences and tables + key_sequence: std.ArrayListUnmanaged([:0]const u8) = .empty, + key_tables: std.ArrayListUnmanaged([:0]const u8) = .empty, + // Template binds child_exited_overlay: *ChildExited, context_menu: *gtk.PopoverMenu, @@ -625,6 +691,12 @@ pub const Surface = extern struct { error_page: *adw.StatusPage, terminal_page: *gtk.Overlay, + /// The context for this surface (window, tab, or split) + context: apprt.surface.NewSurfaceContext = .window, + + /// Whether primary paste (middle-click paste) is enabled. + gtk_enable_primary_paste: bool = true, + pub var offset: c_int = 0; }; @@ -650,6 +722,7 @@ pub const Surface = extern struct { pub fn setParent( self: *Self, parent: *CoreSurface, + context: apprt.surface.NewSurfaceContext, ) void { const priv = self.private(); @@ -660,6 +733,9 @@ pub const Surface = extern struct { return; } + // Store the context so initSurface can use it + priv.context = context; + // Setup our font size const font_size_ptr = glib.ext.create(font.face.DesiredSize); errdefer glib.ext.destroy(font_size_ptr); @@ -670,10 +746,8 @@ pub const Surface = extern struct { // Remainder needs a config. If there is no config we just assume // we aren't inheriting any of these values. if (priv.config) |config_obj| { - const config = config_obj.get(); - - // Setup our pwd if configured to inherit - if (config.@"window-inherit-working-directory") { + // Setup our cwd if configured to inherit + if (apprt.surface.shouldInheritWorkingDirectory(context, config_obj.get())) { if (parent.rt_surface.surface.getPwd()) |pwd| { priv.pwd = glib.ext.dupeZ(u8, pwd); self.as(gobject.Object).notifyByPspec(properties.pwd.impl.param_spec); @@ -778,6 +852,74 @@ pub const Surface = extern struct { if (priv.inspector) |v| v.queueRender(); } + /// Handle a key sequence action from the apprt. + pub fn keySequenceAction( + self: *Self, + value: apprt.action.KeySequence, + ) Allocator.Error!void { + const priv = self.private(); + const alloc = Application.default().allocator(); + + self.as(gobject.Object).freezeNotify(); + defer self.as(gobject.Object).thawNotify(); + self.as(gobject.Object).notifyByPspec(properties.@"key-sequence".impl.param_spec); + + switch (value) { + .trigger => |trigger| { + // Convert the trigger to a human-readable label + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + if (gtk_key.labelFromTrigger(&buf.writer, trigger)) |success| { + if (!success) return; + } else |_| return error.OutOfMemory; + + // Make space + try priv.key_sequence.ensureUnusedCapacity(alloc, 1); + + // Copy and append + const duped = try buf.toOwnedSliceSentinel(0); + errdefer alloc.free(duped); + priv.key_sequence.appendAssumeCapacity(duped); + }, + .end => { + // Free all the stored strings and clear + for (priv.key_sequence.items) |s| alloc.free(s); + priv.key_sequence.clearAndFree(alloc); + }, + } + } + + /// Handle a key table action from the apprt. + pub fn keyTableAction( + self: *Self, + value: apprt.action.KeyTable, + ) Allocator.Error!void { + const priv = self.private(); + const alloc = Application.default().allocator(); + + self.as(gobject.Object).freezeNotify(); + defer self.as(gobject.Object).thawNotify(); + self.as(gobject.Object).notifyByPspec(properties.@"key-table".impl.param_spec); + + switch (value) { + .activate => |name| { + // Duplicate the name string and push onto stack + const duped = try alloc.dupeZ(u8, name); + errdefer alloc.free(duped); + try priv.key_tables.append(alloc, duped); + }, + .deactivate => { + // Pop and free the top table + if (priv.key_tables.pop()) |s| alloc.free(s); + }, + .deactivate_all => { + // Free all tables and clear + for (priv.key_tables.items) |s| alloc.free(s); + priv.key_tables.clearAndFree(alloc); + }, + } + } + pub fn showOnScreenKeyboard(self: *Self, event: ?*gdk.Event) bool { const priv = self.private(); return priv.im_context.as(gtk.IMContext).activateOsk(event) != 0; @@ -983,6 +1125,20 @@ pub const Surface = extern struct { return true; } + /// Get the readonly state from the core surface. + pub fn getReadonly(self: *Self) bool { + const priv: *Private = self.private(); + const surface = priv.core_surface orelse return false; + return surface.readonly; + } + + /// Notify anyone interested that the readonly status has changed. + pub fn setReadonly(self: *Self, _: apprt.Action.Value(.readonly)) bool { + self.as(gobject.Object).notifyByPspec(properties.readonly.impl.param_spec); + + return true; + } + /// Key press event (press or release). /// /// At a high level, we want to construct an `input.KeyEvent` and @@ -1392,19 +1548,17 @@ pub const Surface = extern struct { const xft_dpi_scale = xft_scale: { // gtk-xft-dpi is font DPI multiplied by 1024. See // https://docs.gtk.org/gtk4/property.Settings.gtk-xft-dpi.html - const settings = gtk.Settings.getDefault() orelse break :xft_scale 1.0; - var value = std.mem.zeroes(gobject.Value); - defer value.unset(); - _ = value.init(gobject.ext.typeFor(c_int)); - settings.as(gobject.Object).getProperty("gtk-xft-dpi", &value); - const gtk_xft_dpi = value.getInt(); + const gtk_xft_dpi = gsettings.get(.@"gtk-xft-dpi") orelse { + log.warn("gtk-xft-dpi was not set, using default value", .{}); + break :xft_scale 1.0; + }; // Use a value of 1.0 for the XFT DPI scale if the setting is <= 0 // See: // https://gitlab.gnome.org/GNOME/libadwaita/-/commit/a7738a4d269bfdf4d8d5429ca73ccdd9b2450421 // https://gitlab.gnome.org/GNOME/libadwaita/-/commit/9759d3fd81129608dd78116001928f2aed974ead if (gtk_xft_dpi <= 0) { - log.warn("gtk-xft-dpi was not set, using default value", .{}); + log.warn("gtk-xft-dpi has invalid value ({}), using default", .{gtk_xft_dpi}); break :xft_scale 1.0; } @@ -1552,8 +1706,8 @@ pub const Surface = extern struct { self: *Self, clipboard_type: apprt.Clipboard, state: apprt.ClipboardRequest, - ) !void { - try Clipboard.request( + ) !bool { + return try Clipboard.request( self, clipboard_type, state, @@ -1648,6 +1802,9 @@ pub const Surface = extern struct { priv.im_composing = false; priv.im_len = 0; + // Read GTK primary paste setting + priv.gtk_enable_primary_paste = gsettings.get(.@"gtk-enable-primary-paste") orelse true; + // Set up to handle items being dropped on our surface. Files can be dropped // from Nautilus and strings can be dropped from many programs. The order // of these types matter. @@ -1658,13 +1815,7 @@ pub const Surface = extern struct { }; priv.drop_target.setGtypes(&drop_target_types, drop_target_types.len); - // Initialize our GLArea. We only set the values we can't set - // in our blueprint file. - const gl_area = priv.gl_area; - gl_area.setRequiredVersion( - renderer.OpenGL.MIN_VERSION_MAJOR, - renderer.OpenGL.MIN_VERSION_MINOR, - ); + // Setup properties we can't set from our Blueprint file. self.as(gtk.Widget).setCursorFromName("text"); // Initialize our config @@ -1793,6 +1944,14 @@ pub const Surface = extern struct { glib.free(@ptrCast(@constCast(v))); priv.title_override = null; } + + // Clean up key sequence and key table state + const alloc = Application.default().allocator(); + for (priv.key_sequence.items) |s| alloc.free(s); + priv.key_sequence.deinit(alloc); + for (priv.key_tables.items) |s| alloc.free(s); + priv.key_tables.deinit(alloc); + self.clearCgroup(); gobject.Object.virtual_methods.finalize.call( @@ -1879,6 +2038,20 @@ pub const Surface = extern struct { self.as(gobject.Object).notifyByPspec(properties.@"default-size".impl.param_spec); } + /// Get the key sequence list. Full transfer. + fn getKeySequence(self: *Self) ?*ext.StringList { + const priv = self.private(); + const alloc = Application.default().allocator(); + return ext.StringList.create(alloc, priv.key_sequence.items) catch null; + } + + /// Get the key table list. Full transfer. + fn getKeyTable(self: *Self) ?*ext.StringList { + const priv = self.private(); + const alloc = Application.default().allocator(); + return ext.StringList.create(alloc, priv.key_tables.items) catch null; + } + /// Return the min size, if set. pub fn getMinSize(self: *Self) ?*Size { const priv = self.private(); @@ -1956,7 +2129,7 @@ pub const Surface = extern struct { self.as(gobject.Object).notifyByPspec(properties.@"error".impl.param_spec); } - pub fn setSearchActive(self: *Self, active: bool) void { + pub fn setSearchActive(self: *Self, active: bool, needle: [:0]const u8) void { const priv = self.private(); var value = gobject.ext.Value.newFrom(active); defer value.unset(); @@ -1966,6 +2139,10 @@ pub const Surface = extern struct { &value, ); + if (!std.mem.eql(u8, needle, "")) { + priv.search_overlay.setSearchContents(needle); + } + if (active) { priv.search_overlay.grabFocus(); } @@ -2546,6 +2723,11 @@ pub const Surface = extern struct { // Report the event const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton()); + + if (button == .middle and !priv.gtk_enable_primary_paste) { + return; + } + const consumed = consumed: { const gtk_mods = event.getModifierState(); const mods = gtk_key.translateMods(gtk_mods); @@ -2597,6 +2779,10 @@ pub const Surface = extern struct { const gtk_mods = event.getModifierState(); const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton()); + if (button == .middle and !priv.gtk_enable_primary_paste) { + return; + } + const mods = gtk_key.translateMods(gtk_mods); const consumed = surface.mouseButtonCallback( .release, @@ -3076,6 +3262,7 @@ pub const Surface = extern struct { var config = try apprt.surface.newConfig( app.core(), priv.config.?.get(), + priv.context, ); defer config.deinit(); @@ -3242,6 +3429,7 @@ pub const Surface = extern struct { fn init(class: *Class) callconv(.c) void { gobject.ext.ensureType(ResizeOverlay); gobject.ext.ensureType(SearchOverlay); + gobject.ext.ensureType(KeyStateOverlay); gobject.ext.ensureType(ChildExited); gtk.Widget.Class.setTemplateFromResource( class.as(gtk.Widget.Class), @@ -3262,6 +3450,7 @@ pub const Surface = extern struct { class.bindTemplateChildPrivate("progress_bar_overlay", .{}); class.bindTemplateChildPrivate("resize_overlay", .{}); class.bindTemplateChildPrivate("search_overlay", .{}); + class.bindTemplateChildPrivate("key_state_overlay", .{}); class.bindTemplateChildPrivate("terminal_page", .{}); class.bindTemplateChildPrivate("drop_target", .{}); class.bindTemplateChildPrivate("im_context", .{}); @@ -3313,6 +3502,8 @@ pub const Surface = extern struct { properties.@"error".impl, properties.@"font-size-request".impl, properties.focused.impl, + properties.@"key-sequence".impl, + properties.@"key-table".impl, properties.@"min-size".impl, properties.@"mouse-shape".impl, properties.@"mouse-hidden".impl, @@ -3322,6 +3513,7 @@ pub const Surface = extern struct { properties.@"title-override".impl, properties.zoom.impl, properties.@"is-split".impl, + properties.readonly.impl, // For Gtk.Scrollable properties.hadjustment.impl, @@ -3489,16 +3681,30 @@ const Clipboard = struct { /// Request data from the clipboard (read the clipboard). This /// completes asynchronously and will call the `completeClipboardRequest` /// core surface API when done. + /// + /// Returns true if the request was started, false if the clipboard + /// doesn't contain text (allowing performable keybinds to pass through). pub fn request( self: *Surface, clipboard_type: apprt.Clipboard, state: apprt.ClipboardRequest, - ) Allocator.Error!void { + ) Allocator.Error!bool { // Get our requested clipboard const clipboard = get( self.private().gl_area.as(gtk.Widget), clipboard_type, - ) orelse return; + ) orelse return false; + + // For paste requests, check if clipboard has text format available. + // This is a synchronous check that allows performable keybinds to + // pass through when the clipboard contains non-text content (e.g., images). + if (state == .paste) { + const formats = clipboard.getFormats(); + if (formats.containGtype(gobject.ext.types.string) == 0) { + log.debug("clipboard has no text format, not starting paste request", .{}); + return false; + } + } // Allocate our userdata const alloc = Application.default().allocator(); @@ -3518,6 +3724,8 @@ const Clipboard = struct { clipboardReadText, ud, ); + + return true; } /// Paste explicit text directly into the surface, regardless of the diff --git a/src/apprt/gtk/class/tab.zig b/src/apprt/gtk/class/tab.zig index c8b5607a6..ae05cd1ad 100644 --- a/src/apprt/gtk/class/tab.zig +++ b/src/apprt/gtk/class/tab.zig @@ -161,8 +161,12 @@ pub const Tab = extern struct { /// ever created for a tab. If a surface was already created this does /// nothing. pub fn setParent(self: *Self, parent: *CoreSurface) void { + self.setParentWithContext(parent, .tab); + } + + pub fn setParentWithContext(self: *Self, parent: *CoreSurface, context: apprt.surface.NewSurfaceContext) void { if (self.getActiveSurface()) |surface| { - surface.setParent(parent); + surface.setParent(parent, context); } } @@ -347,6 +351,7 @@ pub const Tab = extern struct { switch (mode) { .this => tab_view.closePage(page), .other => tab_view.closeOtherPages(page), + .right => tab_view.closePagesAfter(page), } } diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index c691b84a6..4a16580ef 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -361,10 +361,14 @@ pub const Window = extern struct { /// at the position dictated by the `window-new-tab-position` config. /// The new tab will be selected. pub fn newTab(self: *Self, parent_: ?*CoreSurface) void { - _ = self.newTabPage(parent_); + _ = self.newTabPage(parent_, .tab); } - fn newTabPage(self: *Self, parent_: ?*CoreSurface) *adw.TabPage { + pub fn newTabForWindow(self: *Self, parent_: ?*CoreSurface) void { + _ = self.newTabPage(parent_, .window); + } + + fn newTabPage(self: *Self, parent_: ?*CoreSurface, context: apprt.surface.NewSurfaceContext) *adw.TabPage { const priv = self.private(); const tab_view = priv.tab_view; @@ -372,7 +376,9 @@ pub const Window = extern struct { const tab = gobject.ext.newInstance(Tab, .{ .config = priv.config, }); - if (parent_) |p| tab.setParent(p); + if (parent_) |p| { + tab.setParentWithContext(p, context); + } // Get the position that we should insert the new tab at. const config = if (priv.config) |v| v.get() else { @@ -793,7 +799,7 @@ pub const Window = extern struct { /// Get the currently active surface. See the "active-surface" property. /// This does not ref the value. - fn getActiveSurface(self: *Self) ?*Surface { + pub fn getActiveSurface(self: *Self) ?*Surface { const tab = self.getSelectedTab() orelse return null; return tab.getActiveSurface(); } @@ -804,6 +810,11 @@ pub const Window = extern struct { return self.private().config; } + /// Get the tab view for this window. + pub fn getTabView(self: *Self) *adw.TabView { + return self.private().tab_view; + } + /// Get the current window decoration value for this window. pub fn getWindowDecoration(self: *Self) configpkg.WindowDecoration { const priv = self.private(); @@ -1231,7 +1242,7 @@ pub const Window = extern struct { _: *adw.TabOverview, self: *Self, ) callconv(.c) *adw.TabPage { - return self.newTabPage(if (self.getActiveSurface()) |v| v.core() else null); + return self.newTabPage(if (self.getActiveSurface()) |v| v.core() else null, .tab); } fn tabOverviewOpen( diff --git a/src/apprt/gtk/css/style.css b/src/apprt/gtk/css/style.css index 938d23ad8..9c0f115f1 100644 --- a/src/apprt/gtk/css/style.css +++ b/src/apprt/gtk/css/style.css @@ -46,6 +46,18 @@ label.url-overlay.right { outline-width: 1px; } +/* + * GhosttySurface key state overlay + */ +.key-state-overlay { + padding: 6px 10px; + margin: 8px; + border-radius: 8px; + outline-style: solid; + outline-color: #555555; + outline-width: 1px; +} + /* * GhosttySurface resize overlay */ @@ -122,6 +134,16 @@ label.resize-overlay { border-style: solid; } +.surface .readonly_overlay { + /* Should be the equivalent of the following SwiftUI color: */ + /* Color(hue: 0.08, saturation: 0.5, brightness: 0.8) */ + color: hsl(25 50 75); + padding: 8px 8px 8px 8px; + margin: 8px 8px 8px 8px; + border-radius: 6px 6px 6px 6px; + outline-style: solid; + outline-width: 1px; +} /* * Command Palette */ diff --git a/src/apprt/gtk/ext.zig b/src/apprt/gtk/ext.zig index 9b1eeecc6..df9ab4ea2 100644 --- a/src/apprt/gtk/ext.zig +++ b/src/apprt/gtk/ext.zig @@ -12,6 +12,8 @@ const gobject = @import("gobject"); const gtk = @import("gtk"); pub const actions = @import("ext/actions.zig"); +const slice = @import("ext/slice.zig"); +pub const StringList = slice.StringList; /// Wrapper around `gobject.boxedCopy` to copy a boxed type `T`. pub fn boxedCopy(comptime T: type, ptr: *const T) *T { @@ -64,4 +66,5 @@ pub fn gValueHolds(value_: ?*const gobject.Value, g_type: gobject.Type) bool { test { _ = actions; + _ = slice; } diff --git a/src/apprt/gtk/ext/slice.zig b/src/apprt/gtk/ext/slice.zig new file mode 100644 index 000000000..49ad63d85 --- /dev/null +++ b/src/apprt/gtk/ext/slice.zig @@ -0,0 +1,111 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const glib = @import("glib"); +const gobject = @import("gobject"); + +/// A boxed type that holds a list of string slices. +pub const StringList = struct { + arena: ArenaAllocator, + strings: []const [:0]const u8, + + pub fn create( + alloc: Allocator, + strings: []const [:0]const u8, + ) Allocator.Error!*StringList { + var arena: ArenaAllocator = .init(alloc); + errdefer arena.deinit(); + const arena_alloc = arena.allocator(); + var stored = try arena_alloc.alloc([:0]const u8, strings.len); + for (strings, 0..) |s, i| stored[i] = try arena_alloc.dupeZ(u8, s); + + const ptr = try alloc.create(StringList); + errdefer alloc.destroy(ptr); + ptr.* = .{ .arena = arena, .strings = stored }; + + return ptr; + } + + pub fn deinit(self: *StringList) void { + self.arena.deinit(); + } + + pub fn destroy(self: *StringList) void { + const alloc = self.arena.child_allocator; + self.deinit(); + alloc.destroy(self); + } + + /// Returns the general-purpose allocator used by this StringList. + pub fn allocator(self: *const StringList) Allocator { + return self.arena.child_allocator; + } + + pub const getGObjectType = gobject.ext.defineBoxed( + StringList, + .{ + .name = "GhosttyStringList", + .funcs = .{ + .copy = &struct { + fn copy(self: *StringList) callconv(.c) *StringList { + return StringList.create( + self.arena.child_allocator, + self.strings, + ) catch @panic("OOM"); + } + }.copy, + .free = &struct { + fn free(self: *StringList) callconv(.c) void { + self.destroy(); + } + }.free, + }, + }, + ); +}; + +test "StringList create and destroy" { + const testing = std.testing; + const alloc = testing.allocator; + + const input: []const [:0]const u8 = &.{ "hello", "world" }; + const list = try StringList.create(alloc, input); + defer list.destroy(); + + try testing.expectEqual(@as(usize, 2), list.strings.len); + try testing.expectEqualStrings("hello", list.strings[0]); + try testing.expectEqualStrings("world", list.strings[1]); +} + +test "StringList create empty list" { + const testing = std.testing; + const alloc = testing.allocator; + + const input: []const [:0]const u8 = &.{}; + const list = try StringList.create(alloc, input); + defer list.destroy(); + + try testing.expectEqual(@as(usize, 0), list.strings.len); +} + +test "StringList boxedCopy and boxedFree" { + const testing = std.testing; + const alloc = testing.allocator; + + const input: []const [:0]const u8 = &.{ "foo", "bar", "baz" }; + const original = try StringList.create(alloc, input); + defer original.destroy(); + + const copied: *StringList = @ptrCast(@alignCast(gobject.boxedCopy( + StringList.getGObjectType(), + original, + ))); + defer gobject.boxedFree(StringList.getGObjectType(), copied); + + try testing.expectEqual(@as(usize, 3), copied.strings.len); + try testing.expectEqualStrings("foo", copied.strings[0]); + try testing.expectEqualStrings("bar", copied.strings[1]); + try testing.expectEqualStrings("baz", copied.strings[2]); + + try testing.expect(original.strings.ptr != copied.strings.ptr); +} diff --git a/src/apprt/gtk/gsettings.zig b/src/apprt/gtk/gsettings.zig new file mode 100644 index 000000000..8cf7f12d2 --- /dev/null +++ b/src/apprt/gtk/gsettings.zig @@ -0,0 +1,101 @@ +const std = @import("std"); +const gtk = @import("gtk"); +const gobject = @import("gobject"); + +/// GTK Settings keys with well-defined types. +pub const Key = enum { + @"gtk-enable-primary-paste", + @"gtk-xft-dpi", + @"gtk-font-name", + + fn Type(comptime self: Key) type { + return switch (self) { + .@"gtk-enable-primary-paste" => bool, + .@"gtk-xft-dpi" => c_int, + .@"gtk-font-name" => []const u8, + }; + } + + fn GValueType(comptime self: Key) type { + return switch (self.Type()) { + bool => c_int, + c_int => c_int, + []const u8 => ?[*:0]const u8, + else => @compileError("Unsupported type for GTK settings"), + }; + } + + /// Returns true if this setting type requires memory allocation. + /// Types that do not need allocation must be explicitly marked. + fn requiresAllocation(comptime self: Key) bool { + const T = self.Type(); + return switch (T) { + bool, c_int => false, + else => true, + }; + } +}; + +/// Reads a GTK setting for non-allocating types. +/// Automatically uses XDG Desktop Portal in Flatpak environments. +/// Returns null if the setting is unavailable. +pub fn get(comptime key: Key) ?key.Type() { + if (comptime key.requiresAllocation()) { + @compileError("Allocating types require an allocator; use getAlloc() instead"); + } + const settings = gtk.Settings.getDefault() orelse return null; + return getImpl(settings, null, key) catch unreachable; +} + +/// Reads a GTK setting, allocating memory if necessary. +/// Automatically uses XDG Desktop Portal in Flatpak environments. +/// Caller must free returned memory with the provided allocator. +/// Returns null if the setting is unavailable. +pub fn getAlloc(allocator: std.mem.Allocator, comptime key: Key) !?key.Type() { + const settings = gtk.Settings.getDefault() orelse return null; + return getImpl(settings, allocator, key); +} + +fn getImpl(settings: *gtk.Settings, allocator: ?std.mem.Allocator, comptime key: Key) !?key.Type() { + const GValType = key.GValueType(); + var value = gobject.ext.Value.new(GValType); + defer value.unset(); + + settings.as(gobject.Object).getProperty(@tagName(key).ptr, &value); + + return switch (key.Type()) { + bool => value.getInt() != 0, + c_int => value.getInt(), + []const u8 => blk: { + const alloc = allocator.?; + const ptr = value.getString() orelse break :blk null; + const str = std.mem.span(ptr); + break :blk try alloc.dupe(u8, str); + }, + else => @compileError("Unsupported type for GTK settings"), + }; +} + +test "Key.Type returns correct types" { + try std.testing.expectEqual(bool, Key.@"gtk-enable-primary-paste".Type()); + try std.testing.expectEqual(c_int, Key.@"gtk-xft-dpi".Type()); + try std.testing.expectEqual([]const u8, Key.@"gtk-font-name".Type()); +} + +test "Key.requiresAllocation identifies allocating types" { + try std.testing.expectEqual(false, Key.@"gtk-enable-primary-paste".requiresAllocation()); + try std.testing.expectEqual(false, Key.@"gtk-xft-dpi".requiresAllocation()); + try std.testing.expectEqual(true, Key.@"gtk-font-name".requiresAllocation()); +} + +test "Key.GValueType returns correct GObject types" { + try std.testing.expectEqual(c_int, Key.@"gtk-enable-primary-paste".GValueType()); + try std.testing.expectEqual(c_int, Key.@"gtk-xft-dpi".GValueType()); + try std.testing.expectEqual(?[*:0]const u8, Key.@"gtk-font-name".GValueType()); +} + +test "@tagName returns correct GTK property names" { + try std.testing.expectEqualStrings("gtk-enable-primary-paste", @tagName(Key.@"gtk-enable-primary-paste")); + try std.testing.expectEqualStrings("gtk-xft-dpi", @tagName(Key.@"gtk-xft-dpi")); + try std.testing.expectEqualStrings("gtk-font-name", @tagName(Key.@"gtk-font-name")); +} diff --git a/src/apprt/gtk/key.zig b/src/apprt/gtk/key.zig index 19bdc8315..5f717e14a 100644 --- a/src/apprt/gtk/key.zig +++ b/src/apprt/gtk/key.zig @@ -74,6 +74,8 @@ fn writeTriggerKey( try writer.print("{u}", .{cp}); } }, + + .catch_all => return false, } return true; @@ -231,6 +233,70 @@ pub fn keyvalFromKey(key: input.Key) ?c_uint { } } +/// Converts a trigger to a human-readable label for display in UI. +/// +/// Uses GTK accelerator-style formatting (e.g., "Ctrl+Shift+A"). +/// Returns false if the trigger cannot be formatted (e.g., catch_all). +pub fn labelFromTrigger( + writer: *std.Io.Writer, + trigger: input.Binding.Trigger, +) std.Io.Writer.Error!bool { + // Modifiers first, using human-readable format + if (trigger.mods.super) try writer.writeAll("Super+"); + if (trigger.mods.ctrl) try writer.writeAll("Ctrl+"); + if (trigger.mods.alt) try writer.writeAll("Alt+"); + if (trigger.mods.shift) try writer.writeAll("Shift+"); + + // Write the key + return writeTriggerKeyLabel(writer, trigger); +} + +/// Writes the key portion of a trigger in human-readable format. +fn writeTriggerKeyLabel( + writer: *std.Io.Writer, + trigger: input.Binding.Trigger, +) error{WriteFailed}!bool { + switch (trigger.key) { + .physical => |k| { + const keyval = keyvalFromKey(k) orelse return false; + const name = gdk.keyvalName(keyval) orelse return false; + // Capitalize the first letter for nicer display + const span = std.mem.span(name); + if (span.len > 0) { + if (span[0] >= 'a' and span[0] <= 'z') { + try writer.writeByte(span[0] - 'a' + 'A'); + if (span.len > 1) try writer.writeAll(span[1..]); + } else { + try writer.writeAll(span); + } + } + }, + + .unicode => |cp| { + // Try to get a nice name from GDK first + if (gdk.keyvalName(cp)) |name| { + const span = std.mem.span(name); + if (span.len > 0) { + // Capitalize the first letter for nicer display + if (span[0] >= 'a' and span[0] <= 'z') { + try writer.writeByte(span[0] - 'a' + 'A'); + if (span.len > 1) try writer.writeAll(span[1..]); + } else { + try writer.writeAll(span); + } + } + } else { + // Fall back to printing the character + try writer.print("{u}", .{cp}); + } + }, + + .catch_all => return false, + } + + return true; +} + test "accelFromTrigger" { const testing = std.testing; var buf: [256]u8 = undefined; @@ -261,6 +327,64 @@ test "xdgShortcutFromTrigger" { })).?); } +test "labelFromTrigger" { + const testing = std.testing; + + // Simple unicode key with modifier + { + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + try testing.expect(try labelFromTrigger(&buf.writer, .{ + .mods = .{ .super = true }, + .key = .{ .unicode = 'q' }, + })); + try testing.expectEqualStrings("Super+Q", buf.written()); + } + + // Multiple modifiers + { + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + try testing.expect(try labelFromTrigger(&buf.writer, .{ + .mods = .{ .ctrl = true, .alt = true, .super = true, .shift = true }, + .key = .{ .unicode = 92 }, + })); + try testing.expectEqualStrings("Super+Ctrl+Alt+Shift+Backslash", buf.written()); + } + + // Physical key + { + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + try testing.expect(try labelFromTrigger(&buf.writer, .{ + .mods = .{ .ctrl = true }, + .key = .{ .physical = .key_a }, + })); + try testing.expectEqualStrings("Ctrl+A", buf.written()); + } + + // No modifiers + { + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + try testing.expect(try labelFromTrigger(&buf.writer, .{ + .mods = .{}, + .key = .{ .physical = .escape }, + })); + try testing.expectEqualStrings("Escape", buf.written()); + } + + // catch_all returns false + { + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + try testing.expect(!try labelFromTrigger(&buf.writer, .{ + .mods = .{}, + .key = .catch_all, + })); + } +} + /// A raw entry in the keymap. Our keymap contains mappings between /// GDK keys and our own key enum. const RawEntry = struct { c_uint, input.Key }; diff --git a/src/apprt/gtk/ui/1.2/key-state-overlay.blp b/src/apprt/gtk/ui/1.2/key-state-overlay.blp new file mode 100644 index 000000000..c8654bfbb --- /dev/null +++ b/src/apprt/gtk/ui/1.2/key-state-overlay.blp @@ -0,0 +1,58 @@ +using Gtk 4.0; +using Adw 1; + +template $GhosttyKeyStateOverlay: Adw.Bin { + visible: bind $has_state(template.has-tables, template.has-sequence) as ; + valign-target: end; + halign: center; + valign: bind template.valign-target; + + GestureDrag { + button: 1; + propagation-phase: capture; + drag-end => $on_drag_end(); + } + + Adw.Bin { + Box container { + styles [ + "background", + "key-state-overlay", + ] + + orientation: horizontal; + spacing: 6; + + Image { + icon-name: "input-keyboard-symbolic"; + pixel-size: 16; + } + + Label tables_label { + visible: bind template.has-tables; + label: bind $tables_text(template.tables) as ; + xalign: 0.0; + } + + Label chevron_label { + visible: bind $show_chevron(template.has-tables, template.has-sequence) as ; + label: "›"; + + styles [ + "dim-label", + ] + } + + Label sequence_label { + visible: bind template.has-sequence; + label: bind $sequence_text(template.sequence) as ; + xalign: 0.0; + } + + Spinner pending_spinner { + visible: bind template.has-sequence; + spinning: bind template.has-sequence; + } + } + } +} diff --git a/src/apprt/gtk/ui/1.2/surface.blp b/src/apprt/gtk/ui/1.2/surface.blp index 4ebfeabfb..dd6ded5de 100644 --- a/src/apprt/gtk/ui/1.2/surface.blp +++ b/src/apprt/gtk/ui/1.2/surface.blp @@ -71,6 +71,39 @@ Overlay terminal_page { } }; + [overlay] + Revealer { + reveal-child: bind template.readonly; + transition-type: crossfade; + transition-duration: 500; + // Revealers take up the full size, we need this to not capture events. + can-focus: false; + can-target: false; + focusable: false; + + Box readonly_overlay { + styles [ + "readonly_overlay", + ] + + // TODO: the tooltip doesn't actually work, but keep it here for now so + // that we can get the tooltip text translated. + has-tooltip: true; + tooltip-text: _("This terminal is in read-only mode. You can still view, select, and scroll through the content, but no input events will be sent to the running application."); + halign: end; + valign: start; + spacing: 6; + + Image { + icon-name: "changes-prevent-symbolic"; + } + + Label { + label: _("Read-only"); + } + } + } + [overlay] ProgressBar progress_bar_overlay { styles [ @@ -155,6 +188,12 @@ Overlay terminal_page { previous-match => $search_previous_match(); } + [overlay] + $GhosttyKeyStateOverlay key_state_overlay { + tables: bind template.key-table; + sequence: bind template.key-sequence; + } + [overlay] // Apply unfocused-split-fill and unfocused-split-opacity to current surface // this is only applied when a tab has more than one surface diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig index 9dc273563..1e73c6139 100644 --- a/src/apprt/gtk/winproto/x11.zig +++ b/src/apprt/gtk/winproto/x11.zig @@ -173,6 +173,12 @@ pub const Window = struct { blur_region: Region = .{}, + // Cache last applied values to avoid redundant X11 property updates. + // Redundant property updates seem to cause some visual glitches + // with some window managers: https://github.com/ghostty-org/ghostty/pull/8075 + last_applied_blur_region: ?Region = null, + last_applied_decoration_hints: ?MotifWMHints = null, + pub fn init( alloc: Allocator, app: *App, @@ -255,30 +261,42 @@ pub const Window = struct { const gtk_widget = self.apprt_window.as(gtk.Widget); const config = if (self.apprt_window.getConfig()) |v| v.get() else return; + // When blur is disabled, remove the property if it was previously set + const blur = config.@"background-blur"; + if (!blur.enabled()) { + if (self.last_applied_blur_region != null) { + try self.deleteProperty(self.app.atoms.kde_blur); + self.last_applied_blur_region = null; + } + + return; + } + // Transform surface coordinates to device coordinates. const scale = gtk_widget.getScaleFactor(); self.blur_region.width = gtk_widget.getWidth() * scale; self.blur_region.height = gtk_widget.getHeight() * scale; - const blur = config.@"background-blur"; + // Only update X11 properties when the blur region actually changes + if (self.last_applied_blur_region) |last| { + if (std.meta.eql(self.blur_region, last)) return; + } + log.debug("set blur={}, window xid={}, region={}", .{ blur, self.x11_surface.getXid(), self.blur_region, }); - if (blur.enabled()) { - try self.changeProperty( - Region, - self.app.atoms.kde_blur, - c.XA_CARDINAL, - ._32, - .{ .mode = .replace }, - &self.blur_region, - ); - } else { - try self.deleteProperty(self.app.atoms.kde_blur); - } + try self.changeProperty( + Region, + self.app.atoms.kde_blur, + c.XA_CARDINAL, + ._32, + .{ .mode = .replace }, + &self.blur_region, + ); + self.last_applied_blur_region = self.blur_region; } fn syncDecorations(self: *Window) !void { @@ -307,6 +325,11 @@ pub const Window = struct { .auto, .client, .none => false, }; + // Only update decoration hints when they actually change + if (self.last_applied_decoration_hints) |last| { + if (std.meta.eql(hints, last)) return; + } + try self.changeProperty( MotifWMHints, self.app.atoms.motif_wm_hints, @@ -315,6 +338,7 @@ pub const Window = struct { .{ .mode = .replace }, &hints, ); + self.last_applied_decoration_hints = hints; } pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void { diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 45a847493..5c25281c8 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -63,11 +63,6 @@ pub const Message = union(enum) { /// Health status change for the renderer. renderer_health: renderer.Health, - /// Report the color scheme. The bool parameter is whether to force or not. - /// If force is true, the color scheme should be reported even if mode - /// 2031 is not set. - report_color_scheme: bool, - /// Tell the surface to present itself to the user. This may require raising /// a window and switching tabs. present_surface: void, @@ -159,12 +154,28 @@ pub const Mailbox = struct { } }; +/// Context for new surface creation to determine inheritance behavior +pub const NewSurfaceContext = enum(c_int) { + window = 0, + tab = 1, + split = 2, +}; + +pub fn shouldInheritWorkingDirectory(context: NewSurfaceContext, config: *const Config) bool { + return switch (context) { + .window => config.@"window-inherit-working-directory", + .tab => config.@"tab-inherit-working-directory", + .split => config.@"split-inherit-working-directory", + }; +} + /// Returns a new config for a surface for the given app that should be /// used for any new surfaces. The resulting config should be deinitialized /// after the surface is initialized. pub fn newConfig( app: *const App, config: *const Config, + context: NewSurfaceContext, ) Allocator.Error!Config { // Create a shallow clone var copy = config.shallowClone(app.alloc); @@ -175,7 +186,7 @@ pub fn newConfig( // Get our previously focused surface for some inherited values. const prev = app.focusedSurface(); if (prev) |p| { - if (config.@"window-inherit-working-directory") { + if (shouldInheritWorkingDirectory(context, config)) { if (try p.pwd(alloc)) |pwd| { copy.@"working-directory" = pwd; } diff --git a/src/benchmark/OscParser.zig b/src/benchmark/OscParser.zig new file mode 100644 index 000000000..d4b416de8 --- /dev/null +++ b/src/benchmark/OscParser.zig @@ -0,0 +1,118 @@ +//! This benchmark tests the throughput of the OSC parser. +const OscParser = @This(); + +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const Benchmark = @import("Benchmark.zig"); +const options = @import("options.zig"); +const Parser = @import("../terminal/osc.zig").Parser; +const log = std.log.scoped(.@"osc-parser-bench"); + +opts: Options, + +/// The file, opened in the setup function. +data_f: ?std.fs.File = null, + +parser: Parser, + +pub const Options = struct { + /// The data to read as a filepath. If this is "-" then + /// we will read stdin. If this is unset, then we will + /// do nothing (benchmark is a noop). It'd be more unixy to + /// use stdin by default but I find that a hanging CLI command + /// with no interaction is a bit annoying. + data: ?[]const u8 = null, +}; + +/// Create a new terminal stream handler for the given arguments. +pub fn create( + alloc: Allocator, + opts: Options, +) !*OscParser { + const ptr = try alloc.create(OscParser); + errdefer alloc.destroy(ptr); + ptr.* = .{ + .opts = opts, + .data_f = null, + .parser = .init(alloc), + }; + return ptr; +} + +pub fn destroy(self: *OscParser, alloc: Allocator) void { + self.parser.deinit(); + alloc.destroy(self); +} + +pub fn benchmark(self: *OscParser) Benchmark { + return .init(self, .{ + .stepFn = step, + .setupFn = setup, + .teardownFn = teardown, + }); +} + +fn setup(ptr: *anyopaque) Benchmark.Error!void { + const self: *OscParser = @ptrCast(@alignCast(ptr)); + + // Open our data file to prepare for reading. We can do more + // validation here eventually. + assert(self.data_f == null); + self.data_f = options.dataFile(self.opts.data) catch |err| { + log.warn("error opening data file err={}", .{err}); + return error.BenchmarkFailed; + }; + self.parser.reset(); +} + +fn teardown(ptr: *anyopaque) void { + const self: *OscParser = @ptrCast(@alignCast(ptr)); + if (self.data_f) |f| { + f.close(); + self.data_f = null; + } +} + +fn step(ptr: *anyopaque) Benchmark.Error!void { + const self: *OscParser = @ptrCast(@alignCast(ptr)); + + const f = self.data_f orelse return; + var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined; + var r = f.reader(&read_buf); + + var osc_buf: [4096]u8 align(std.atomic.cache_line) = undefined; + while (true) { + r.interface.fill(@bitSizeOf(usize) / 8) catch |err| switch (err) { + error.EndOfStream => return, + error.ReadFailed => return error.BenchmarkFailed, + }; + const len = r.interface.takeInt(usize, .little) catch |err| switch (err) { + error.EndOfStream => return, + error.ReadFailed => return error.BenchmarkFailed, + }; + + if (len > osc_buf.len) return error.BenchmarkFailed; + + r.interface.readSliceAll(osc_buf[0..len]) catch |err| switch (err) { + error.EndOfStream => return, + error.ReadFailed => return error.BenchmarkFailed, + }; + + for (osc_buf[0..len]) |c| @call(.always_inline, Parser.next, .{ &self.parser, c }); + std.mem.doNotOptimizeAway(self.parser.end(std.ascii.control_code.bel)); + self.parser.reset(); + } +} + +test OscParser { + const testing = std.testing; + const alloc = testing.allocator; + + const impl: *OscParser = try .create(alloc, .{}); + defer impl.destroy(alloc); + + const bench = impl.benchmark(); + _ = try bench.run(.once); +} diff --git a/src/benchmark/cli.zig b/src/benchmark/cli.zig index 816ecd3f6..13f070774 100644 --- a/src/benchmark/cli.zig +++ b/src/benchmark/cli.zig @@ -12,6 +12,7 @@ pub const Action = enum { @"terminal-parser", @"terminal-stream", @"is-symbol", + @"osc-parser", /// Returns the struct associated with the action. The struct /// should have a few decls: @@ -29,6 +30,7 @@ pub const Action = enum { .@"grapheme-break" => @import("GraphemeBreak.zig"), .@"terminal-parser" => @import("TerminalParser.zig"), .@"is-symbol" => @import("IsSymbol.zig"), + .@"osc-parser" => @import("OscParser.zig"), }; } }; diff --git a/src/build/Config.zig b/src/build/Config.zig index e88213d71..3a8a4e0c7 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -218,6 +218,16 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config { try std.SemanticVersion.parse(v) else version: { const app_version = try std.SemanticVersion.parse(appVersion); + + // Is ghostty a dependency? If so, skip git detection. + // @src().file won't resolve from b.build_root unless ghostty + // is the project being built. + b.build_root.handle.access(@src().file, .{}) catch break :version .{ + .major = app_version.major, + .minor = app_version.minor, + .patch = app_version.patch, + }; + // If no explicit version is given, we try to detect it from git. const vsn = GitVersion.detect(b) catch |err| switch (err) { // If Git isn't available we just make an unknown dev version. diff --git a/src/build/GhosttyXcodebuild.zig b/src/build/GhosttyXcodebuild.zig index 27691d744..5ca4c5e9a 100644 --- a/src/build/GhosttyXcodebuild.zig +++ b/src/build/GhosttyXcodebuild.zig @@ -151,7 +151,7 @@ pub fn init( // This overrides our default behavior and forces logs to show // up on stderr (in addition to the centralized macOS log). - open.setEnvironmentVariable("GHOSTTY_LOG", "1"); + open.setEnvironmentVariable("GHOSTTY_LOG", "stderr,macos"); // Configure how we're launching open.setEnvironmentVariable("GHOSTTY_MAC_LAUNCH_SOURCE", "zig_run"); diff --git a/src/build/GhosttyZig.zig b/src/build/GhosttyZig.zig index a8d2726bc..e63120e74 100644 --- a/src/build/GhosttyZig.zig +++ b/src/build/GhosttyZig.zig @@ -73,6 +73,9 @@ fn initVt( // We always need unicode tables deps.unicode_tables.addModuleImport(vt); + // We need uucode for grapheme break support + deps.addUucode(b, vt, cfg.target, cfg.optimize); + // If SIMD is enabled, add all our SIMD dependencies. if (cfg.simd) { try SharedDeps.addSimd(b, vt, null); diff --git a/src/build/GitVersion.zig b/src/build/GitVersion.zig index bfa9af821..566fec2e9 100644 --- a/src/build/GitVersion.zig +++ b/src/build/GitVersion.zig @@ -19,14 +19,22 @@ branch: []const u8, pub fn detect(b: *std.Build) !Version { // Execute a bunch of git commands to determine the automatic version. var code: u8 = 0; - const branch: []const u8 = b.runAllowFail( - &[_][]const u8{ "git", "-C", b.build_root.path orelse ".", "rev-parse", "--abbrev-ref", "HEAD" }, - &code, - .Ignore, - ) catch |err| switch (err) { - error.FileNotFound => return error.GitNotFound, - error.ExitCodeFailure => return error.GitNotRepository, - else => return err, + const branch: []const u8 = b: { + const tmp: []u8 = b.runAllowFail( + &[_][]const u8{ "git", "-C", b.build_root.path orelse ".", "rev-parse", "--abbrev-ref", "HEAD" }, + &code, + .Ignore, + ) catch |err| switch (err) { + error.FileNotFound => return error.GitNotFound, + error.ExitCodeFailure => return error.GitNotRepository, + else => return err, + }; + // Replace any '/' with '-' as including slashes will mess up building + // the dist tarball - the tarball uses the branch as part of the + // name and including slashes means that the tarball will end up in + // subdirectories instead of where it's supposed to be. + std.mem.replaceScalar(u8, tmp, '/', '-'); + break :b tmp; }; const short_hash = short_hash: { diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index dfa676bba..0ca43e78d 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -135,29 +135,28 @@ pub fn add( // Every exe needs the terminal options self.config.terminalOptions().add(b, step.root_module); - // Freetype + // Freetype. We always include this even if our font backend doesn't + // use it because Dear Imgui uses Freetype. _ = b.systemIntegrationOption("freetype", .{}); // Shows it in help - if (self.config.font_backend.hasFreetype()) { - if (b.lazyDependency("freetype", .{ - .target = target, - .optimize = optimize, - .@"enable-libpng" = true, - })) |freetype_dep| { - step.root_module.addImport( - "freetype", - freetype_dep.module("freetype"), - ); + if (b.lazyDependency("freetype", .{ + .target = target, + .optimize = optimize, + .@"enable-libpng" = true, + })) |freetype_dep| { + step.root_module.addImport( + "freetype", + freetype_dep.module("freetype"), + ); - if (b.systemIntegrationOption("freetype", .{})) { - step.linkSystemLibrary2("bzip2", dynamic_link_opts); - step.linkSystemLibrary2("freetype2", dynamic_link_opts); - } else { - step.linkLibrary(freetype_dep.artifact("freetype")); - try static_libs.append( - b.allocator, - freetype_dep.artifact("freetype").getEmittedBin(), - ); - } + if (b.systemIntegrationOption("freetype", .{})) { + step.linkSystemLibrary2("bzip2", dynamic_link_opts); + step.linkSystemLibrary2("freetype2", dynamic_link_opts); + } else { + step.linkLibrary(freetype_dep.artifact("freetype")); + try static_libs.append( + b.allocator, + freetype_dep.artifact("freetype").getEmittedBin(), + ); } } @@ -413,14 +412,7 @@ pub fn add( })) |dep| { step.root_module.addImport("z2d", dep.module("z2d")); } - if (b.lazyDependency("uucode", .{ - .target = target, - .optimize = optimize, - .tables_path = self.uucode_tables, - .build_config_path = b.path("src/build/uucode_config.zig"), - })) |dep| { - step.root_module.addImport("uucode", dep.module("uucode")); - } + self.addUucode(b, step.root_module, target, optimize); if (b.lazyDependency("zf", .{ .target = target, .optimize = optimize, @@ -479,15 +471,19 @@ pub fn add( } // cimgui - if (b.lazyDependency("cimgui", .{ + if (b.lazyDependency("dcimgui", .{ .target = target, .optimize = optimize, - })) |cimgui_dep| { - step.root_module.addImport("cimgui", cimgui_dep.module("cimgui")); - step.linkLibrary(cimgui_dep.artifact("cimgui")); + .freetype = true, + .@"backend-metal" = target.result.os.tag.isDarwin(), + .@"backend-osx" = target.result.os.tag == .macos, + .@"backend-opengl3" = target.result.os.tag != .macos, + })) |dep| { + step.root_module.addImport("dcimgui", dep.module("dcimgui")); + step.linkLibrary(dep.artifact("dcimgui")); try static_libs.append( b.allocator, - cimgui_dep.artifact("cimgui").getEmittedBin(), + dep.artifact("dcimgui").getEmittedBin(), ); } @@ -719,15 +715,19 @@ pub fn addSimd( } // Highway - if (b.lazyDependency("highway", .{ - .target = target, - .optimize = optimize, - })) |highway_dep| { - m.linkLibrary(highway_dep.artifact("highway")); - if (static_libs) |v| try v.append( - b.allocator, - highway_dep.artifact("highway").getEmittedBin(), - ); + if (b.systemIntegrationOption("highway", .{ .default = false })) { + m.linkSystemLibrary("libhwy", dynamic_link_opts); + } else { + if (b.lazyDependency("highway", .{ + .target = target, + .optimize = optimize, + })) |highway_dep| { + m.linkLibrary(highway_dep.artifact("highway")); + if (static_libs) |v| try v.append( + b.allocator, + highway_dep.artifact("highway").getEmittedBin(), + ); + } } // utfcpp - This is used as a dependency on our hand-written C++ code @@ -746,6 +746,7 @@ pub fn addSimd( m.addIncludePath(b.path("src")); { // From hwy/detect_targets.h + const HWY_AVX10_2: c_int = 1 << 3; const HWY_AVX3_SPR: c_int = 1 << 4; const HWY_AVX3_ZEN4: c_int = 1 << 6; const HWY_AVX3_DL: c_int = 1 << 7; @@ -756,7 +757,7 @@ pub fn addSimd( // The performance difference between AVX2 and AVX512 is not // significant for our use case and AVX512 is very rare on consumer // hardware anyways. - const HWY_DISABLED_TARGETS: c_int = HWY_AVX3_SPR | HWY_AVX3_ZEN4 | HWY_AVX3_DL | HWY_AVX3; + const HWY_DISABLED_TARGETS: c_int = HWY_AVX10_2 | HWY_AVX3_SPR | HWY_AVX3_ZEN4 | HWY_AVX3_DL | HWY_AVX3; m.addCSourceFiles(.{ .files = &.{ @@ -870,6 +871,23 @@ pub fn gtkNgDistResources( }; } +pub fn addUucode( + self: *const SharedDeps, + b: *std.Build, + module: *std.Build.Module, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, +) void { + if (b.lazyDependency("uucode", .{ + .target = target, + .optimize = optimize, + .tables_path = self.uucode_tables, + .build_config_path = b.path("src/build/uucode_config.zig"), + })) |dep| { + module.addImport("uucode", dep.module("uucode")); + } +} + // For dynamic linking, we prefer dynamic linking and to search by // mode first. Mode first will search all paths for a dynamic library // before falling back to static. diff --git a/src/build/mdgen/ghostty_1_footer.md b/src/build/mdgen/ghostty_1_footer.md index 88aa16273..a63a85fd4 100644 --- a/src/build/mdgen/ghostty_1_footer.md +++ b/src/build/mdgen/ghostty_1_footer.md @@ -37,6 +37,19 @@ precedence over the XDG environment locations. : **WINDOWS ONLY:** alternate location to search for configuration files. +**GHOSTTY_LOG** + +: The `GHOSTTY_LOG` environment variable can be used to control which +destinations receive logs. Ghostty currently defines two destinations: + +: - `stderr` - logging to `stderr`. +: - `macos` - logging to macOS's unified log (has no effect on non-macOS platforms). + +: Combine values with a comma to enable multiple destinations. Prefix a +destination with `no-` to disable it. Enabling and disabling destinations +can be done at the same time. Setting `GHOSTTY_LOG` to `true` will enable all +destinations. Setting `GHOSTTY_LOG` to `false` will disable all destinations. + # BUGS See GitHub issues: diff --git a/src/build/mdgen/ghostty_5_header.md b/src/build/mdgen/ghostty_5_header.md index b9d4cb751..ce3196eb6 100644 --- a/src/build/mdgen/ghostty_5_header.md +++ b/src/build/mdgen/ghostty_5_header.md @@ -73,16 +73,12 @@ the public config files of many Ghostty users for examples and inspiration. ## Configuration Errors If your configuration file has any errors, Ghostty does its best to ignore -them and move on. Configuration errors currently show up in the log. The log -is written directly to stderr, so it is up to you to figure out how to access -that for your system (for now). On macOS, you can also use the system `log` CLI -utility with `log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'`. +them and move on. Configuration errors will be logged. ## Debugging Configuration You can verify that configuration is being properly loaded by looking at the -debug output of Ghostty. Documentation for how to view the debug output is in -the "building Ghostty" section at the end of the README. +debug output of Ghostty. In the debug output, you should see in the first 20 lines or so messages about loading (or not loading) a configuration file, as well as any errors it may have @@ -93,3 +89,34 @@ will fall back to default values for erroneous keys. You can also view the full configuration Ghostty is loading using `ghostty +show-config` from the command-line. Use the `--help` flag to additional options for that command. + +## Logging + +Ghostty can write logs to a number of destinations. On all platforms, logging to +`stderr` is available. Depending on the platform and how Ghostty was launched, +logs sent to `stderr` may be stored by the system and made available for later +retrieval. + +On Linux if Ghostty is launched by the default `systemd` user service, you can use +`journald` to see Ghostty's logs: `journalctl --user --unit app-com.mitchellh.ghostty.service`. + +On macOS logging to the macOS unified log is available and enabled by default. +--Use the system `log` CLI to view Ghostty's logs: `sudo log stream --level debug +--predicate 'subsystem=="com.mitchellh.ghostty"'`. + +Ghostty's logging can be configured in two ways. The first is by what +optimization level Ghostty is compiled with. If Ghostty is compiled with `Debug` +optimizations debug logs will be output to `stderr`. If Ghostty is compiled with +any other optimization the debug logs will not be output to `stderr`. + +Ghostty also checks the `GHOSTTY_LOG` environment variable. It can be used +to control which destinations receive logs. Ghostty currently defines two +destinations: + +- `stderr` - logging to `stderr`. +- `macos` - logging to macOS's unified log (has no effect on non-macOS platforms). + +Combine values with a comma to enable multiple destinations. Prefix a +destination with `no-` to disable it. Enabling and disabling destinations +can be done at the same time. Setting `GHOSTTY_LOG` to `true` will enable all +destinations. Setting `GHOSTTY_LOG` to `false` will disable all destinations. diff --git a/src/cli/args.zig b/src/cli/args.zig index 43a15ca06..bd5060d69 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -604,7 +604,7 @@ pub fn parseAutoStruct( return result; } -fn parsePackedStruct(comptime T: type, v: []const u8) !T { +pub fn parsePackedStruct(comptime T: type, v: []const u8) !T { const info = @typeInfo(T).@"struct"; comptime assert(info.layout == .@"packed"); diff --git a/src/cli/list_keybinds.zig b/src/cli/list_keybinds.zig index a8899a4f5..61050d0cb 100644 --- a/src/cli/list_keybinds.zig +++ b/src/cli/list_keybinds.zig @@ -95,18 +95,35 @@ const TriggerNode = struct { }; const ChordBinding = struct { + table_name: ?[]const u8 = null, triggers: std.SinglyLinkedList, - action: Binding.Action, + actions: []const Binding.Action, // Order keybinds based on various properties - // 1. Longest chord sequence - // 2. Most active modifiers - // 3. Alphabetically by active modifiers - // 4. Trigger key order + // 1. Default bindings before table bindings (tables grouped at end) + // 2. Longest chord sequence + // 3. Most active modifiers + // 4. Alphabetically by active modifiers + // 5. Trigger key order + // 6. Within tables, sort by table name // These properties propagate through chorded keypresses // // Adapted from Binding.lessThan pub fn lessThan(_: void, lhs: ChordBinding, rhs: ChordBinding) bool { + const lhs_has_table = lhs.table_name != null; + const rhs_has_table = rhs.table_name != null; + + if (lhs_has_table != rhs_has_table) { + return !lhs_has_table; + } + + if (lhs_has_table) { + const table_cmp = std.mem.order(u8, lhs.table_name.?, rhs.table_name.?); + if (table_cmp != .eq) { + return table_cmp == .lt; + } + } + const lhs_len = lhs.triggers.len(); const rhs_len = rhs.triggers.len(); @@ -166,16 +183,19 @@ const ChordBinding = struct { var r_trigger = rhs.triggers.first; while (l_trigger != null and r_trigger != null) { + // We want catch_all to sort last. const lhs_key: c_int = blk: { switch (TriggerNode.get(l_trigger.?).data.key) { .physical => |key| break :blk @intFromEnum(key), .unicode => |key| break :blk @intCast(key), + .catch_all => break :blk std.math.maxInt(c_int), } }; const rhs_key: c_int = blk: { switch (TriggerNode.get(r_trigger.?).data.key) { .physical => |key| break :blk @intFromEnum(key), .unicode => |key| break :blk @intCast(key), + .catch_all => break :blk std.math.maxInt(c_int), } }; @@ -228,10 +248,30 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { const win = vx.window(); - // Generate a list of bindings, recursively traversing chorded keybindings + // Collect default bindings, recursively flattening chords var iter = keybinds.set.bindings.iterator(); - const bindings, const widest_chord = try iterateBindings(alloc, &iter, &win); + const default_bindings, var widest_chord = try iterateBindings(alloc, &iter, &win); + var bindings_list: std.ArrayList(ChordBinding) = .empty; + try bindings_list.appendSlice(alloc, default_bindings); + + // Collect key table bindings + var widest_table_prefix: u16 = 0; + var table_iter = keybinds.tables.iterator(); + while (table_iter.next()) |table_entry| { + const table_name = table_entry.key_ptr.*; + var binding_iter = table_entry.value_ptr.bindings.iterator(); + const table_bindings, const table_width = try iterateBindings(alloc, &binding_iter, &win); + for (table_bindings) |*b| { + b.table_name = table_name; + } + + try bindings_list.appendSlice(alloc, table_bindings); + widest_chord = @max(widest_chord, table_width); + widest_table_prefix = @max(widest_table_prefix, @as(u16, @intCast(win.gwidth(table_name) + win.gwidth("/")))); + } + + const bindings = bindings_list.items; std.mem.sort(ChordBinding, bindings, {}, ChordBinding.lessThan); // Set up styles for each modifier @@ -239,12 +279,22 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { const ctrl_style: vaxis.Style = .{ .fg = .{ .index = 2 } }; const alt_style: vaxis.Style = .{ .fg = .{ .index = 3 } }; const shift_style: vaxis.Style = .{ .fg = .{ .index = 4 } }; + const table_style: vaxis.Style = .{ .fg = .{ .index = 8 } }; // Print the list for (bindings) |bind| { win.clear(); var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false }; + + if (bind.table_name) |name| { + result = win.printSegment( + .{ .text = name, .style = table_style }, + .{ .col_offset = result.col }, + ); + result = win.printSegment(.{ .text = "/", .style = table_style }, .{ .col_offset = result.col }); + } + var maybe_trigger = bind.triggers.first; while (maybe_trigger) |node| : (maybe_trigger = node.next) { const trigger: *TriggerNode = .get(node); @@ -268,6 +318,7 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { const key = switch (trigger.data.key) { .physical => |k| try std.fmt.allocPrint(alloc, "{t}", .{k}), .unicode => |c| try std.fmt.allocPrint(alloc, "{u}", .{c}), + .catch_all => "catch_all", }; result = win.printSegment(.{ .text = key }, .{ .col_offset = result.col }); @@ -277,16 +328,32 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { } } - const action = try std.fmt.allocPrint(alloc, "{f}", .{bind.action}); - // If our action has an argument, we print the argument in a different color - if (std.mem.indexOfScalar(u8, action, ':')) |idx| { - _ = win.print(&.{ - .{ .text = action[0..idx] }, - .{ .text = action[idx .. idx + 1], .style = .{ .dim = true } }, - .{ .text = action[idx + 1 ..], .style = .{ .fg = .{ .index = 5 } } }, - }, .{ .col_offset = widest_chord + 3 }); - } else { - _ = win.printSegment(.{ .text = action }, .{ .col_offset = widest_chord + 3 }); + var action_col: u16 = widest_table_prefix + widest_chord + 3; + for (bind.actions, 0..) |act, i| { + if (i > 0) { + const chain_result = win.printSegment( + .{ .text = ", ", .style = .{ .dim = true } }, + .{ .col_offset = action_col }, + ); + action_col = chain_result.col; + } + + const action = try std.fmt.allocPrint(alloc, "{f}", .{act}); + // If our action has an argument, we print the argument in a different color + if (std.mem.indexOfScalar(u8, action, ':')) |idx| { + const print_result = win.print(&.{ + .{ .text = action[0..idx] }, + .{ .text = action[idx .. idx + 1], .style = .{ .dim = true } }, + .{ .text = action[idx + 1 ..], .style = .{ .fg = .{ .index = 5 } } }, + }, .{ .col_offset = action_col }); + action_col = print_result.col; + } else { + const print_result = win.printSegment( + .{ .text = action }, + .{ .col_offset = action_col }, + ); + action_col = print_result.col; + } } try vx.prettyPrint(writer); } @@ -314,6 +381,7 @@ fn iterateBindings( switch (t.key) { .physical => |k| try buf.writer.print("{t}", .{k}), .unicode => |c| try buf.writer.print("{u}", .{c}), + .catch_all => try buf.writer.print("catch_all", .{}), } break :blk win.gwidth(buf.written()); @@ -321,7 +389,6 @@ fn iterateBindings( switch (bind.value_ptr.*) { .leader => |leader| { - // Recursively iterate on the set of bindings for this leader key var n_iter = leader.bindings.iterator(); const sub_bindings, const max_width = try iterateBindings(alloc, &n_iter, win); @@ -342,10 +409,23 @@ fn iterateBindings( const node = try alloc.create(TriggerNode); node.* = .{ .data = bind.key_ptr.* }; + const actions = try alloc.alloc(Binding.Action, 1); + actions[0] = leaf.action; + widest_chord = @max(widest_chord, width); try bindings.append(alloc, .{ .triggers = .{ .first = &node.node }, - .action = leaf.action, + .actions = actions, + }); + }, + .leaf_chained => |leaf| { + const node = try alloc.create(TriggerNode); + node.* = .{ .data = bind.key_ptr.* }; + + widest_chord = @max(widest_chord, width); + try bindings.append(alloc, .{ + .triggers = .{ .first = &node.node }, + .actions = leaf.actions.items, }); }, } diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig index 45a80723e..42aff9d56 100644 --- a/src/cli/list_themes.zig +++ b/src/cli/list_themes.zig @@ -2,6 +2,7 @@ const std = @import("std"); const args = @import("args.zig"); const Action = @import("ghostty.zig").Action; const Config = @import("../config/Config.zig"); +const configpkg = @import("../config.zig"); const themepkg = @import("../config/theme.zig"); const tui = @import("tui.zig"); const global_state = &@import("../global.zig").state; @@ -196,6 +197,31 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 { return 0; } +fn resolveAutoThemePath(alloc: std.mem.Allocator) ![]u8 { + const main_cfg_path = try configpkg.preferredDefaultFilePath(alloc); + defer alloc.free(main_cfg_path); + + const base_dir = std.fs.path.dirname(main_cfg_path) orelse return error.BadPathName; + return try std.fs.path.join(alloc, &.{ base_dir, "auto", "theme.ghostty" }); +} + +fn writeAutoThemeFile(alloc: std.mem.Allocator, theme_name: []const u8) !void { + const auto_path = try resolveAutoThemePath(alloc); + defer alloc.free(auto_path); + + if (std.fs.path.dirname(auto_path)) |dir| { + try std.fs.cwd().makePath(dir); + } + + var f = try std.fs.createFileAbsolute(auto_path, .{ .truncate = true }); + defer f.close(); + + var buf: [128]u8 = undefined; + var w = f.writer(&buf); + try w.interface.print("theme = {s}\n", .{theme_name}); + try w.interface.flush(); +} + const Event = union(enum) { key_press: vaxis.Key, mouse: vaxis.Mouse, @@ -487,6 +513,9 @@ const Preview = struct { self.should_quit = true; if (key.matchesAny(&.{ vaxis.Key.escape, vaxis.Key.enter, vaxis.Key.kp_enter }, .{})) self.mode = .normal; + if (key.matches('w', .{})) { + self.saveSelectedTheme(); + } }, } }, @@ -698,7 +727,7 @@ const Preview = struct { .help => { win.hideCursor(); const width = 60; - const height = 20; + const height = 22; const child = win.child( .{ .x_off = win.width / 2 -| width / 2, @@ -733,6 +762,7 @@ const Preview = struct { .{ .keys = "/", .help = "Start search." }, .{ .keys = "^X, ^/", .help = "Clear search." }, .{ .keys = "⏎", .help = "Save theme or close search window." }, + .{ .keys = "w", .help = "Write theme to auto config file." }, }; for (key_help, 0..) |help, captured_i| { @@ -786,8 +816,8 @@ const Preview = struct { .save => { const theme = self.themes[self.filtered.items[self.current]]; - const width = 90; - const height = 12; + const width = 92; + const height = 17; const child = win.child( .{ .x_off = win.width / 2 -| width / 2, @@ -809,6 +839,12 @@ const Preview = struct { try std.fmt.allocPrint(alloc, "theme = {s}", .{theme.theme}), "", "Save the configuration file and then reload it to apply the new theme.", + "", + "Or press 'w' to write an auto theme file to your system's preferred default config path.", + "Then add the following line to your Ghostty configuration and reload:", + "", + "config-file = ?auto/theme.ghostty", + "", "For more details on configuration and themes, visit the Ghostty documentation:", "", "https://ghostty.org/docs/config/reference", @@ -1657,6 +1693,18 @@ const Preview = struct { }); } } + + fn saveSelectedTheme(self: *Preview) void { + if (self.filtered.items.len == 0) + return; + + const idx = self.filtered.items[self.current]; + const theme = self.themes[idx]; + + writeAutoThemeFile(self.allocator, theme.theme) catch { + return; + }; + } }; fn color(config: Config, palette: usize) vaxis.Color { diff --git a/src/cli/ssh-cache/DiskCache.zig b/src/cli/ssh-cache/DiskCache.zig index 62620ecb0..6214d0429 100644 --- a/src/cli/ssh-cache/DiskCache.zig +++ b/src/cli/ssh-cache/DiskCache.zig @@ -9,7 +9,6 @@ const assert = @import("../../quirks.zig").inlineAssert; const Allocator = std.mem.Allocator; const internal_os = @import("../../os/main.zig"); const xdg = internal_os.xdg; -const TempDir = internal_os.TempDir; const Entry = @import("Entry.zig"); // 512KB - sufficient for approximately 10k entries @@ -125,7 +124,7 @@ pub fn add( break :update .updated; }; - try self.writeCacheFile(alloc, entries, null); + try self.writeCacheFile(entries, null); return result; } @@ -166,7 +165,7 @@ pub fn remove( alloc.free(kv.value.terminfo_version); } - try self.writeCacheFile(alloc, entries, null); + try self.writeCacheFile(entries, null); } /// Check if a hostname exists in the cache. @@ -209,32 +208,30 @@ fn fixupPermissions(file: std.fs.File) (std.fs.File.StatError || std.fs.File.Chm fn writeCacheFile( self: DiskCache, - alloc: Allocator, entries: std.StringHashMap(Entry), expire_days: ?u32, ) !void { - var td: TempDir = try .init(); - defer td.deinit(); + const cache_dir = std.fs.path.dirname(self.path) orelse return error.InvalidCachePath; + const cache_basename = std.fs.path.basename(self.path); - const tmp_file = try td.dir.createFile("ssh-cache", .{ .mode = 0o600 }); - defer tmp_file.close(); - const tmp_path = try td.dir.realpathAlloc(alloc, "ssh-cache"); - defer alloc.free(tmp_path); + var dir = try std.fs.cwd().openDir(cache_dir, .{}); + defer dir.close(); var buf: [1024]u8 = undefined; - var writer = tmp_file.writer(&buf); + var atomic_file = try dir.atomicFile(cache_basename, .{ + .mode = 0o600, + .write_buffer = &buf, + }); + defer atomic_file.deinit(); + var iter = entries.iterator(); while (iter.next()) |kv| { // Only write non-expired entries if (kv.value_ptr.isExpired(expire_days)) continue; - try kv.value_ptr.format(&writer.interface); + try kv.value_ptr.format(&atomic_file.file_writer.interface); } - // Don't forget to flush!! - try writer.interface.flush(); - - // Atomic replace - try std.fs.renameAbsolute(tmp_path, self.path); + try atomic_file.finish(); } /// List all entries in the cache. @@ -382,16 +379,16 @@ test "disk cache clear" { const alloc = testing.allocator; // Create our path - var td: TempDir = try .init(); - defer td.deinit(); + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); var buf: [4096]u8 = undefined; { - var file = try td.dir.createFile("cache", .{}); + var file = try tmp.dir.createFile("cache", .{}); defer file.close(); var file_writer = file.writer(&buf); try file_writer.interface.writeAll("HELLO!"); } - const path = try td.dir.realpathAlloc(alloc, "cache"); + const path = try tmp.dir.realpathAlloc(alloc, "cache"); defer alloc.free(path); // Setup our cache @@ -401,7 +398,7 @@ test "disk cache clear" { // Verify the file is gone try testing.expectError( error.FileNotFound, - td.dir.openFile("cache", .{}), + tmp.dir.openFile("cache", .{}), ); } @@ -410,18 +407,18 @@ test "disk cache operations" { const alloc = testing.allocator; // Create our path - var td: TempDir = try .init(); - defer td.deinit(); + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); var buf: [4096]u8 = undefined; { - var file = try td.dir.createFile("cache", .{}); + var file = try tmp.dir.createFile("cache", .{}); defer file.close(); var file_writer = file.writer(&buf); const writer = &file_writer.interface; try writer.writeAll("HELLO!"); try writer.flush(); } - const path = try td.dir.realpathAlloc(alloc, "cache"); + const path = try tmp.dir.realpathAlloc(alloc, "cache"); defer alloc.free(path); // Setup our cache @@ -453,6 +450,32 @@ test "disk cache operations" { ); } +test "disk cache cleans up temp files" { + const testing = std.testing; + const alloc = testing.allocator; + + var tmp = testing.tmpDir(.{ .iterate = true }); + defer tmp.cleanup(); + + const tmp_path = try tmp.dir.realpathAlloc(alloc, "."); + defer alloc.free(tmp_path); + const cache_path = try std.fs.path.join(alloc, &.{ tmp_path, "cache" }); + defer alloc.free(cache_path); + + const cache: DiskCache = .{ .path = cache_path }; + try testing.expectEqual(AddResult.added, try cache.add(alloc, "example.com")); + try testing.expectEqual(AddResult.added, try cache.add(alloc, "example.org")); + + // Verify only the cache file exists and no temp files left behind + var count: usize = 0; + var iter = tmp.dir.iterate(); + while (try iter.next()) |entry| { + count += 1; + try testing.expectEqualStrings("cache", entry.name); + } + try testing.expectEqual(1, count); +} + test isValidHost { const testing = std.testing; diff --git a/src/config/CApi.zig b/src/config/CApi.zig index a970a8d33..4ea9ea63f 100644 --- a/src/config/CApi.zig +++ b/src/config/CApi.zig @@ -65,6 +65,15 @@ export fn ghostty_config_load_default_files(self: *Config) void { }; } +/// Load the configuration from a specific file path. +/// The path must be null-terminated. +export fn ghostty_config_load_file(self: *Config, path: [*:0]const u8) void { + const path_slice = std.mem.span(path); + self.loadFile(state.alloc, path_slice) catch |err| { + log.err("error loading config from file path={s} err={}", .{ path_slice, err }); + }; +} + /// Load the configuration from the user-specified configuration /// file locations in the previously loaded configuration. This will /// recursively continue to load up to a built-in limit. diff --git a/src/config/Config.zig b/src/config/Config.zig index 20256e951..8ca64efe9 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -17,6 +17,7 @@ const assert = @import("../quirks.zig").inlineAssert; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const global_state = &@import("../global.zig").state; +const deepEqual = @import("../datastruct/comparison.zig").deepEqual; const fontpkg = @import("../font/main.zig"); const inputpkg = @import("../input.zig"); const internal_os = @import("../os/main.zig"); @@ -37,6 +38,7 @@ const RepeatableStringMap = @import("RepeatableStringMap.zig"); pub const Path = @import("path.zig").Path; pub const RepeatablePath = @import("path.zig").RepeatablePath; const ClipboardCodepointMap = @import("ClipboardCodepointMap.zig"); +const KeyRemapSet = @import("../input/key_mods.zig").RemapSet; // We do this instead of importing all of terminal/main.zig to // limit the dependency graph. This is important because some things @@ -86,6 +88,10 @@ pub const compatibility = std.StaticStringMap( // Ghostty 1.2 removed the "desktop" option and renamed it to "detect". // The semantics also changed slightly but this is the correct mapping. .{ "gtk-single-instance", compatGtkSingleInstance }, + + // Ghostty 1.3 rename the "window" option to "new-window". + // See: https://github.com/ghostty-org/ghostty/pull/9764 + .{ "macos-dock-drop-behavior", compatMacOSDockDropBehavior }, }); /// The font families to use. @@ -706,6 +712,32 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// on the same selection. @"selection-clear-on-copy": bool = false, +/// Characters that mark word boundaries during text selection operations such +/// as double-clicking. When selecting a word, the selection will stop at any +/// of these characters. +/// +/// This is similar to the `WORDCHARS` environment variable in zsh, except this +/// specifies the boundary characters rather than the word characters. The +/// default includes common delimiters and punctuation that typically separate +/// words in code and prose. +/// +/// Each character in this string becomes a word boundary. Multi-byte UTF-8 +/// characters are supported, but only single codepoints can be specified. +/// Multi-codepoint sequences (e.g. emoji) are not supported. +/// +/// The null character (U+0000) is always treated as a boundary and does not +/// need to be included in this configuration. +/// +/// Default: ` \t'"│`|:;,()[]{}<>$` +/// +/// To add or remove specific characters, you can set this to a custom value. +/// For example, to treat semicolons as part of words: +/// +/// selection-word-chars = " \t'\"│`|:,()[]{}<>$" +/// +/// Available since: 1.3.0 +@"selection-word-chars": SelectionWordChars = .{}, + /// The minimum contrast ratio between the foreground and background colors. /// The contrast ratio is a value between 1 and 21. A value of 1 allows for no /// contrast (e.g. black on black). This value is the contrast ratio as defined @@ -927,6 +959,15 @@ palette: Palette = .{}, /// reasonable for a good looking blur. Higher blur intensities may /// cause strange rendering and performance issues. /// +/// On macOS 26.0 and later, there are additional special values that +/// can be set to use the native macOS glass effects: +/// +/// * `macos-glass-regular` - Standard glass effect with some opacity +/// * `macos-glass-clear` - Highly transparent glass effect +/// +/// If the macOS values are set, then this implies `background-blur = true` +/// on non-macOS platforms. +/// /// Supported on macOS and on some Linux desktop environments, including: /// /// * KDE Plasma (Wayland and X11) @@ -976,6 +1017,22 @@ palette: Palette = .{}, /// Available since: 1.1.0 @"split-divider-color": ?Color = null, +/// Control when Ghostty preserves a zoomed split. Under normal circumstances, +/// any operation that changes focus or layout of the split tree in a window +/// will unzoom any zoomed split. This configuration allows you to control +/// this behavior. +/// +/// This can be set to `navigation` to preserve the zoomed split state +/// when navigating to another split (e.g. via `goto_split`). This will +/// change the zoomed split to the newly focused split instead of unzooming. +/// +/// Any options can also be prefixed with `no-` to disable that option. +/// +/// Example: `split-preserve-zoom = navigation` +/// +/// Available since: 1.3.0 +@"split-preserve-zoom": SplitPreserveZoom = .{}, + /// The foreground and background color for search matches. This only applies /// to non-focused search matches, also known as candidate matches. /// @@ -1329,7 +1386,7 @@ maximize: bool = false, /// new windows, not just the first one. /// /// On macOS, this setting does not work if window-decoration is set to -/// "false", because native fullscreen on macOS requires window decorations +/// "none", because native fullscreen on macOS requires window decorations /// to be set. fullscreen: bool = false, @@ -1448,6 +1505,13 @@ class: ?[:0]const u8 = null, /// so if you specify both `a` and `KeyA`, the physical key will always be used /// regardless of what order they are configured. /// +/// The special key `catch_all` can be used to match any key that is not +/// otherwise bound. This can be combined with modifiers, for example +/// `ctrl+catch_all` will match any key pressed with `ctrl` that is not +/// otherwise bound. When looking up a binding, Ghostty first tries to match +/// `catch_all` with modifiers. If no match is found and the event has +/// modifiers, it falls back to `catch_all` without modifiers. +/// /// Valid modifiers are `shift`, `ctrl` (alias: `control`), `alt` (alias: `opt`, /// `option`), and `super` (alias: `cmd`, `command`). You may use the modifier /// or the alias. When debugging keybinds, the non-aliased modifier will always @@ -1485,6 +1549,11 @@ class: ?[:0]const u8 = null, /// specifically output that key (e.g. `ctrl+a>ctrl+a=text:foo`) or /// press an unbound key which will send both keys to the program. /// +/// * If an unbound key is pressed during a sequence and a `catch_all` +/// binding exists that would `ignore` the input, the entire sequence +/// is dropped and nothing happens. Otherwise, the entire sequence is +/// encoded and sent to the running program as if no keybind existed. +/// /// * If a prefix in a sequence is previously bound, the sequence will /// override the previous binding. For example, if `ctrl+a` is bound to /// `new_window` and `ctrl+a>n` is bound to `new_tab`, pressing `ctrl+a` @@ -1630,8 +1699,138 @@ class: ?[:0]const u8 = null, /// /// - Notably, global shortcuts have not been implemented on wlroots-based /// compositors like Sway (see [upstream issue](https://github.com/emersion/xdg-desktop-portal-wlr/issues/240)). +/// +/// ## Chained Actions +/// +/// A keybind can have multiple actions by using the `chain` keyword for +/// subsequent actions. When a keybind is activated, all chained actions are +/// executed in order. The syntax is: +/// +/// ```ini +/// keybind = ctrl+a=new_window +/// keybind = chain=goto_split:left +/// ``` +/// +/// This binds `ctrl+a` to first open a new window, then move focus to the +/// left split. Each `chain` entry appends an action to the most recently +/// defined keybind. You can chain as many actions as you want: +/// +/// ```ini +/// keybind = ctrl+a=new_window +/// keybind = chain=goto_split:left +/// keybind = chain=toggle_fullscreen +/// ``` +/// +/// Chained actions cannot have prefixes like `global:` or `unconsumed:`. +/// The flags from the original keybind apply to the entire chain. +/// +/// Chained actions work with key sequences as well. For example: +/// +/// ```ini +/// keybind = ctrl+a>n=new_window +/// keybind = chain=goto_split:left +/// ```` +/// +/// Chains with key sequences apply to the most recent binding in the +/// sequence. +/// +/// Chained keybinds are available since Ghostty 1.3.0. +/// +/// ## Key Tables +/// +/// You may also create a named set of keybindings known as a "key table." +/// A key table must be explicitly activated for the bindings to become +/// available. This can be used to implement features such as a +/// "copy mode", "vim mode", etc. Generically, this can implement modal +/// keyboard input. +/// +/// Key tables are defined using the syntax `/`. The +/// `` value is everything documented above for keybinds. The +/// `
` value is the name of the key table. Table names can contain +/// anything except `/`, `=`, `+`, and `>`. The characters `+` and `>` are +/// reserved for keybind syntax (modifier combinations and key sequences). +/// For example `foo/ctrl+a=new_window` defines a binding within a table +/// named `foo`. +/// +/// Tables are activated and deactivated using the binding actions +/// `activate_key_table:` and `deactivate_key_table`. Other table +/// related binding actions also exist; see the documentation for a full list. +/// These are the primary way to interact with key tables. +/// +/// Binding lookup proceeds from the innermost table outward, so keybinds in +/// the default table remain available unless explicitly unbound in an inner +/// table. +/// +/// A key table has some special syntax and handling: +/// +/// * `/` (with no binding) defines and clears a table, resetting all +/// of its keybinds and settings. +/// +/// * You cannot activate a table that is already the innermost table; such +/// attempts are ignored. However, the same table can appear multiple times +/// in the stack as long as it is not innermost (e.g., `A -> B -> A -> B` +/// is valid, but `A -> B -> B` is not). +/// +/// * A table can be activated in one-shot mode using +/// `activate_key_table_once:`. A one-shot table is automatically +/// deactivated when any non-catch-all binding is invoked. +/// +/// * Key sequences work within tables: `foo/ctrl+a>ctrl+b=new_window`. +/// If an invalid key is pressed, the sequence ends but the table remains +/// active. +/// +/// * Prefixes like `global:` work within tables: +/// `foo/global:ctrl+a=new_window`. +/// +/// Key tables are available since Ghostty 1.3.0. keybind: Keybinds = .{}, +/// Remap modifier keys within Ghostty. This allows you to swap or reassign +/// modifier keys at the application level without affecting system-wide +/// settings. +/// +/// The format is `from=to` where both `from` and `to` are modifier key names. +/// You can use generic names like `ctrl`, `alt`, `shift`, `super` (macOS: +/// `cmd`/`command`) or sided names like `left_ctrl`, `right_alt`, etc. +/// +/// This will NOT change keyboard layout or key encodings outside of Ghostty. +/// For example, on macOS, `option+a` may still produce `å` even if `option` is +/// remapped to `ctrl`. Desktop environments usually handle key layout long +/// before Ghostty receives the key events. +/// +/// Example: +/// +/// key-remap = ctrl=super +/// key-remap = left_control=right_alt +/// +/// Important notes: +/// +/// * This is a one-way remap. If you remap `ctrl=super`, then the physical +/// Ctrl key acts as Super, but the Super key remains Super. +/// +/// * Remaps are not transitive. If you remap `ctrl=super` and `alt=ctrl`, +/// pressing Alt will produce Ctrl, NOT Super. +/// +/// * This affects both keybind matching and terminal input encoding. +/// This does NOT impact keyboard layout or how keys are interpreted +/// prior to Ghostty receiving them. For example, `option+a` on macOS +/// may still produce `å` even if `option` is remapped to `ctrl`. +/// +/// * Generic modifiers (e.g. `ctrl`) match both left and right physical keys. +/// Use sided names (e.g. `left_ctrl`) to remap only one side. +/// +/// There are other edge case scenarios that may not behave as expected +/// but are working as intended the way this feature is designed: +/// +/// * On macOS, bindings in the main menu will trigger before any remapping +/// is done. This is because macOS itself handles menu activation and +/// this happens before Ghostty receives the key event. To workaround +/// this, you should unbind the menu items and rebind them using your +/// desired modifier. +/// +/// This configuration can be repeated to specify multiple remaps. +@"key-remap": KeyRemapSet = .empty, + /// Horizontal window padding. This applies padding between the terminal cells /// and the left and right window borders. The value is in points, meaning that /// it will be scaled appropriately for screen DPI. @@ -1720,11 +1919,21 @@ keybind: Keybinds = .{}, /// This setting is only supported currently on macOS. @"window-vsync": bool = true, -/// If true, new windows and tabs will inherit the working directory of the +/// If true, new windows will inherit the working directory of the /// previously focused window. If no window was previously focused, the default /// working directory will be used (the `working-directory` option). @"window-inherit-working-directory": bool = true, +/// If true, new tabs will inherit the working directory of the +/// previously focused tab. If no tab was previously focused, the default +/// working directory will be used (the `working-directory` option). +@"tab-inherit-working-directory": bool = true, + +/// If true, new split panes will inherit the working directory of the +/// previously focused split. If no split was previously focused, the default +/// working directory will be used (the `working-directory` option). +@"split-inherit-working-directory": bool = true, + /// If true, new windows and tabs will inherit the font size of the previously /// focused window. If no window was previously focused, the default font size /// will be used. If this is false, the default font size specified in the @@ -2484,7 +2693,7 @@ keybind: Keybinds = .{}, /// /// * `detect` - Detect the shell based on the filename. /// -/// * `bash`, `elvish`, `fish`, `zsh` - Use this specific shell injection scheme. +/// * `bash`, `elvish`, `fish`, `nushell`, `zsh` - Use this specific shell injection scheme. /// /// The default value is `detect`. @"shell-integration": ShellIntegration = .detect, @@ -2658,6 +2867,40 @@ keybind: Keybinds = .{}, /// the same time as the `iTime` uniform, allowing you to compute the /// time since the change by subtracting this from `iTime`. /// +/// * `float iTimeFocus` - Timestamp when the surface last gained iFocus. +/// +/// When the surface gains focus, this is set to the current value of +/// `iTime`, similar to how `iTimeCursorChange` works. This allows you +/// to compute the time since focus was gained or lost by calculating +/// `iTime - iTimeFocus`. Use this to create animations that restart +/// when the terminal regains focus. +/// +/// * `int iFocus` - Current focus state of the surface. +/// +/// Set to 1.0 when the surface is focused, 0.0 when unfocused. This +/// allows shaders to detect unfocused state and avoid animation artifacts +/// from large time deltas caused by infrequent "deceptive frames" +/// (e.g., modifier key presses, link hover events in unfocused split panes). +/// Check `iFocus > 0` to determine if the surface is currently focused. +/// +/// * `vec3 iPalette[256]` - The 256-color terminal palette. +/// +/// RGB values for all 256 colors in the terminal palette, normalized +/// to [0.0, 1.0]. Index 0-15 are the ANSI colors, 16-231 are the 6x6x6 +/// color cube, and 232-255 are the grayscale colors. +/// +/// * `vec3 iBackgroundColor` - Terminal background color (RGB). +/// +/// * `vec3 iForegroundColor` - Terminal foreground color (RGB). +/// +/// * `vec3 iCursorColor` - Terminal cursor color (RGB). +/// +/// * `vec3 iCursorText` - Terminal cursor text color (RGB). +/// +/// * `vec3 iSelectionBackgroundColor` - Selection background color (RGB). +/// +/// * `vec3 iSelectionForegroundColor` - Selection foreground color (RGB). +/// /// If the shader fails to compile, the shader will be ignored. Any errors /// related to shader compilation will not show up as configuration errors /// and only show up in the log, since shader compilation happens after @@ -2736,7 +2979,7 @@ keybind: Keybinds = .{}, /// Display a border around the alerted surface until the terminal is /// re-focused or interacted with (such as on keyboard input). /// -/// GTK only. +/// Available since: 1.2.0 on GTK, 1.2.1 on macOS /// /// Example: `audio`, `no-audio`, `system`, `no-system` /// @@ -2825,7 +3068,7 @@ keybind: Keybinds = .{}, /// also known as the traffic lights, that allow you to close, miniaturize, and /// zoom the window. /// -/// This setting has no effect when `window-decoration = false` or +/// This setting has no effect when `window-decoration = none` or /// `macos-titlebar-style = hidden`, as the window buttons are always hidden in /// these modes. /// @@ -2866,7 +3109,7 @@ keybind: Keybinds = .{}, /// macOS 14 does not have this issue and any other macOS version has not /// been tested. /// -/// The "hidden" style hides the titlebar. Unlike `window-decoration = false`, +/// The "hidden" style hides the titlebar. Unlike `window-decoration = none`, /// however, it does not remove the frame from the window or cause it to have /// squared corners. Changing to or from this option at run-time may affect /// existing windows in buggy ways. @@ -2911,7 +3154,7 @@ keybind: Keybinds = .{}, /// /// * `new-tab` - Create a new tab in the current window, or open /// a new window if none exist. -/// * `window` - Create a new window unconditionally. +/// * `new-window` - Create a new window unconditionally. /// /// The default value is `new-tab`. /// @@ -3205,7 +3448,7 @@ else /// manager's simple titlebar. The behavior of this option will vary with your /// window manager. /// -/// This option does nothing when `window-decoration` is false or when running +/// This option does nothing when `window-decoration` is none or when running /// under macOS. @"gtk-titlebar": bool = true, @@ -3235,7 +3478,7 @@ else /// more subtle border. @"gtk-toolbar-style": GtkToolbarStyle = .raised, -/// The style of the GTK titlbar. Available values are `native` and `tabs`. +/// The style of the GTK titlebar. Available values are `native` and `tabs`. /// /// The `native` titlebar style is a traditional titlebar with a title, a few /// buttons and window controls. A separate tab bar will show up below the @@ -3922,7 +4165,7 @@ pub fn changeConditionalState( // Conditional set contains the keys that this config uses. So we // only continue if we use this key. - if (self._conditional_set.contains(key) and !equalField( + if (self._conditional_set.contains(key) and !deepEqual( @TypeOf(@field(self._conditional_state, field.name)), @field(self._conditional_state, field.name), @field(new, field.name), @@ -4285,6 +4528,9 @@ pub fn finalize(self: *Config) !void { } self.@"faint-opacity" = std.math.clamp(self.@"faint-opacity", 0.0, 1.0); + + // Finalize key remapping set for efficient lookups + self.@"key-remap".finalize(); } /// Callback for src/cli/args.zig to allow us to handle special cases @@ -4445,6 +4691,23 @@ fn compatBoldIsBright( return true; } +fn compatMacOSDockDropBehavior( + self: *Config, + alloc: Allocator, + key: []const u8, + value: ?[]const u8, +) bool { + _ = alloc; + assert(std.mem.eql(u8, key, "macos-dock-drop-behavior")); + + if (std.mem.eql(u8, value orelse "", "window")) { + self.@"macos-dock-drop-behavior" = .@"new-window"; + return true; + } + + return false; +} + /// Add a diagnostic message to the config with the given string. /// This is always added with a location of "none". pub fn addDiagnosticFmt( @@ -4608,7 +4871,7 @@ pub fn changed(self: *const Config, new: *const Config, comptime key: Key) bool const old_value = @field(self, field.name); const new_value = @field(new, field.name); - return !equalField(field.type, old_value, new_value); + return !deepEqual(field.type, old_value, new_value); } /// This yields a key for every changed field between old and new. @@ -4636,91 +4899,6 @@ pub const ChangeIterator = struct { } }; -/// A config-specific helper to determine if two values of the same -/// type are equal. This isn't the same as std.mem.eql or std.testing.equals -/// because we expect structs to implement their own equality. -/// -/// This also doesn't support ALL Zig types, because we only add to it -/// as we need types for the config. -fn equalField(comptime T: type, old: T, new: T) bool { - // Do known named types first - switch (T) { - inline []const u8, - [:0]const u8, - => return std.mem.eql(u8, old, new), - - []const [:0]const u8, - => { - if (old.len != new.len) return false; - for (old, new) |a, b| { - if (!std.mem.eql(u8, a, b)) return false; - } - - return true; - }, - - else => {}, - } - - // Back into types of types - switch (@typeInfo(T)) { - .void => return true, - - inline .bool, - .int, - .float, - .@"enum", - => return old == new, - - .optional => |info| { - if (old == null and new == null) return true; - if (old == null or new == null) return false; - return equalField(info.child, old.?, new.?); - }, - - .@"struct" => |info| { - if (@hasDecl(T, "equal")) return old.equal(new); - - // If a struct doesn't declare an "equal" function, we fall back - // to a recursive field-by-field compare. - inline for (info.fields) |field_info| { - if (!equalField( - field_info.type, - @field(old, field_info.name), - @field(new, field_info.name), - )) return false; - } - return true; - }, - - .@"union" => |info| { - if (@hasDecl(T, "equal")) return old.equal(new); - - const tag_type = info.tag_type.?; - const old_tag = std.meta.activeTag(old); - const new_tag = std.meta.activeTag(new); - if (old_tag != new_tag) return false; - - inline for (info.fields) |field_info| { - if (@field(tag_type, field_info.name) == old_tag) { - return equalField( - field_info.type, - @field(old, field_info.name), - @field(new, field_info.name), - ); - } - } - - unreachable; - }, - - else => { - @compileLog(T); - @compileError("unsupported field type"); - }, - } -} - /// This runs a heuristic to determine if we are likely running /// Ghostty in a CLI environment. We need this to change some behaviors. /// We should keep the set of behaviors that depend on this as small @@ -5629,6 +5807,113 @@ pub const RepeatableString = struct { } }; +/// SelectionWordChars stores the parsed codepoints for word boundary +/// characters used during text selection. The string is parsed once +/// during configuration and stored as u21 codepoints for efficient +/// lookup during selection operations. +pub const SelectionWordChars = struct { + const Self = @This(); + + /// Default boundary characters: ` \t'"│`|:;,()[]{}<>$` + const default_codepoints = [_]u21{ + 0, // null + ' ', // space + '\t', // tab + '\'', // single quote + '"', // double quote + '│', // U+2502 box drawing + '`', // backtick + '|', // pipe + ':', // colon + ';', // semicolon + ',', // comma + '(', // left paren + ')', // right paren + '[', // left bracket + ']', // right bracket + '{', // left brace + '}', // right brace + '<', // less than + '>', // greater than + '$', // dollar + }; + + /// The parsed codepoints. Always includes null (U+0000) at index 0. + codepoints: []const u21 = &default_codepoints, + + pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void { + const value = input orelse return error.ValueRequired; + + // Parse UTF-8 string into codepoints + var list: std.ArrayList(u21) = .empty; + defer list.deinit(alloc); + + // Always include null as first boundary + try list.append(alloc, 0); + + // Parse the UTF-8 string + const utf8_view = std.unicode.Utf8View.init(value) catch { + // Invalid UTF-8, just use null boundary + self.codepoints = try list.toOwnedSlice(alloc); + return; + }; + + var utf8_it = utf8_view.iterator(); + while (utf8_it.nextCodepoint()) |codepoint| { + try list.append(alloc, codepoint); + } + + self.codepoints = try list.toOwnedSlice(alloc); + } + + /// Deep copy of the struct. Required by Config. + pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { + const copy = try alloc.dupe(u21, self.codepoints); + return .{ .codepoints = copy }; + } + + /// Compare if two values are equal. Required by Config. + pub fn equal(self: Self, other: Self) bool { + return std.mem.eql(u21, self.codepoints, other.codepoints); + } + + /// Used by Formatter + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { + // Convert codepoints back to UTF-8 string for display + var buf: [4096]u8 = undefined; + var pos: usize = 0; + + // Skip the null character at index 0 + for (self.codepoints[1..]) |codepoint| { + var utf8_buf: [4]u8 = undefined; + const len = std.unicode.utf8Encode(codepoint, &utf8_buf) catch continue; + if (pos + len > buf.len) break; + @memcpy(buf[pos..][0..len], utf8_buf[0..len]); + pos += len; + } + + try formatter.formatEntry([]const u8, buf[0..pos]); + } + + test "parseCLI" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var chars: Self = .{}; + try chars.parseCLI(alloc, " \t;,"); + + // Should have null + 4 characters + try testing.expectEqual(@as(usize, 5), chars.codepoints.len); + try testing.expectEqual(@as(u21, 0), chars.codepoints[0]); + try testing.expectEqual(@as(u21, ' '), chars.codepoints[1]); + try testing.expectEqual(@as(u21, '\t'), chars.codepoints[2]); + try testing.expectEqual(@as(u21, ';'), chars.codepoints[3]); + try testing.expectEqual(@as(u21, ','), chars.codepoints[4]); + } +}; + /// FontVariation is a repeatable configuration value that sets a single /// font variation value. Font variations are configurations for what /// are often called "variable fonts." The font files usually end in @@ -5759,12 +6044,17 @@ pub const RepeatableFontVariation = struct { pub const Keybinds = struct { set: inputpkg.Binding.Set = .{}, + /// Defined key tables. The default key table is always the root "set", + /// which allows all table names to be available without reservation. + tables: std.StringArrayHashMapUnmanaged(inputpkg.Binding.Set) = .empty, + pub fn init(self: *Keybinds, alloc: Allocator) !void { // We don't clear the memory because it's in the arena and unlikely // to be free-able anyways (since arenas can only clear the last // allocated value). This isn't a memory leak because the arena // will be freed when the config is freed. self.set = .{}; + self.tables = .empty; // keybinds for opening and reloading config try self.set.put( @@ -5835,7 +6125,7 @@ pub const Keybinds = struct { // set the expected keybind for the menu. try self.set.put( alloc, - .{ .key = .{ .physical = .equal }, .mods = inputpkg.ctrlOrSuper(.{}) }, + .{ .key = .{ .unicode = '=' }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .increase_font_size = 1 }, ); try self.set.put( @@ -6003,13 +6293,13 @@ pub const Keybinds = struct { ); try self.set.putFlags( alloc, - .{ .key = .{ .physical = .bracket_left }, .mods = .{ .ctrl = true, .super = true } }, + .{ .key = .{ .unicode = '[' }, .mods = .{ .ctrl = true, .super = true } }, .{ .goto_split = .previous }, .{ .performable = true }, ); try self.set.putFlags( alloc, - .{ .key = .{ .physical = .bracket_right }, .mods = .{ .ctrl = true, .super = true } }, + .{ .key = .{ .unicode = ']' }, .mods = .{ .ctrl = true, .super = true } }, .{ .goto_split = .next }, .{ .performable = true }, ); @@ -6283,6 +6573,12 @@ pub const Keybinds = struct { .{ .key = .{ .physical = .page_down }, .mods = .{ .super = true } }, .{ .scroll_page_down = {} }, ); + try self.set.putFlags( + alloc, + .{ .key = .{ .unicode = 'j' }, .mods = .{ .super = true } }, + .{ .scroll_to_selection = {} }, + .{ .performable = true }, + ); // Semantic prompts try self.set.put( @@ -6329,12 +6625,12 @@ pub const Keybinds = struct { ); try self.set.put( alloc, - .{ .key = .{ .physical = .bracket_left }, .mods = .{ .super = true, .shift = true } }, + .{ .key = .{ .unicode = '[' }, .mods = .{ .super = true, .shift = true } }, .{ .previous_tab = {} }, ); try self.set.put( alloc, - .{ .key = .{ .physical = .bracket_right }, .mods = .{ .super = true, .shift = true } }, + .{ .key = .{ .unicode = ']' }, .mods = .{ .super = true, .shift = true } }, .{ .next_tab = {} }, ); try self.set.put( @@ -6349,12 +6645,12 @@ pub const Keybinds = struct { ); try self.set.put( alloc, - .{ .key = .{ .physical = .bracket_left }, .mods = .{ .super = true } }, + .{ .key = .{ .unicode = '[' }, .mods = .{ .super = true } }, .{ .goto_split = .previous }, ); try self.set.put( alloc, - .{ .key = .{ .physical = .bracket_right }, .mods = .{ .super = true } }, + .{ .key = .{ .unicode = ']' }, .mods = .{ .super = true } }, .{ .goto_split = .next }, ); try self.set.put( @@ -6399,7 +6695,7 @@ pub const Keybinds = struct { ); try self.set.put( alloc, - .{ .key = .{ .physical = .equal }, .mods = .{ .super = true, .ctrl = true } }, + .{ .key = .{ .unicode = '=' }, .mods = .{ .super = true, .ctrl = true } }, .{ .equalize_splits = {} }, ); @@ -6422,6 +6718,12 @@ pub const Keybinds = struct { .start_search, .{ .performable = true }, ); + try self.set.putFlags( + alloc, + .{ .key = .{ .unicode = 'e' }, .mods = .{ .super = true } }, + .search_selection, + .{ .performable = true }, + ); try self.set.putFlags( alloc, .{ .key = .{ .unicode = 'f' }, .mods = .{ .super = true, .shift = true } }, @@ -6534,21 +6836,86 @@ pub const Keybinds = struct { // will be freed when the config is freed. log.info("config has 'keybind = clear', all keybinds cleared", .{}); self.set = .{}; + self.tables = .empty; return; } - // Let our much better tested binding package handle parsing and storage. + // Check for table syntax: "name/" or "name/binding" + // We look for '/' only before the first '=' to avoid matching + // action arguments like "foo=text:/hello". + const eq_idx = std.mem.indexOfScalar(u8, value, '=') orelse value.len; + if (std.mem.indexOfScalar(u8, value[0..eq_idx], '/')) |slash_idx| table: { + const table_name = value[0..slash_idx]; + + // Length zero is valid, so you can set `/=action` for the slash key + if (table_name.len == 0) break :table; + + // Ignore '+', '>' because they can be part of sequences and + // triggers. This lets things like `ctrl+/=action` work. + if (std.mem.indexOfAny( + u8, + table_name, + "+>", + ) != null) break :table; + + const binding = value[slash_idx + 1 ..]; + + // Get or create the table + const gop = try self.tables.getOrPut(alloc, table_name); + if (!gop.found_existing) { + // We need to copy our table name into the arena + // for valid lookups later. + gop.key_ptr.* = try alloc.dupe(u8, table_name); + gop.value_ptr.* = .{}; + } + + // If there's no binding after the slash, this is a table + // definition/clear command + if (binding.len == 0) { + log.debug("config has 'keybind = {s}/', table cleared", .{table_name}); + gop.value_ptr.* = .{}; + return; + } + + // Parse and add the binding to the table + try gop.value_ptr.parseAndPut(alloc, binding); + return; + } + + // Parse into default set try self.set.parseAndPut(alloc, value); } /// Deep copy of the struct. Required by Config. pub fn clone(self: *const Keybinds, alloc: Allocator) Allocator.Error!Keybinds { - return .{ .set = try self.set.clone(alloc) }; + var tables: std.StringArrayHashMapUnmanaged(inputpkg.Binding.Set) = .empty; + try tables.ensureTotalCapacity(alloc, @intCast(self.tables.count())); + var it = self.tables.iterator(); + while (it.next()) |entry| { + const key = try alloc.dupe(u8, entry.key_ptr.*); + tables.putAssumeCapacity(key, try entry.value_ptr.clone(alloc)); + } + + return .{ + .set = try self.set.clone(alloc), + .tables = tables, + }; } /// Compare if two of our value are requal. Required by Config. pub fn equal(self: Keybinds, other: Keybinds) bool { - return equalSet(&self.set, &other.set); + if (!equalSet(&self.set, &other.set)) return false; + + // Compare tables + if (self.tables.count() != other.tables.count()) return false; + + var it = self.tables.iterator(); + while (it.next()) |entry| { + const other_set = other.tables.get(entry.key_ptr.*) orelse return false; + if (!equalSet(entry.value_ptr, &other_set)) return false; + } + + return true; } fn equalSet( @@ -6585,12 +6952,27 @@ pub const Keybinds = struct { const self_leaf = self_entry.value_ptr.*.leaf; const other_leaf = other_entry.value_ptr.*.leaf; - if (!equalField( + if (!deepEqual( inputpkg.Binding.Set.Leaf, self_leaf, other_leaf, )) return false; }, + + .leaf_chained => { + const self_chain = self_entry.value_ptr.*.leaf_chained; + const other_chain = other_entry.value_ptr.*.leaf_chained; + + if (self_chain.flags != other_chain.flags) return false; + if (self_chain.actions.items.len != other_chain.actions.items.len) return false; + for (self_chain.actions.items, other_chain.actions.items) |a1, a2| { + if (!deepEqual( + inputpkg.Binding.Action, + a1, + a2, + )) return false; + } + }, } } @@ -6599,12 +6981,14 @@ pub const Keybinds = struct { /// Like formatEntry but has an option to include docs. pub fn formatEntryDocs(self: Keybinds, formatter: formatterpkg.EntryFormatter, docs: bool) !void { - if (self.set.bindings.size == 0) { + if (self.set.bindings.count() == 0 and self.tables.count() == 0) { try formatter.formatEntry(void, {}); return; } var buf: [1024]u8 = undefined; + + // Format root set bindings var iter = self.set.bindings.iterator(); while (iter.next()) |next| { const k = next.key_ptr.*; @@ -6631,6 +7015,23 @@ pub const Keybinds = struct { writer.print("{f}", .{k}) catch return error.OutOfMemory; try v.formatEntries(&writer, formatter); } + + // Format table bindings + var table_iter = self.tables.iterator(); + while (table_iter.next()) |table_entry| { + const table_name = table_entry.key_ptr.*; + const table_set = table_entry.value_ptr.*; + + var binding_iter = table_set.bindings.iterator(); + while (binding_iter.next()) |next| { + const k = next.key_ptr.*; + const v = next.value_ptr.*; + + var writer: std.Io.Writer = .fixed(&buf); + writer.print("{s}/{f}", .{ table_name, k }) catch return error.OutOfMemory; + try v.formatEntries(&writer, formatter); + } + } } /// Used by Formatter @@ -6682,8 +7083,8 @@ pub const Keybinds = struct { // Note they turn into translated keys because they match // their ASCII mapping. const want = - \\keybind = ctrl+z>2=goto_tab:2 \\keybind = ctrl+z>1=goto_tab:1 + \\keybind = ctrl+z>2=goto_tab:2 \\ ; try std.testing.expectEqualStrings(want, buf.written()); @@ -6707,14 +7108,390 @@ pub const Keybinds = struct { // NB: This does not currently retain the order of the keybinds. const want = - \\a = ctrl+a>ctrl+c>t=new_tab - \\a = ctrl+a>ctrl+b>w=close_window \\a = ctrl+a>ctrl+b>n=new_window + \\a = ctrl+a>ctrl+b>w=close_window + \\a = ctrl+a>ctrl+c>t=new_tab \\a = ctrl+b>ctrl+d>a=previous_tab \\ ; try std.testing.expectEqualStrings(want, buf.written()); } + + test "parseCLI table definition" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Define a table by adding a binding to it + try keybinds.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + try testing.expectEqual(1, keybinds.tables.count()); + try testing.expect(keybinds.tables.contains("foo")); + + const table = keybinds.tables.get("foo").?; + try testing.expectEqual(1, table.bindings.count()); + } + + test "parseCLI table clear" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Add a binding to a table + try keybinds.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + try testing.expectEqual(1, keybinds.tables.get("foo").?.bindings.count()); + + // Clear the table with "foo/" + try keybinds.parseCLI(alloc, "foo/"); + try testing.expectEqual(0, keybinds.tables.get("foo").?.bindings.count()); + } + + test "parseCLI table multiple bindings" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + try keybinds.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + try keybinds.parseCLI(alloc, "foo/shift+b=paste_from_clipboard"); + try keybinds.parseCLI(alloc, "bar/ctrl+c=close_window"); + + try testing.expectEqual(2, keybinds.tables.count()); + try testing.expectEqual(2, keybinds.tables.get("foo").?.bindings.count()); + try testing.expectEqual(1, keybinds.tables.get("bar").?.bindings.count()); + } + + test "parseCLI table does not affect root set" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + try keybinds.parseCLI(alloc, "shift+a=copy_to_clipboard"); + try keybinds.parseCLI(alloc, "foo/shift+b=paste_from_clipboard"); + + // Root set should have the first binding + try testing.expectEqual(1, keybinds.set.bindings.count()); + // Table should have the second binding + try testing.expectEqual(1, keybinds.tables.get("foo").?.bindings.count()); + } + + test "parseCLI table empty name is invalid" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + try testing.expectError(error.InvalidFormat, keybinds.parseCLI(alloc, "/shift+a=copy_to_clipboard")); + } + + test "parseCLI table with key sequence" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Key sequences should work within tables + try keybinds.parseCLI(alloc, "foo/ctrl+a>ctrl+b=new_window"); + + const table = keybinds.tables.get("foo").?; + try testing.expectEqual(1, table.bindings.count()); + } + + test "parseCLI slash in action argument is not a table" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // A slash after the = should not be interpreted as a table delimiter + try keybinds.parseCLI(alloc, "ctrl+a=text:/hello"); + + // Should be in root set, not a table + try testing.expectEqual(1, keybinds.set.bindings.count()); + try testing.expectEqual(0, keybinds.tables.count()); + } + + test "parseCLI slash as key with modifier is not a table" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // ctrl+/ should be parsed as a keybind with '/' as the key, not a table + try keybinds.parseCLI(alloc, "ctrl+/=text:foo"); + + // Should be in root set, not a table + try testing.expectEqual(1, keybinds.set.bindings.count()); + try testing.expectEqual(0, keybinds.tables.count()); + } + + test "parseCLI shift+slash as key is not a table" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // shift+/ should be parsed as a keybind, not a table + try keybinds.parseCLI(alloc, "shift+/=ignore"); + + // Should be in root set, not a table + try testing.expectEqual(1, keybinds.set.bindings.count()); + try testing.expectEqual(0, keybinds.tables.count()); + } + + test "parseCLI bare slash as key is not a table" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Bare / as a key should work (empty table name is rejected) + try keybinds.parseCLI(alloc, "/=text:foo"); + + // Should be in root set, not a table + try testing.expectEqual(1, keybinds.set.bindings.count()); + try testing.expectEqual(0, keybinds.tables.count()); + } + + test "parseCLI slash in key sequence is not a table" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Key sequence ending with / should work + try keybinds.parseCLI(alloc, "ctrl+a>ctrl+/=new_window"); + + // Should be in root set, not a table + try testing.expectEqual(1, keybinds.set.bindings.count()); + try testing.expectEqual(0, keybinds.tables.count()); + } + + test "parseCLI table with slash in binding" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Table with a binding that uses / as the key + try keybinds.parseCLI(alloc, "mytable//=text:foo"); + + // Should be in the table + try testing.expectEqual(0, keybinds.set.bindings.count()); + try testing.expectEqual(1, keybinds.tables.count()); + try testing.expect(keybinds.tables.contains("mytable")); + try testing.expectEqual(1, keybinds.tables.get("mytable").?.bindings.count()); + } + + test "parseCLI table with sequence containing slash" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Table with a key sequence that ends with / + try keybinds.parseCLI(alloc, "mytable/a>/=new_window"); + + // Should be in the table + try testing.expectEqual(0, keybinds.set.bindings.count()); + try testing.expectEqual(1, keybinds.tables.count()); + try testing.expect(keybinds.tables.contains("mytable")); + } + + test "clone with tables" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + try keybinds.parseCLI(alloc, "shift+a=copy_to_clipboard"); + try keybinds.parseCLI(alloc, "foo/shift+b=paste_from_clipboard"); + try keybinds.parseCLI(alloc, "bar/ctrl+c=close_window"); + + const cloned = try keybinds.clone(alloc); + + // Verify the clone has the same structure + try testing.expectEqual(keybinds.set.bindings.count(), cloned.set.bindings.count()); + try testing.expectEqual(keybinds.tables.count(), cloned.tables.count()); + try testing.expectEqual( + keybinds.tables.get("foo").?.bindings.count(), + cloned.tables.get("foo").?.bindings.count(), + ); + try testing.expectEqual( + keybinds.tables.get("bar").?.bindings.count(), + cloned.tables.get("bar").?.bindings.count(), + ); + } + + test "equal with tables" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds1: Keybinds = .{}; + try keybinds1.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + + var keybinds2: Keybinds = .{}; + try keybinds2.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + + try testing.expect(keybinds1.equal(keybinds2)); + } + + test "equal with tables different table count" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds1: Keybinds = .{}; + try keybinds1.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + + var keybinds2: Keybinds = .{}; + try keybinds2.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + try keybinds2.parseCLI(alloc, "bar/shift+b=paste_from_clipboard"); + + try testing.expect(!keybinds1.equal(keybinds2)); + } + + test "equal with tables different table names" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds1: Keybinds = .{}; + try keybinds1.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + + var keybinds2: Keybinds = .{}; + try keybinds2.parseCLI(alloc, "bar/shift+a=copy_to_clipboard"); + + try testing.expect(!keybinds1.equal(keybinds2)); + } + + test "equal with tables different bindings" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds1: Keybinds = .{}; + try keybinds1.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + + var keybinds2: Keybinds = .{}; + try keybinds2.parseCLI(alloc, "foo/shift+b=paste_from_clipboard"); + + try testing.expect(!keybinds1.equal(keybinds2)); + } + + test "formatEntry with tables" { + const testing = std.testing; + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + try keybinds.parseCLI(alloc, "foo/shift+a=csi:hello"); + try keybinds.formatEntry(formatterpkg.entryFormatter("keybind", &buf.writer)); + + try testing.expectEqualStrings("keybind = foo/shift+a=csi:hello\n", buf.written()); + } + + test "formatEntry with tables and root set" { + const testing = std.testing; + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + try keybinds.parseCLI(alloc, "shift+b=csi:world"); + try keybinds.parseCLI(alloc, "foo/shift+a=csi:hello"); + try keybinds.formatEntry(formatterpkg.entryFormatter("keybind", &buf.writer)); + + const output = buf.written(); + try testing.expect(std.mem.indexOf(u8, output, "keybind = shift+b=csi:world\n") != null); + try testing.expect(std.mem.indexOf(u8, output, "keybind = foo/shift+a=csi:hello\n") != null); + } + + test "parseCLI clear clears tables" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Add bindings to root set and tables + try keybinds.parseCLI(alloc, "shift+a=copy_to_clipboard"); + try keybinds.parseCLI(alloc, "foo/shift+b=paste_from_clipboard"); + try keybinds.parseCLI(alloc, "bar/ctrl+c=close_window"); + + try testing.expectEqual(1, keybinds.set.bindings.count()); + try testing.expectEqual(2, keybinds.tables.count()); + + // Clear all keybinds + try keybinds.parseCLI(alloc, "clear"); + + // Both root set and tables should be cleared + try testing.expectEqual(0, keybinds.set.bindings.count()); + try testing.expectEqual(0, keybinds.tables.count()); + } + + test "parseCLI reset clears tables" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Add bindings to tables + try keybinds.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + try keybinds.parseCLI(alloc, "bar/shift+b=paste_from_clipboard"); + + try testing.expectEqual(2, keybinds.tables.count()); + + // Reset to defaults (empty value) + try keybinds.parseCLI(alloc, ""); + + // Tables should be cleared, root set has defaults + try testing.expectEqual(0, keybinds.tables.count()); + try testing.expect(keybinds.set.bindings.count() > 0); + } }; /// See "font-codepoint-map" for documentation. @@ -7401,6 +8178,7 @@ pub const ShellIntegration = enum { bash, elvish, fish, + nushell, zsh, }; @@ -7414,16 +8192,42 @@ pub const ShellIntegrationFeatures = packed struct { path: bool = true, }; -pub const RepeatableCommand = struct { - value: std.ArrayListUnmanaged(inputpkg.Command) = .empty, +pub const SplitPreserveZoom = packed struct { + navigation: bool = false, +}; - pub fn init(self: *RepeatableCommand, alloc: Allocator) !void { +pub const RepeatableCommand = struct { + const Self = @This(); + + value: std.ArrayListUnmanaged(inputpkg.Command) = .empty, + value_c: std.ArrayListUnmanaged(inputpkg.Command.C) = .empty, + + /// ghostty_config_command_list_s + pub const C = extern struct { + commands: [*]inputpkg.Command.C, + len: usize, + }; + + pub fn cval(self: *const Self) C { + return .{ + .commands = self.value_c.items.ptr, + .len = self.value_c.items.len, + }; + } + + pub fn init(self: *Self, alloc: Allocator) !void { self.value = .empty; + self.value_c = .empty; + errdefer { + self.value.deinit(alloc); + self.value_c.deinit(alloc); + } try self.value.appendSlice(alloc, inputpkg.command.defaults); + try self.value_c.appendSlice(alloc, inputpkg.command.defaultsC); } pub fn parseCLI( - self: *RepeatableCommand, + self: *Self, alloc: Allocator, input_: ?[]const u8, ) !void { @@ -7431,26 +8235,36 @@ pub const RepeatableCommand = struct { const input = input_ orelse ""; if (input.len == 0) { self.value.clearRetainingCapacity(); + self.value_c.clearRetainingCapacity(); return; } + // Reserve space in our lists + try self.value.ensureUnusedCapacity(alloc, 1); + try self.value_c.ensureUnusedCapacity(alloc, 1); + const cmd = try cli.args.parseAutoStruct( inputpkg.Command, alloc, input, null, ); - try self.value.append(alloc, cmd); + const cmd_c = try cmd.cval(alloc); + self.value.appendAssumeCapacity(cmd); + self.value_c.appendAssumeCapacity(cmd_c); } /// Deep copy of the struct. Required by Config. - pub fn clone(self: *const RepeatableCommand, alloc: Allocator) Allocator.Error!RepeatableCommand { + pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { const value = try self.value.clone(alloc); for (value.items) |*item| { item.* = try item.clone(alloc); } - return .{ .value = value }; + return .{ + .value = value, + .value_c = try self.value_c.clone(alloc), + }; } /// Compare if two of our value are equal. Required by Config. @@ -7606,6 +8420,50 @@ pub const RepeatableCommand = struct { try testing.expectEqualStrings("kurwa", item.action.text); } } + + test "RepeatableCommand cval" { + const testing = std.testing; + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: RepeatableCommand = .{}; + try list.parseCLI(alloc, "title:Foo,action:ignore"); + try list.parseCLI(alloc, "title:Bar,description:bobr,action:text:ale bydle"); + + try testing.expectEqual(@as(usize, 2), list.value.items.len); + try testing.expectEqual(@as(usize, 2), list.value_c.items.len); + + const cv = list.cval(); + try testing.expectEqual(@as(usize, 2), cv.len); + + // First entry + try testing.expectEqualStrings("Foo", std.mem.sliceTo(cv.commands[0].title, 0)); + try testing.expectEqualStrings("ignore", std.mem.sliceTo(cv.commands[0].action_key, 0)); + try testing.expectEqualStrings("ignore", std.mem.sliceTo(cv.commands[0].action, 0)); + + // Second entry + try testing.expectEqualStrings("Bar", std.mem.sliceTo(cv.commands[1].title, 0)); + try testing.expectEqualStrings("bobr", std.mem.sliceTo(cv.commands[1].description, 0)); + try testing.expectEqualStrings("text", std.mem.sliceTo(cv.commands[1].action_key, 0)); + try testing.expectEqualStrings("text:ale bydle", std.mem.sliceTo(cv.commands[1].action, 0)); + } + + test "RepeatableCommand cval cleared" { + const testing = std.testing; + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: RepeatableCommand = .{}; + try list.parseCLI(alloc, "title:Foo,action:ignore"); + try testing.expectEqual(@as(usize, 1), list.cval().len); + + try list.parseCLI(alloc, ""); + try testing.expectEqual(@as(usize, 0), list.cval().len); + } }; /// OSC 4, 10, 11, and 12 default color reporting format. @@ -7875,7 +8733,7 @@ pub const WindowNewTabPosition = enum { /// See macos-dock-drop-behavior pub const MacOSDockDropBehavior = enum { @"new-tab", - window, + @"new-window", }; /// See window-show-tab-bar @@ -8315,6 +9173,8 @@ pub const AutoUpdate = enum { pub const BackgroundBlur = union(enum) { false, true, + @"macos-glass-regular", + @"macos-glass-clear", radius: u8, pub fn parseCLI(self: *BackgroundBlur, input: ?[]const u8) !void { @@ -8324,14 +9184,35 @@ pub const BackgroundBlur = union(enum) { return; }; - self.* = if (cli.args.parseBool(input_)) |b| - if (b) .true else .false - else |_| - .{ .radius = std.fmt.parseInt( - u8, - input_, - 0, - ) catch return error.InvalidValue }; + // Try to parse normal bools + if (cli.args.parseBool(input_)) |b| { + self.* = if (b) .true else .false; + return; + } else |_| {} + + // Try to parse enums + if (std.meta.stringToEnum( + std.meta.Tag(BackgroundBlur), + input_, + )) |v| switch (v) { + inline else => |tag| tag: { + // We can only parse void types + const info = std.meta.fieldInfo(BackgroundBlur, tag); + if (info.type != void) break :tag; + self.* = @unionInit( + BackgroundBlur, + @tagName(tag), + {}, + ); + return; + }, + }; + + self.* = .{ .radius = std.fmt.parseInt( + u8, + input_, + 0, + ) catch return error.InvalidValue }; } pub fn enabled(self: BackgroundBlur) bool { @@ -8339,14 +9220,24 @@ pub const BackgroundBlur = union(enum) { .false => false, .true => true, .radius => |v| v > 0, + + // We treat these as true because they both imply some blur! + // This has the effect of making the standard blur happen on + // Linux. + .@"macos-glass-regular", .@"macos-glass-clear" => true, }; } - pub fn cval(self: BackgroundBlur) u8 { + pub fn cval(self: BackgroundBlur) i16 { return switch (self) { .false => 0, .true => 20, .radius => |v| v, + // I hate sentinel values like this but this is only for + // our macOS application currently. We can switch to a proper + // tagged union if we ever need to. + .@"macos-glass-regular" => -1, + .@"macos-glass-clear" => -2, }; } @@ -8358,6 +9249,8 @@ pub const BackgroundBlur = union(enum) { .false => try formatter.formatEntry(bool, false), .true => try formatter.formatEntry(bool, true), .radius => |v| try formatter.formatEntry(u8, v), + .@"macos-glass-regular" => try formatter.formatEntry([]const u8, "macos-glass-regular"), + .@"macos-glass-clear" => try formatter.formatEntry([]const u8, "macos-glass-clear"), } } @@ -8377,6 +9270,12 @@ pub const BackgroundBlur = union(enum) { try v.parseCLI("42"); try testing.expectEqual(42, v.radius); + try v.parseCLI("macos-glass-regular"); + try testing.expectEqual(.@"macos-glass-regular", v); + + try v.parseCLI("macos-glass-clear"); + try testing.expectEqual(.@"macos-glass-clear", v); + try testing.expectError(error.InvalidValue, v.parseCLI("")); try testing.expectError(error.InvalidValue, v.parseCLI("aaaa")); try testing.expectError(error.InvalidValue, v.parseCLI("420")); @@ -9491,3 +10390,22 @@ test "compatibility: removed bold-is-bright" { ); } } + +test "compatibility: window new-window" { + const testing = std.testing; + const alloc = testing.allocator; + + { + var cfg = try Config.default(alloc); + defer cfg.deinit(); + var it: TestIterator = .{ .data = &.{ + "--macos-dock-drop-behavior=window", + } }; + try cfg.loadIter(alloc, &it); + try cfg.finalize(); + try testing.expectEqual( + MacOSDockDropBehavior.@"new-window", + cfg.@"macos-dock-drop-behavior", + ); + } +} diff --git a/src/config/c_get.zig b/src/config/c_get.zig index f235f596a..dcfdc6716 100644 --- a/src/config/c_get.zig +++ b/src/config/c_get.zig @@ -193,20 +193,48 @@ test "c_get: background-blur" { { c.@"background-blur" = .false; - var cval: u8 = undefined; + var cval: i16 = undefined; try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval))); try testing.expectEqual(0, cval); } { c.@"background-blur" = .true; - var cval: u8 = undefined; + var cval: i16 = undefined; try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval))); try testing.expectEqual(20, cval); } { c.@"background-blur" = .{ .radius = 42 }; - var cval: u8 = undefined; + var cval: i16 = undefined; try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval))); try testing.expectEqual(42, cval); } + { + c.@"background-blur" = .@"macos-glass-regular"; + var cval: i16 = undefined; + try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval))); + try testing.expectEqual(-1, cval); + } + { + c.@"background-blur" = .@"macos-glass-clear"; + var cval: i16 = undefined; + try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval))); + try testing.expectEqual(-2, cval); + } +} + +test "c_get: split-preserve-zoom" { + const testing = std.testing; + const alloc = testing.allocator; + + var c = try Config.default(alloc); + defer c.deinit(); + + var bits: c_uint = undefined; + try testing.expect(get(&c, .@"split-preserve-zoom", @ptrCast(&bits))); + try testing.expectEqual(@as(c_uint, 0), bits); + + c.@"split-preserve-zoom".navigation = true; + try testing.expect(get(&c, .@"split-preserve-zoom", @ptrCast(&bits))); + try testing.expectEqual(@as(c_uint, 1), bits); } diff --git a/src/config/theme.zig b/src/config/theme.zig index 7ba6e5885..8776fb1bf 100644 --- a/src/config/theme.zig +++ b/src/config/theme.zig @@ -221,7 +221,7 @@ pub fn open( // Unlikely scenario: the theme doesn't exist. In this case, we reset // our iterator, reiterate over in order to build a better error message. - // This does double allocate some memory but for errors I think thats + // This does double allocate some memory but for errors I think that's // fine. it.reset(); while (try it.next()) |loc| { diff --git a/src/config/url.zig b/src/config/url.zig index da3928aff..fdbc964d7 100644 --- a/src/config/url.zig +++ b/src/config/url.zig @@ -26,7 +26,7 @@ pub const regex = "(?:" ++ url_schemes ++ \\)(?: ++ ipv6_url_pattern ++ - \\|[\w\-.~:/?#@!$&*+,;=%]+(?:[\(\[]\w*[\)\]])?)+(? return std.mem.eql(u8, old, new), + + []const [:0]const u8, + => { + if (old.len != new.len) return false; + for (old, new) |a, b| { + if (!std.mem.eql(u8, a, b)) return false; + } + + return true; + }, + + else => {}, + } + + // Back into types of types + switch (@typeInfo(T)) { + .void => return true, + + inline .bool, + .int, + .float, + .@"enum", + => return old == new, + + .optional => |info| { + if (old == null and new == null) return true; + if (old == null or new == null) return false; + return deepEqual(info.child, old.?, new.?); + }, + + .array => |info| for (old, new) |old_elem, new_elem| { + if (!deepEqual( + info.child, + old_elem, + new_elem, + )) return false; + } else return true, + + .@"struct" => |info| { + if (@hasDecl(T, "equal")) return old.equal(new); + + // If a struct doesn't declare an "equal" function, we fall back + // to a recursive field-by-field compare. + inline for (info.fields) |field_info| { + if (!deepEqual( + field_info.type, + @field(old, field_info.name), + @field(new, field_info.name), + )) return false; + } + return true; + }, + + .@"union" => |info| { + if (@hasDecl(T, "equal")) return old.equal(new); + + const tag_type = info.tag_type.?; + const old_tag = std.meta.activeTag(old); + const new_tag = std.meta.activeTag(new); + if (old_tag != new_tag) return false; + + inline for (info.fields) |field_info| { + if (@field(tag_type, field_info.name) == old_tag) { + return deepEqual( + field_info.type, + @field(old, field_info.name), + @field(new, field_info.name), + ); + } + } + + unreachable; + }, + + else => { + @compileLog(T); + @compileError("unsupported field type"); + }, + } +} /// Generic, recursive equality testing utility using approximate comparison for /// floats and equality for everything else /// -/// Based on `std.testing.expectEqual` and `std.testing.expectEqualSlices`. +/// Based on `testing.expectEqual` and `testing.expectEqualSlices`. /// /// The relative tolerance is currently hardcoded to `sqrt(eps(float_type))`. pub inline fn expectApproxEqual(expected: anytype, actual: anytype) !void { @@ -59,7 +150,7 @@ fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void { if (union_info.tag_type == null) { // untagged unions can only be compared bitwise, // so expectEqual is all we need - std.testing.expectEqual(expected, actual) catch { + testing.expectEqual(expected, actual) catch { return error.TestExpectedApproxEqual; }; } @@ -69,7 +160,7 @@ fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void { const expectedTag = @as(Tag, expected); const actualTag = @as(Tag, actual); - std.testing.expectEqual(expectedTag, actualTag) catch { + testing.expectEqual(expectedTag, actualTag) catch { return error.TestExpectedApproxEqual; }; @@ -84,23 +175,23 @@ fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void { }; // we only reach this point if there's at least one null or error, // in which case expectEqual is all we need - std.testing.expectEqual(expected, actual) catch { + testing.expectEqual(expected, actual) catch { return error.TestExpectedApproxEqual; }; }, // fall back to expectEqual for everything else - else => std.testing.expectEqual(expected, actual) catch { + else => testing.expectEqual(expected, actual) catch { return error.TestExpectedApproxEqual; }, } } -/// Copy of std.testing.print (not public) +/// Copy of testing.print (not public) fn print(comptime fmt: []const u8, args: anytype) void { if (@inComptime()) { @compileError(std.fmt.comptimePrint(fmt, args)); - } else if (std.testing.backend_can_print) { + } else if (testing.backend_can_print) { std.debug.print(fmt, args); } } @@ -145,3 +236,195 @@ test "expectApproxEqual struct" { try expectApproxEqual(a, b); } + +test "deepEqual void" { + try testing.expect(deepEqual(void, {}, {})); +} + +test "deepEqual bool" { + try testing.expect(deepEqual(bool, true, true)); + try testing.expect(deepEqual(bool, false, false)); + try testing.expect(!deepEqual(bool, true, false)); + try testing.expect(!deepEqual(bool, false, true)); +} + +test "deepEqual int" { + try testing.expect(deepEqual(i32, 42, 42)); + try testing.expect(deepEqual(i32, -100, -100)); + try testing.expect(!deepEqual(i32, 42, 43)); + try testing.expect(deepEqual(u64, 0, 0)); + try testing.expect(!deepEqual(u64, 0, 1)); +} + +test "deepEqual float" { + try testing.expect(deepEqual(f32, 1.0, 1.0)); + try testing.expect(!deepEqual(f32, 1.0, 1.1)); + try testing.expect(deepEqual(f64, 3.14159, 3.14159)); + try testing.expect(!deepEqual(f64, 3.14159, 3.14158)); +} + +test "deepEqual enum" { + const Color = enum { red, green, blue }; + try testing.expect(deepEqual(Color, .red, .red)); + try testing.expect(deepEqual(Color, .blue, .blue)); + try testing.expect(!deepEqual(Color, .red, .green)); + try testing.expect(!deepEqual(Color, .green, .blue)); +} + +test "deepEqual []const u8" { + try testing.expect(deepEqual([]const u8, "hello", "hello")); + try testing.expect(deepEqual([]const u8, "", "")); + try testing.expect(!deepEqual([]const u8, "hello", "world")); + try testing.expect(!deepEqual([]const u8, "hello", "hell")); + try testing.expect(!deepEqual([]const u8, "hello", "hello!")); +} + +test "deepEqual [:0]const u8" { + try testing.expect(deepEqual([:0]const u8, "foo", "foo")); + try testing.expect(!deepEqual([:0]const u8, "foo", "bar")); + try testing.expect(!deepEqual([:0]const u8, "foo", "fo")); +} + +test "deepEqual []const [:0]const u8" { + const a: []const [:0]const u8 = &.{ "one", "two", "three" }; + const b: []const [:0]const u8 = &.{ "one", "two", "three" }; + const c: []const [:0]const u8 = &.{ "one", "two" }; + const d: []const [:0]const u8 = &.{ "one", "two", "four" }; + const e: []const [:0]const u8 = &.{}; + + try testing.expect(deepEqual([]const [:0]const u8, a, b)); + try testing.expect(!deepEqual([]const [:0]const u8, a, c)); + try testing.expect(!deepEqual([]const [:0]const u8, a, d)); + try testing.expect(deepEqual([]const [:0]const u8, e, e)); + try testing.expect(!deepEqual([]const [:0]const u8, a, e)); +} + +test "deepEqual optional" { + try testing.expect(deepEqual(?i32, null, null)); + try testing.expect(deepEqual(?i32, 42, 42)); + try testing.expect(!deepEqual(?i32, null, 42)); + try testing.expect(!deepEqual(?i32, 42, null)); + try testing.expect(!deepEqual(?i32, 42, 43)); +} + +test "deepEqual optional nested" { + const Nested = struct { x: i32, y: i32 }; + try testing.expect(deepEqual(?Nested, null, null)); + try testing.expect(deepEqual(?Nested, .{ .x = 1, .y = 2 }, .{ .x = 1, .y = 2 })); + try testing.expect(!deepEqual(?Nested, .{ .x = 1, .y = 2 }, .{ .x = 1, .y = 3 })); + try testing.expect(!deepEqual(?Nested, .{ .x = 1, .y = 2 }, null)); +} + +test "deepEqual array" { + try testing.expect(deepEqual([3]i32, .{ 1, 2, 3 }, .{ 1, 2, 3 })); + try testing.expect(!deepEqual([3]i32, .{ 1, 2, 3 }, .{ 1, 2, 4 })); + try testing.expect(!deepEqual([3]i32, .{ 1, 2, 3 }, .{ 0, 2, 3 })); + try testing.expect(deepEqual([0]i32, .{}, .{})); +} + +test "deepEqual nested array" { + const a = [2][2]i32{ .{ 1, 2 }, .{ 3, 4 } }; + const b = [2][2]i32{ .{ 1, 2 }, .{ 3, 4 } }; + const c = [2][2]i32{ .{ 1, 2 }, .{ 3, 5 } }; + + try testing.expect(deepEqual([2][2]i32, a, b)); + try testing.expect(!deepEqual([2][2]i32, a, c)); +} + +test "deepEqual struct" { + const Point = struct { x: i32, y: i32 }; + try testing.expect(deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 10, .y = 20 })); + try testing.expect(!deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 10, .y = 21 })); + try testing.expect(!deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 11, .y = 20 })); +} + +test "deepEqual struct nested" { + const Inner = struct { value: i32 }; + const Outer = struct { a: Inner, b: Inner }; + + const x = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 2 } }; + const y = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 2 } }; + const z = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 3 } }; + + try testing.expect(deepEqual(Outer, x, y)); + try testing.expect(!deepEqual(Outer, x, z)); +} + +test "deepEqual struct with equal decl" { + const Custom = struct { + value: i32, + + pub fn equal(self: @This(), other: @This()) bool { + return @mod(self.value, 10) == @mod(other.value, 10); + } + }; + + try testing.expect(deepEqual(Custom, .{ .value = 5 }, .{ .value = 15 })); + try testing.expect(deepEqual(Custom, .{ .value = 100 }, .{ .value = 0 })); + try testing.expect(!deepEqual(Custom, .{ .value = 5 }, .{ .value = 6 })); +} + +test "deepEqual union" { + const Value = union(enum) { + int: i32, + float: f32, + none, + }; + + try testing.expect(deepEqual(Value, .{ .int = 42 }, .{ .int = 42 })); + try testing.expect(!deepEqual(Value, .{ .int = 42 }, .{ .int = 43 })); + try testing.expect(!deepEqual(Value, .{ .int = 42 }, .{ .float = 42.0 })); + try testing.expect(deepEqual(Value, .none, .none)); + try testing.expect(!deepEqual(Value, .none, .{ .int = 0 })); +} + +test "deepEqual union with equal decl" { + const Value = union(enum) { + num: i32, + str: []const u8, + + pub fn equal(self: @This(), other: @This()) bool { + return switch (self) { + .num => |n| switch (other) { + .num => |m| @mod(n, 10) == @mod(m, 10), + else => false, + }, + .str => |s| switch (other) { + .str => |t| s.len == t.len, + else => false, + }, + }; + } + }; + + try testing.expect(deepEqual(Value, .{ .num = 5 }, .{ .num = 25 })); + try testing.expect(!deepEqual(Value, .{ .num = 5 }, .{ .num = 6 })); + try testing.expect(deepEqual(Value, .{ .str = "abc" }, .{ .str = "xyz" })); + try testing.expect(!deepEqual(Value, .{ .str = "abc" }, .{ .str = "ab" })); +} + +test "deepEqual array of structs" { + const Item = struct { id: i32, name: []const u8 }; + const a = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "two" } }; + const b = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "two" } }; + const c = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "TWO" } }; + + try testing.expect(deepEqual([2]Item, a, b)); + try testing.expect(!deepEqual([2]Item, a, c)); +} + +test "deepEqual struct with optional field" { + const Config = struct { name: []const u8, port: ?u16 }; + + try testing.expect(deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = 8080 })); + try testing.expect(deepEqual(Config, .{ .name = "app", .port = null }, .{ .name = "app", .port = null })); + try testing.expect(!deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = null })); + try testing.expect(!deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = 8081 })); +} + +test "deepEqual struct with array field" { + const Data = struct { values: [3]i32 }; + + try testing.expect(deepEqual(Data, .{ .values = .{ 1, 2, 3 } }, .{ .values = .{ 1, 2, 3 } })); + try testing.expect(!deepEqual(Data, .{ .values = .{ 1, 2, 3 } }, .{ .values = .{ 1, 2, 4 } })); +} diff --git a/src/datastruct/main.zig b/src/datastruct/main.zig index 64a29269e..bfee23427 100644 --- a/src/datastruct/main.zig +++ b/src/datastruct/main.zig @@ -19,4 +19,6 @@ pub const SplitTree = split_tree.SplitTree; test { @import("std").testing.refAllDecls(@This()); + + _ = @import("comparison.zig"); } diff --git a/src/datastruct/split_tree.zig b/src/datastruct/split_tree.zig index be24187f6..0a4c0bdbd 100644 --- a/src/datastruct/split_tree.zig +++ b/src/datastruct/split_tree.zig @@ -170,6 +170,19 @@ pub fn SplitTree(comptime V: type) type { return self.nodes.len == 0; } + /// Returns true if this tree has more than one split (i.e., the root + /// is a split node). This is useful for determining if actions like + /// resize_split or toggle_split_zoom are performable. + pub fn isSplit(self: *const Self) bool { + // An empty tree is not split. + if (self.isEmpty()) return false; + // The root node is at index 0. If it's a split, we have multiple splits. + return switch (self.nodes[0]) { + .split => true, + .leaf => false, + }; + } + /// An iterator over all the views in the tree. pub fn iterator( self: *const Self, @@ -760,9 +773,9 @@ pub fn SplitTree(comptime V: type) type { /// Resize the nearest split matching the layout by the given ratio. /// Positive is right and down. /// - /// The ratio is a value between 0 and 1 representing the percentage - /// to move the divider in the given direction. The percentage is - /// of the entire grid size, not just the specific split size. + /// The ratio is a signed delta representing the percentage to move + /// the divider. The percentage is of the entire grid size, not just + /// the specific split size. /// We use the entire grid size because that's what Ghostty's /// `resize_split` keybind does, because it maps to a general human /// understanding of moving a split relative to the entire window @@ -781,7 +794,7 @@ pub fn SplitTree(comptime V: type) type { layout: Split.Layout, ratio: f16, ) Allocator.Error!Self { - assert(ratio >= 0 and ratio <= 1); + assert(ratio >= -1 and ratio <= 1); assert(!std.math.isNan(ratio)); assert(!std.math.isInf(ratio)); @@ -1326,6 +1339,36 @@ const TestView = struct { } }; +test "SplitTree: isSplit" { + const testing = std.testing; + const alloc = testing.allocator; + + // Empty tree should not be split + var empty: TestTree = .empty; + defer empty.deinit(); + try testing.expect(!empty.isSplit()); + + // Single node tree should not be split + var v1: TestView = .{ .label = "A" }; + var single: TestTree = try TestTree.init(alloc, &v1); + defer single.deinit(); + try testing.expect(!single.isSplit()); + + // Split tree should be split + var v2: TestView = .{ .label = "B" }; + var tree2: TestTree = try TestTree.init(alloc, &v2); + defer tree2.deinit(); + var split = try single.split( + alloc, + .root, + .right, + 0.5, + &tree2, + ); + defer split.deinit(); + try testing.expect(split.isSplit()); +} + test "SplitTree: empty tree" { const testing = std.testing; const alloc = testing.allocator; @@ -2007,6 +2050,32 @@ test "SplitTree: resize" { \\ ); } + + // Resize the other direction (negative ratio) + { + var resized = try split.resize( + alloc, + at: { + var it = split.iterator(); + break :at while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "B")) { + break entry.handle; + } + } else return error.NotFound; + }, + .horizontal, // resize left + -0.25, + ); + defer resized.deinit(); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(resized, .formatDiagram)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+---++-------------+ + \\| A || B | + \\+---++-------------+ + \\ + ); + } } test "SplitTree: clone empty tree" { diff --git a/src/extra/bash.zig b/src/extra/bash.zig index ee9a7895c..0cea3e317 100644 --- a/src/extra/bash.zig +++ b/src/extra/bash.zig @@ -158,9 +158,6 @@ fn writeBashCompletions(writer: *std.Io.Writer) !void { ); for (@typeInfo(Action).@"enum".fields) |field| { - if (std.mem.eql(u8, "help", field.name)) continue; - if (std.mem.eql(u8, "version", field.name)) continue; - const options = @field(Action, field.name).options(); // assumes options will never be created with only <_name> members if (@typeInfo(options).@"struct".fields.len == 0) continue; @@ -194,9 +191,6 @@ fn writeBashCompletions(writer: *std.Io.Writer) !void { ); for (@typeInfo(Action).@"enum".fields) |field| { - if (std.mem.eql(u8, "help", field.name)) continue; - if (std.mem.eql(u8, "version", field.name)) continue; - const options = @field(Action, field.name).options(); if (@typeInfo(options).@"struct".fields.len == 0) continue; @@ -272,9 +266,6 @@ fn writeBashCompletions(writer: *std.Io.Writer) !void { ); for (@typeInfo(Action).@"enum".fields) |field| { - if (std.mem.eql(u8, "help", field.name)) continue; - if (std.mem.eql(u8, "version", field.name)) continue; - try writer.writeAll(pad1 ++ "topLevel+=\" +" ++ field.name ++ "\"\n"); } @@ -296,7 +287,7 @@ fn writeBashCompletions(writer: *std.Io.Writer) !void { \\ else prev="${COMP_WORDS[COMP_CWORD-1]}" \\ fi \\ - \\ # current completion is double quoted add a space so the curor progresses + \\ # current completion is double quoted add a space so the cursor progresses \\ if [[ "$2" == \"*\" ]]; then \\ COMPREPLY=( "$cur " ); \\ return; diff --git a/src/extra/fish.zig b/src/extra/fish.zig index 12343c62f..73fa9a706 100644 --- a/src/extra/fish.zig +++ b/src/extra/fish.zig @@ -28,8 +28,6 @@ fn writeCompletions(writer: *std.Io.Writer) !void { try writer.writeAll("set -l commands \""); var count: usize = 0; for (@typeInfo(Action).@"enum".fields) |field| { - if (std.mem.eql(u8, "help", field.name)) continue; - if (std.mem.eql(u8, "version", field.name)) continue; if (count > 0) try writer.writeAll(" "); try writer.writeAll("+"); try writer.writeAll(field.name); @@ -98,8 +96,6 @@ fn writeCompletions(writer: *std.Io.Writer) !void { try writer.writeAll("complete -c ghostty -n \"string match -q -- '+*' (commandline -pt)\" -f -a \""); var count: usize = 0; for (@typeInfo(Action).@"enum".fields) |field| { - if (std.mem.eql(u8, "help", field.name)) continue; - if (std.mem.eql(u8, "version", field.name)) continue; if (count > 0) try writer.writeAll(" "); try writer.writeAll("+"); try writer.writeAll(field.name); diff --git a/src/extra/vim.zig b/src/extra/vim.zig index 9140b83f8..062ccd2b6 100644 --- a/src/extra/vim.zig +++ b/src/extra/vim.zig @@ -10,7 +10,7 @@ pub const ftdetect = \\" \\" THIS FILE IS AUTO-GENERATED \\ - \\au BufRead,BufNewFile */ghostty/config,*/ghostty/themes/*,*.ghostty setf ghostty + \\au BufRead,BufNewFile */ghostty/config,*/*.ghostty/config,*/ghostty/themes/*,*.ghostty setf ghostty \\ ; pub const ftplugin = diff --git a/src/extra/zsh.zig b/src/extra/zsh.zig index 2fad4234a..376db807f 100644 --- a/src/extra/zsh.zig +++ b/src/extra/zsh.zig @@ -139,9 +139,6 @@ fn writeZshCompletions(writer: *std.Io.Writer) !void { var count: usize = 0; const padding = " "; for (@typeInfo(Action).@"enum".fields) |field| { - if (std.mem.eql(u8, "help", field.name)) continue; - if (std.mem.eql(u8, "version", field.name)) continue; - try writer.writeAll(padding ++ "'+"); try writer.writeAll(field.name); try writer.writeAll("'\n"); @@ -168,9 +165,6 @@ fn writeZshCompletions(writer: *std.Io.Writer) !void { { const padding = " "; for (@typeInfo(Action).@"enum".fields) |field| { - if (std.mem.eql(u8, "help", field.name)) continue; - if (std.mem.eql(u8, "version", field.name)) continue; - const options = @field(Action, field.name).options(); // assumes options will never be created with only <_name> members if (@typeInfo(options).@"struct".fields.len == 0) continue; diff --git a/src/font/Atlas.zig b/src/font/Atlas.zig index 0648c0edf..d12064576 100644 --- a/src/font/Atlas.zig +++ b/src/font/Atlas.zig @@ -20,6 +20,7 @@ const assert = @import("../quirks.zig").inlineAssert; const Allocator = std.mem.Allocator; const testing = std.testing; const fastmem = @import("../fastmem.zig"); +const tripwire = @import("../tripwire.zig"); const log = std.log.scoped(.atlas); @@ -91,7 +92,15 @@ pub const Region = extern struct { /// TODO: figure out optimal prealloc based on real world usage const node_prealloc: usize = 64; +pub const init_tw = tripwire.module(enum { + alloc_data, + alloc_nodes, +}, init); + pub fn init(alloc: Allocator, size: u32, format: Format) Allocator.Error!Atlas { + const tw = init_tw; + + try tw.check(.alloc_data); var result = Atlas{ .data = try alloc.alloc(u8, size * size * format.depth()), .size = size, @@ -101,6 +110,7 @@ pub fn init(alloc: Allocator, size: u32, format: Format) Allocator.Error!Atlas { errdefer result.deinit(alloc); // Prealloc some nodes. + try tw.check(.alloc_nodes); result.nodes = try .initCapacity(alloc, node_prealloc); // This sets up our initial state @@ -115,6 +125,10 @@ pub fn deinit(self: *Atlas, alloc: Allocator) void { self.* = undefined; } +pub const reserve_tw = tripwire.module(enum { + insert_node, +}, reserve); + /// Reserve a region within the atlas with the given width and height. /// /// May allocate to add a new rectangle into the internal list of rectangles. @@ -125,6 +139,8 @@ pub fn reserve( width: u32, height: u32, ) (Allocator.Error || Error)!Region { + const tw = reserve_tw; + // x, y are populated within :best_idx below var region: Region = .{ .x = 0, .y = 0, .width = width, .height = height }; @@ -162,11 +178,13 @@ pub fn reserve( }; // Insert our new node for this rectangle at the exact best index + try tw.check(.insert_node); try self.nodes.insert(alloc, best_idx, .{ .x = region.x, .y = region.y + height, .width = width, }); + errdefer comptime unreachable; // Optimize our rectangles var i: usize = best_idx + 1; @@ -287,15 +305,24 @@ pub fn setFromLarger( _ = self.modified.fetchAdd(1, .monotonic); } +pub const grow_tw = tripwire.module(enum { + ensure_node_capacity, + alloc_data, +}, grow); + // Grow the texture to the new size, preserving all previously written data. pub fn grow(self: *Atlas, alloc: Allocator, size_new: u32) Allocator.Error!void { + const tw = grow_tw; + assert(size_new >= self.size); if (size_new == self.size) return; // We reserve space ahead of time for the new node, so that we // won't have to handle any errors after allocating our new data. + try tw.check(.ensure_node_capacity); try self.nodes.ensureUnusedCapacity(alloc, 1); + try tw.check(.alloc_data); const data_new = try alloc.alloc( u8, size_new * size_new * self.format.depth(), @@ -355,7 +382,7 @@ pub fn clear(self: *Atlas) void { /// swapped because PPM expects RGB. This would be /// easy enough to fix so next time someone needs /// to debug a color atlas they should fix it. -pub fn dump(self: Atlas, writer: *std.Io.Writer) !void { +pub fn dump(self: Atlas, writer: *std.Io.Writer) std.Io.Writer.Error!void { try writer.print( \\P{c} \\{d} {d} @@ -562,7 +589,7 @@ test "exact fit" { try testing.expectError(Error.AtlasFull, atlas.reserve(alloc, 1, 1)); } -test "doesnt fit" { +test "doesn't fit" { const alloc = testing.allocator; var atlas = try init(alloc, 32, .grayscale); defer atlas.deinit(alloc); @@ -795,3 +822,68 @@ test "grow OOM" { try testing.expectEqual(@as(u8, 3), atlas.data[9]); try testing.expectEqual(@as(u8, 4), atlas.data[10]); } + +test "init error" { + // Test every failure point in `init` and ensure that we don't + // leak memory (testing.allocator verifies) since we're exiting early. + for (std.meta.tags(init_tw.FailPoint)) |tag| { + const tw = init_tw; + defer tw.end(.reset) catch unreachable; + tw.errorAlways(tag, error.OutOfMemory); + try testing.expectError( + error.OutOfMemory, + init(testing.allocator, 32, .grayscale), + ); + } +} + +test "reserve error" { + // Test every failure point in `reserve` and ensure that we don't + // leak memory (testing.allocator verifies) since we're exiting early. + for (std.meta.tags(reserve_tw.FailPoint)) |tag| { + const tw = reserve_tw; + defer tw.end(.reset) catch unreachable; + + var atlas = try init(testing.allocator, 32, .grayscale); + defer atlas.deinit(testing.allocator); + + tw.errorAlways(tag, error.OutOfMemory); + try testing.expectError( + error.OutOfMemory, + atlas.reserve(testing.allocator, 2, 2), + ); + } +} + +test "grow error" { + // Test every failure point in `grow` and ensure that we don't + // leak memory (testing.allocator verifies) since we're exiting early. + for (std.meta.tags(grow_tw.FailPoint)) |tag| { + const tw = grow_tw; + defer tw.end(.reset) catch unreachable; + + var atlas = try init(testing.allocator, 4, .grayscale); + defer atlas.deinit(testing.allocator); + + // Write some data to verify it's preserved after failed grow + const reg = try atlas.reserve(testing.allocator, 2, 2); + atlas.set(reg, &[_]u8{ 1, 2, 3, 4 }); + + const old_modified = atlas.modified.load(.monotonic); + const old_resized = atlas.resized.load(.monotonic); + + tw.errorAlways(tag, error.OutOfMemory); + try testing.expectError( + error.OutOfMemory, + atlas.grow(testing.allocator, atlas.size + 1), + ); + + // Verify atlas state is unchanged after failed grow + try testing.expectEqual(old_modified, atlas.modified.load(.monotonic)); + try testing.expectEqual(old_resized, atlas.resized.load(.monotonic)); + try testing.expectEqual(@as(u8, 1), atlas.data[5]); + try testing.expectEqual(@as(u8, 2), atlas.data[6]); + try testing.expectEqual(@as(u8, 3), atlas.data[9]); + try testing.expectEqual(@as(u8, 4), atlas.data[10]); + } +} diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 412098f10..5d7bfa519 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -196,8 +196,18 @@ pub fn getFace(self: *Collection, index: Index) !*Face { return try self.getFaceFromEntry(try self.getEntry(index)); } +pub const EntryError = error{ + /// Index represents a special font (built-in) and these don't + /// have an associated face. This should be caught upstream and use + /// alternate logic. + SpecialHasNoFace, + + /// Invalid index. + IndexOutOfBounds, +}; + /// Get the unaliased entry from an index -pub fn getEntry(self: *Collection, index: Index) !*Entry { +pub fn getEntry(self: *Collection, index: Index) EntryError!*Entry { if (index.special() != null) return error.SpecialHasNoFace; const list = self.faces.getPtr(index.style); if (index.idx >= list.len) return error.IndexOutOfBounds; diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index 52aedefc6..5fd729b30 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -20,6 +20,7 @@ const SharedGrid = @This(); const std = @import("std"); const assert = @import("../quirks.zig").inlineAssert; +const tripwire = @import("../tripwire.zig"); const Allocator = std.mem.Allocator; const renderer = @import("../renderer.zig"); const font = @import("main.zig"); @@ -61,6 +62,12 @@ metrics: Metrics, /// to review call sites to ensure they are using the lock correctly. lock: std.Thread.RwLock, +pub const init_tw = tripwire.module(enum { + codepoints_capacity, + glyphs_capacity, + reload_metrics, +}, init); + /// Initialize the grid. /// /// The resolver must have a collection that supports deferred loading @@ -74,6 +81,8 @@ pub fn init( alloc: Allocator, resolver: CodepointResolver, ) !SharedGrid { + const tw = init_tw; + // We need to support loading options since we use the size data assert(resolver.collection.load_options != null); @@ -92,10 +101,15 @@ pub fn init( // We set an initial capacity that can fit a good number of characters. // This number was picked empirically based on my own terminal usage. + try tw.check(.codepoints_capacity); try result.codepoints.ensureTotalCapacity(alloc, 128); + errdefer result.codepoints.deinit(alloc); + try tw.check(.glyphs_capacity); try result.glyphs.ensureTotalCapacity(alloc, 128); + errdefer result.glyphs.deinit(alloc); // Initialize our metrics. + try tw.check(.reload_metrics); try result.reloadMetrics(); return result; @@ -232,6 +246,10 @@ pub fn renderCodepoint( return try self.renderGlyph(alloc, index, glyph_index, opts); } +pub const renderGlyph_tw = tripwire.module(enum { + get_presentation, +}, renderGlyph); + /// Render a glyph index. This automatically determines the correct texture /// atlas to use and caches the result. pub fn renderGlyph( @@ -241,6 +259,8 @@ pub fn renderGlyph( glyph_index: u32, opts: RenderOptions, ) !Render { + const tw = renderGlyph_tw; + const key: GlyphKey = .{ .index = index, .glyph = glyph_index, .opts = opts }; // Fast path: the cache has the value. This is almost always true and @@ -257,8 +277,10 @@ pub fn renderGlyph( const gop = try self.glyphs.getOrPut(alloc, key); if (gop.found_existing) return gop.value_ptr.*; + errdefer self.glyphs.removeByPtr(gop.key_ptr); // Get the presentation to determine what atlas to use + try tw.check(.get_presentation); const p = try self.resolver.getPresentation(index, glyph_index); const atlas: *font.Atlas = switch (p) { .text => &self.atlas_grayscale, @@ -426,3 +448,93 @@ test getIndex { try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx); } } + +test "renderGlyph error after cache insert rolls back cache entry" { + // This test verifies that when renderGlyph fails after inserting a cache + // entry (via getOrPut), the errdefer properly removes the entry, preventing + // corrupted/uninitialized data from remaining in the cache. + + const testing = std.testing; + const alloc = testing.allocator; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var grid = try testGrid(.normal, alloc, lib); + defer grid.deinit(alloc); + + // Get the font index for 'A' + const idx = (try grid.getIndex(alloc, 'A', .regular, null)).?; + + // Get the glyph index for 'A' + const glyph_index = glyph_index: { + grid.lock.lockShared(); + defer grid.lock.unlockShared(); + const face = try grid.resolver.collection.getFace(idx); + break :glyph_index face.glyphIndex('A').?; + }; + + const render_opts: RenderOptions = .{ .grid_metrics = grid.metrics }; + const key: GlyphKey = .{ .index = idx, .glyph = glyph_index, .opts = render_opts }; + + // Verify the cache is empty for this glyph + try testing.expect(grid.glyphs.get(key) == null); + + // Set up tripwire to fail after cache insert. + // We use OutOfMemory as it's a valid error in the renderGlyph error set. + const tw = renderGlyph_tw; + defer tw.end(.reset) catch {}; + tw.errorAlways(.get_presentation, error.OutOfMemory); + + // This should fail due to the tripwire + try testing.expectError( + error.OutOfMemory, + grid.renderGlyph(alloc, idx, glyph_index, render_opts), + ); + + // The errdefer should have removed the cache entry, leaving the cache clean. + // Without the errdefer fix, this would contain garbage/uninitialized data. + try testing.expect(grid.glyphs.get(key) == null); +} + +test "init error" { + // Test every failure point in `init` and ensure that we don't + // leak memory (testing.allocator verifies) since we're exiting early. + // + // BUG: Currently this test will fail because init() is missing errdefer + // cleanup for codepoints and glyphs when late operations fail + // (ensureTotalCapacity, reloadMetrics). + const testing = std.testing; + const alloc = testing.allocator; + + for (std.meta.tags(init_tw.FailPoint)) |tag| { + const tw = init_tw; + defer tw.end(.reset) catch unreachable; + tw.errorAlways(tag, error.OutOfMemory); + + // Create a resolver for testing - we need to set up a minimal one. + // The caller is responsible for cleaning up the resolver if init fails. + var lib = try Library.init(alloc); + defer lib.deinit(); + + var c = Collection.init(); + c.load_options = .{ .library = lib }; + _ = try c.add(alloc, try .init( + lib, + font.embedded.regular, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); + + var resolver: CodepointResolver = .{ .collection = c }; + defer resolver.deinit(alloc); // Caller cleans up on init failure + + try testing.expectError( + error.OutOfMemory, + init(alloc, resolver), + ); + } +} diff --git a/src/font/embedded.zig b/src/font/embedded.zig index 1e496075d..8e0ae33b8 100644 --- a/src/font/embedded.zig +++ b/src/font/embedded.zig @@ -47,3 +47,9 @@ pub const monaspace_neon = @embedFile("res/MonaspaceNeon-Regular.otf"); /// Terminus TTF is a scalable font with bitmap glyphs at various sizes. pub const terminus_ttf = @embedFile("res/TerminusTTF-Regular.ttf"); + +/// Spleen is a monospaced bitmap font available in multiple formats. +/// Used for testing bitmap font support across different file formats. +pub const spleen_bdf = @embedFile("res/spleen-8x16.bdf"); +pub const spleen_pcf = @embedFile("res/spleen-8x16.pcf"); +pub const spleen_otb = @embedFile("res/spleen-8x16.otb"); diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index a6ef52c39..827753254 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -1284,3 +1284,153 @@ test "bitmap glyph" { } } } + +// Expected pixel pattern for Spleen 8x16 'A' (glyph index from char 'A') +// Derived from BDF BITMAP data: 00,00,7C,C6,C6,C6,FE,C6,C6,C6,C6,C6,00,00,00,00 +const spleen_A = + \\........ + \\........ + \\.#####.. + \\##...##. + \\##...##. + \\##...##. + \\#######. + \\##...##. + \\##...##. + \\##...##. + \\##...##. + \\##...##. + \\........ + \\........ + \\........ + \\........ +; +// Including the newline +const spleen_A_pitch = 9; +// Test parameters for bitmap font tests +const spleen_test_point_size = 12; +const spleen_test_dpi = 96; + +test "bitmap glyph BDF" { + const alloc = testing.allocator; + const testFont = font.embedded.spleen_bdf; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var atlas = try font.Atlas.init(alloc, 512, .grayscale); + defer atlas.deinit(alloc); + + // Spleen 8x16 is a pure bitmap font at 16px height + var ft_font = try Face.init(lib, testFont, .{ .size = .{ + .points = spleen_test_point_size, + .xdpi = spleen_test_dpi, + .ydpi = spleen_test_dpi, + } }); + defer ft_font.deinit(); + + // Get glyph index for 'A' + const glyph_index = ft_font.glyphIndex('A') orelse return error.GlyphNotFound; + + const glyph = try ft_font.renderGlyph( + alloc, + &atlas, + glyph_index, + .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) }, + ); + + // Verify dimensions match Spleen 8x16 + try testing.expectEqual(8, glyph.width); + try testing.expectEqual(16, glyph.height); + + // Verify pixel-perfect rendering + for (0..glyph.height) |y| { + for (0..glyph.width) |x| { + const pixel = spleen_A[y * spleen_A_pitch + x]; + try testing.expectEqual( + @as(u8, if (pixel == '#') 255 else 0), + atlas.data[(glyph.atlas_y + y) * atlas.size + (glyph.atlas_x + x)], + ); + } + } +} + +test "bitmap glyph PCF" { + const alloc = testing.allocator; + const testFont = font.embedded.spleen_pcf; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var atlas = try font.Atlas.init(alloc, 512, .grayscale); + defer atlas.deinit(alloc); + + var ft_font = try Face.init(lib, testFont, .{ .size = .{ + .points = spleen_test_point_size, + .xdpi = spleen_test_dpi, + .ydpi = spleen_test_dpi, + } }); + defer ft_font.deinit(); + + const glyph_index = ft_font.glyphIndex('A') orelse return error.GlyphNotFound; + + const glyph = try ft_font.renderGlyph( + alloc, + &atlas, + glyph_index, + .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) }, + ); + + try testing.expectEqual(8, glyph.width); + try testing.expectEqual(16, glyph.height); + + for (0..glyph.height) |y| { + for (0..glyph.width) |x| { + const pixel = spleen_A[y * spleen_A_pitch + x]; + try testing.expectEqual( + @as(u8, if (pixel == '#') 255 else 0), + atlas.data[(glyph.atlas_y + y) * atlas.size + (glyph.atlas_x + x)], + ); + } + } +} + +test "bitmap glyph OTB" { + const alloc = testing.allocator; + const testFont = font.embedded.spleen_otb; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var atlas = try font.Atlas.init(alloc, 512, .grayscale); + defer atlas.deinit(alloc); + + var ft_font = try Face.init(lib, testFont, .{ .size = .{ + .points = spleen_test_point_size, + .xdpi = spleen_test_dpi, + .ydpi = spleen_test_dpi, + } }); + defer ft_font.deinit(); + + const glyph_index = ft_font.glyphIndex('A') orelse return error.GlyphNotFound; + + const glyph = try ft_font.renderGlyph( + alloc, + &atlas, + glyph_index, + .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) }, + ); + + try testing.expectEqual(8, glyph.width); + try testing.expectEqual(16, glyph.height); + + for (0..glyph.height) |y| { + for (0..glyph.width) |x| { + const pixel = spleen_A[y * spleen_A_pitch + x]; + try testing.expectEqual( + @as(u8, if (pixel == '#') 255 else 0), + atlas.data[(glyph.atlas_y + y) * atlas.size + (glyph.atlas_x + x)], + ); + } + } +} diff --git a/src/font/res/BSD-2-Clause.txt b/src/font/res/BSD-2-Clause.txt new file mode 100644 index 000000000..4387948e8 --- /dev/null +++ b/src/font/res/BSD-2-Clause.txt @@ -0,0 +1,24 @@ +Copyright (c) 2018-2024, Frederic Cambus +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/src/font/res/README.md b/src/font/res/README.md index 5ad4b274f..b4d77a783 100644 --- a/src/font/res/README.md +++ b/src/font/res/README.md @@ -1,6 +1,6 @@ # Fonts and Licenses -This project uses several fonts which fall under the SIL Open Font License (OFL-1.1) and MIT License: +This project uses several fonts which fall under the SIL Open Font License (OFL-1.1), MIT License, and BSD 2-Clause License: - Code New Roman (OFL-1.1) - [© 2014 Sam Radian. All Rights Reserved.](https://github.com/chrissimpkins/codeface/blob/master/fonts/code-new-roman/license.txt) @@ -28,8 +28,12 @@ This project uses several fonts which fall under the SIL Open Font License (OFL- - Terminus TTF (OFL-1.1) - [Copyright (c) 2010-2020 Dimitar Toshkov Zhekov with Reserved Font Name "Terminus Font"](https://sourceforge.net/projects/terminus-font/) - [Copyright (c) 2011-2023 Tilman Blumenbach with Reserved Font Name "Terminus (TTF)"](https://files.ax86.net/terminus-ttf/) +- Spleen (BSD 2-Clause) + - [Copyright (c) 2018-2024, Frederic Cambus](https://github.com/fcambus/spleen) A full copy of the OFL license can be found at [OFL.txt](./OFL.txt). An accompanying FAQ is also available at . A full copy of the MIT license can be found at [MIT.txt](./MIT.txt). + +A full copy of the BSD 2-Clause license can be found at [BSD-2-Clause.txt](./BSD-2-Clause.txt). diff --git a/src/font/res/spleen-8x16.bdf b/src/font/res/spleen-8x16.bdf new file mode 100644 index 000000000..5c7c2684b --- /dev/null +++ b/src/font/res/spleen-8x16.bdf @@ -0,0 +1,22328 @@ +STARTFONT 2.1 +COMMENT /* +COMMENT * Spleen 8x16 2.1.0 +COMMENT * Copyright (c) 2018-2024, Frederic Cambus +COMMENT * https://www.cambus.net/ +COMMENT * +COMMENT * Created: 2018-08-11 +COMMENT * Last Updated: 2024-01-27 +COMMENT * +COMMENT * Spleen is released under the BSD 2-Clause license. +COMMENT * See LICENSE file for details. +COMMENT * +COMMENT * SPDX-License-Identifier: BSD-2-Clause +COMMENT */ +FONT -misc-spleen-medium-r-normal--16-160-72-72-C-80-ISO10646-1 +SIZE 16 72 72 +FONTBOUNDINGBOX 8 16 0 -4 +STARTPROPERTIES 20 +FAMILY_NAME "Spleen" +WEIGHT_NAME "Medium" +FONT_VERSION "2.1.0" +FOUNDRY "misc" +SLANT "R" +SETWIDTH_NAME "Normal" +PIXEL_SIZE 16 +POINT_SIZE 160 +RESOLUTION_X 72 +RESOLUTION_Y 72 +SPACING "C" +AVERAGE_WIDTH 80 +CHARSET_REGISTRY "ISO10646" +CHARSET_ENCODING "1" +MIN_SPACE 8 +FONT_ASCENT 12 +FONT_DESCENT 4 +COPYRIGHT "Copyright (c) 2018-2024, Frederic Cambus" +DEFAULT_CHAR 32 +_GBDFED_INFO "Edited with gbdfed 1.6." +ENDPROPERTIES +CHARS 969 +STARTCHAR SPACE +ENCODING 32 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR EXCLAMATION MARK +ENCODING 33 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +18 +18 +18 +18 +18 +00 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR QUOTATION MARK +ENCODING 34 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR NUMBER SIGN +ENCODING 35 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +6C +6C +FE +6C +6C +6C +6C +FE +6C +6C +00 +00 +00 +00 +ENDCHAR +STARTCHAR DOLLAR SIGN +ENCODING 36 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +7E +D0 +D0 +D0 +7C +16 +16 +16 +16 +FC +10 +00 +00 +00 +ENDCHAR +STARTCHAR PERCENT SIGN +ENCODING 37 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +06 +66 +6C +0C +18 +18 +30 +36 +66 +60 +00 +00 +00 +00 +ENDCHAR +STARTCHAR AMPERSAND +ENCODING 38 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +38 +6C +6C +6C +38 +70 +DA +CC +CC +7A +00 +00 +00 +00 +ENDCHAR +STARTCHAR APOSTROPHE +ENCODING 39 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +18 +18 +18 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LEFT PARENTHESIS +ENCODING 40 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0E +18 +30 +30 +60 +60 +60 +60 +30 +30 +18 +0E +00 +00 +00 +ENDCHAR +STARTCHAR RIGHT PARENTHESIS +ENCODING 41 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +70 +18 +0C +0C +06 +06 +06 +06 +0C +0C +18 +70 +00 +00 +00 +ENDCHAR +STARTCHAR ASTERISK +ENCODING 42 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +66 +3C +18 +FF +18 +3C +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR PLUS SIGN +ENCODING 43 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +18 +18 +7E +18 +18 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR COMMA +ENCODING 44 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +18 +18 +30 +00 +00 +00 +ENDCHAR +STARTCHAR HYPHEN-MINUS +ENCODING 45 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +7E +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR FULL STOP +ENCODING 46 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR SOLIDUS +ENCODING 47 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +0C +0C +18 +18 +30 +30 +60 +60 +C0 +C0 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT ZERO +ENCODING 48 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +CE +DE +F6 +E6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT ONE +ENCODING 49 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +38 +78 +58 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT TWO +ENCODING 50 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +06 +06 +0C +18 +30 +60 +C6 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT THREE +ENCODING 51 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +06 +06 +3C +06 +06 +06 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT FOUR +ENCODING 52 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +C0 +CC +CC +CC +CC +FE +0C +0C +0C +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT FIVE +ENCODING 53 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FE +C6 +C0 +C0 +FC +06 +06 +06 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT SIX +ENCODING 54 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C0 +C0 +FC +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT SEVEN +ENCODING 55 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FE +C6 +06 +06 +0C +18 +30 +30 +30 +30 +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT EIGHT +ENCODING 56 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +C6 +7C +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT NINE +ENCODING 57 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +C6 +C6 +7E +06 +06 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR COLON +ENCODING 58 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +18 +18 +00 +00 +00 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR SEMICOLON +ENCODING 59 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +18 +18 +00 +00 +00 +18 +18 +30 +00 +00 +00 +ENDCHAR +STARTCHAR LESS-THAN SIGN +ENCODING 60 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +06 +0C +18 +30 +60 +60 +30 +18 +0C +06 +00 +00 +00 +00 +ENDCHAR +STARTCHAR EQUALS SIGN +ENCODING 61 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +00 +00 +7E +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREATER-THAN SIGN +ENCODING 62 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +60 +30 +18 +0C +06 +06 +0C +18 +30 +60 +00 +00 +00 +00 +ENDCHAR +STARTCHAR QUESTION MARK +ENCODING 63 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +06 +0C +18 +30 +30 +00 +30 +30 +00 +00 +00 +00 +ENDCHAR +STARTCHAR COMMERCIAL AT +ENCODING 64 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +C2 +DA +DA +DA +DA +DE +C0 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A +ENCODING 65 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER B +ENCODING 66 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +C6 +C6 +C6 +FC +C6 +C6 +C6 +C6 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER C +ENCODING 67 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER D +ENCODING 68 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E +ENCODING 69 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER F +ENCODING 70 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER G +ENCODING 71 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +DE +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER H +ENCODING 72 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I +ENCODING 73 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +18 +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER J +ENCODING 74 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +18 +18 +18 +18 +18 +18 +18 +18 +F0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER K +ENCODING 75 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +CC +F8 +CC +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER L +ENCODING 76 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER M +ENCODING 77 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +EE +FE +D6 +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER N +ENCODING 78 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +E6 +E6 +D6 +D6 +CE +CE +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O +ENCODING 79 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER P +ENCODING 80 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +C6 +C6 +C6 +FC +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER Q +ENCODING 81 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +D6 +D6 +7C +18 +0C +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER R +ENCODING 82 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +C6 +C6 +C6 +FC +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER S +ENCODING 83 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +7C +06 +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER T +ENCODING 84 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FF +18 +18 +18 +18 +18 +18 +18 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U +ENCODING 85 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER V +ENCODING 86 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +6C +38 +10 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER W +ENCODING 87 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +D6 +FE +EE +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER X +ENCODING 88 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +6C +38 +6C +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER Y +ENCODING 89 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +7E +06 +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER Z +ENCODING 90 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FE +06 +06 +0C +18 +30 +60 +C0 +C0 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LEFT SQUARE BRACKET +ENCODING 91 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +3E +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +3E +00 +00 +00 +ENDCHAR +STARTCHAR REVERSE SOLIDUS +ENCODING 92 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +C0 +C0 +60 +60 +30 +30 +18 +18 +0C +0C +06 +06 +00 +00 +00 +ENDCHAR +STARTCHAR RIGHT SQUARE BRACKET +ENCODING 93 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +7C +0C +0C +0C +0C +0C +0C +0C +0C +0C +0C +7C +00 +00 +00 +ENDCHAR +STARTCHAR CIRCUMFLEX ACCENT +ENCODING 94 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +C6 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LOW LINE +ENCODING 95 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +FE +00 +ENDCHAR +STARTCHAR GRAVE ACCENT +ENCODING 96 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +30 +18 +0C +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A +ENCODING 97 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER B +ENCODING 98 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +C0 +C0 +FC +C6 +C6 +C6 +C6 +C6 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER C +ENCODING 99 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER D +ENCODING 100 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +06 +06 +06 +7E +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E +ENCODING 101 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER F +ENCODING 102 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +1E +30 +30 +30 +7C +30 +30 +30 +30 +30 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER G +ENCODING 103 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C6 +C6 +C6 +C6 +7C +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER H +ENCODING 104 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +C0 +C0 +FC +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I +ENCODING 105 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER J +ENCODING 106 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +18 +18 +18 +18 +18 +18 +18 +18 +18 +70 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER K +ENCODING 107 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +C0 +C0 +CC +D8 +F0 +F0 +D8 +CC +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER L +ENCODING 108 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +30 +30 +30 +30 +30 +30 +30 +30 +30 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER M +ENCODING 109 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +EC +D6 +D6 +D6 +D6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER N +ENCODING 110 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FC +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O +ENCODING 111 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER P +ENCODING 112 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FC +C6 +C6 +C6 +C6 +C6 +FC +C0 +C0 +C0 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER Q +ENCODING 113 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C6 +C6 +C6 +C6 +7E +06 +06 +06 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER R +ENCODING 114 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER S +ENCODING 115 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C0 +C0 +7C +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER T +ENCODING 116 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +30 +30 +30 +7C +30 +30 +30 +30 +30 +1E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U +ENCODING 117 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER V +ENCODING 118 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +C6 +C6 +6C +38 +10 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER W +ENCODING 119 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +D6 +D6 +D6 +D6 +6E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER X +ENCODING 120 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +6C +38 +38 +6C +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER Y +ENCODING 121 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER Z +ENCODING 122 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FE +06 +0C +18 +30 +60 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LEFT CURLY BRACKET +ENCODING 123 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0E +18 +18 +18 +18 +70 +70 +18 +18 +18 +18 +0E +00 +00 +00 +ENDCHAR +STARTCHAR VERTICAL LINE +ENCODING 124 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +00 +00 +00 +ENDCHAR +STARTCHAR RIGHT CURLY BRACKET +ENCODING 125 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +70 +18 +18 +18 +18 +0E +0E +18 +18 +18 +18 +70 +00 +00 +00 +ENDCHAR +STARTCHAR TILDE +ENCODING 126 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +32 +7E +4C +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR +ENCODING 127 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR NO-BREAK SPACE +ENCODING 160 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR INVERTED EXCLAMATION MARK +ENCODING 161 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +18 +18 +18 +18 +18 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CENT SIGN +ENCODING 162 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +08 +7E +C8 +C8 +C8 +C8 +C8 +7E +08 +00 +00 +00 +ENDCHAR +STARTCHAR POUND SIGN +ENCODING 163 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +38 +6C +60 +60 +60 +F8 +60 +60 +C0 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR CURRENCY SIGN +ENCODING 164 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +3C +66 +66 +66 +3C +66 +00 +00 +00 +00 +ENDCHAR +STARTCHAR YEN SIGN +ENCODING 165 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C3 +C3 +66 +3C +18 +3C +18 +3C +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BROKEN BAR +ENCODING 166 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +18 +18 +18 +00 +00 +18 +18 +18 +18 +18 +00 +00 +ENDCHAR +STARTCHAR SECTION SIGN +ENCODING 167 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +3C +66 +60 +30 +3C +66 +66 +66 +66 +3C +0C +06 +66 +3C +00 +ENDCHAR +STARTCHAR DIAERESIS +ENCODING 168 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +6C +6C +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR COPYRIGHT SIGN +ENCODING 169 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +82 +9A +A2 +A2 +A2 +9A +82 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR FEMININE ORDINAL INDICATOR +ENCODING 170 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +0C +3C +4C +3C +00 +7C +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LEFT-POINTING DOUBLE ANGLE QUOTATION MARK +ENCODING 171 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +33 +66 +CC +66 +33 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR NOT SIGN +ENCODING 172 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +FE +06 +06 +06 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR SOFT HYPHEN +ENCODING 173 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +3C +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR REGISTERED SIGN +ENCODING 174 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +82 +BA +AA +B2 +AA +AA +82 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR MACRON +ENCODING 175 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +7C +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR DEGREE SIGN +ENCODING 176 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +6C +6C +38 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR PLUS-MINUS SIGN +ENCODING 177 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +18 +18 +7E +18 +18 +00 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR SUPERSCRIPT TWO +ENCODING 178 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +4C +0C +38 +60 +7C +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR SUPERSCRIPT THREE +ENCODING 179 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +1C +26 +0C +06 +26 +1C +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR ACUTE ACCENT +ENCODING 180 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR MICRO SIGN +ENCODING 181 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +CC +CC +CC +CC +CC +CC +F6 +C0 +C0 +C0 +00 +ENDCHAR +STARTCHAR PILCROW SIGN +ENCODING 182 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +D6 +D6 +D6 +76 +16 +16 +16 +16 +16 +00 +00 +00 +00 +ENDCHAR +STARTCHAR MIDDLE DOT +ENCODING 183 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +18 +18 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CEDILLA +ENCODING 184 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +18 +18 +30 +00 +ENDCHAR +STARTCHAR SUPERSCRIPT ONE +ENCODING 185 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +38 +18 +18 +18 +3C +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR MASCULINE ORDINAL INDICATOR +ENCODING 186 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +6C +6C +38 +00 +00 +7C +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK +ENCODING 187 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +CC +66 +33 +66 +CC +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR VULGAR FRACTION ONE QUARTER +ENCODING 188 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +40 +C0 +40 +42 +46 +EC +18 +30 +70 +D4 +94 +1E +04 +04 +00 +ENDCHAR +STARTCHAR VULGAR FRACTION ONE HALF +ENCODING 189 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +40 +C0 +40 +42 +46 +EC +18 +30 +6C +D2 +82 +0C +10 +1E +00 +ENDCHAR +STARTCHAR VULGAR FRACTION THREE QUARTERS +ENCODING 190 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +90 +20 +12 +96 +6C +18 +30 +70 +D4 +94 +1E +04 +04 +00 +ENDCHAR +STARTCHAR INVERTED QUESTION MARK +ENCODING 191 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +18 +18 +30 +60 +C0 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH GRAVE +ENCODING 192 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +30 +18 +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH ACUTE +ENCODING 193 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH CIRCUMFLEX +ENCODING 194 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH TILDE +ENCODING 195 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +32 +4C +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH DIAERESIS +ENCODING 196 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +6C +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH RING ABOVE +ENCODING 197 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +38 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER AE +ENCODING 198 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +D8 +D8 +D8 +FE +D8 +D8 +D8 +D8 +DE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER C WITH CEDILLA +ENCODING 199 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH GRAVE +ENCODING 200 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +30 +18 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH ACUTE +ENCODING 201 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH CIRCUMFLEX +ENCODING 202 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH DIAERESIS +ENCODING 203 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +6C +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH GRAVE +ENCODING 204 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +30 +18 +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH ACUTE +ENCODING 205 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +0C +18 +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH CIRCUMFLEX +ENCODING 206 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH DIAERESIS +ENCODING 207 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +66 +66 +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER ETH +ENCODING 208 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +66 +66 +66 +F6 +66 +66 +66 +66 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER N WITH TILDE +ENCODING 209 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +32 +4C +00 +C6 +E6 +E6 +D6 +D6 +CE +CE +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH GRAVE +ENCODING 210 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +30 +18 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH ACUTE +ENCODING 211 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH CIRCUMFLEX +ENCODING 212 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH TILDE +ENCODING 213 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +32 +4C +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH DIAERESIS +ENCODING 214 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +6C +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR MULTIPLICATION SIGN +ENCODING 215 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +6C +38 +38 +6C +C6 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH STROKE +ENCODING 216 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +02 +7C +C6 +CE +CE +D6 +D6 +E6 +E6 +C6 +7C +80 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH GRAVE +ENCODING 217 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +30 +18 +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH ACUTE +ENCODING 218 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH CIRCUMFLEX +ENCODING 219 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH DIAERESIS +ENCODING 220 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +6C +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER Y WITH ACUTE +ENCODING 221 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +C6 +C6 +C6 +C6 +7E +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER THORN +ENCODING 222 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +C0 +C0 +FC +C6 +C6 +C6 +C6 +C6 +FC +C0 +C0 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER SHARP S +ENCODING 223 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +78 +CC +CC +CC +D8 +CC +C6 +C6 +D6 +DC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH GRAVE +ENCODING 224 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +30 +18 +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH ACUTE +ENCODING 225 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH CIRCUMFLEX +ENCODING 226 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH TILDE +ENCODING 227 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +32 +7E +4C +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH DIAERESIS +ENCODING 228 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +6C +6C +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH RING ABOVE +ENCODING 229 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +6C +38 +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER AE +ENCODING 230 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +6E +16 +16 +7E +D0 +D0 +6E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER C WITH CEDILLA +ENCODING 231 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C0 +C0 +C0 +C0 +C0 +7E +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH GRAVE +ENCODING 232 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +30 +18 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH ACUTE +ENCODING 233 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH CIRCUMFLEX +ENCODING 234 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH DIAERESIS +ENCODING 235 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +6C +6C +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH GRAVE +ENCODING 236 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +30 +18 +0C +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH ACUTE +ENCODING 237 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH CIRCUMFLEX +ENCODING 238 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +3C +66 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH DIAERESIS +ENCODING 239 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +66 +66 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER ETH +ENCODING 240 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +D8 +70 +D8 +0C +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER N WITH TILDE +ENCODING 241 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +32 +7E +4C +00 +FC +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH GRAVE +ENCODING 242 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +30 +18 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH ACUTE +ENCODING 243 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH CIRCUMFLEX +ENCODING 244 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH TILDE +ENCODING 245 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +32 +7E +4C +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH DIAERESIS +ENCODING 246 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +6C +6C +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIVISION SIGN +ENCODING 247 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +18 +18 +00 +7E +00 +18 +18 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH STROKE +ENCODING 248 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +02 +7C +C6 +CE +D6 +E6 +C6 +7C +80 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH GRAVE +ENCODING 249 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +30 +18 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH ACUTE +ENCODING 250 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH CIRCUMFLEX +ENCODING 251 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH DIAERESIS +ENCODING 252 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +6C +6C +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER Y WITH ACUTE +ENCODING 253 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER THORN +ENCODING 254 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +C0 +C0 +FC +C6 +C6 +C6 +C6 +C6 +FC +C0 +C0 +C0 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER Y WITH DIAERESIS +ENCODING 255 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +6C +6C +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH MACRON +ENCODING 256 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +7C +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH MACRON +ENCODING 257 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH BREVE +ENCODING 258 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH BREVE +ENCODING 259 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +6C +38 +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH OGONEK +ENCODING 260 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +C6 +0C +08 +06 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH OGONEK +ENCODING 261 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7C +06 +7E +C6 +C6 +C6 +7E +0C +08 +06 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER C WITH ACUTE +ENCODING 262 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +7E +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER C WITH ACUTE +ENCODING 263 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +7E +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER C WITH CIRCUMFLEX +ENCODING 264 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7E +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER C WITH CIRCUMFLEX +ENCODING 265 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +00 +7E +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER C WITH DOT ABOVE +ENCODING 266 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +00 +7E +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER C WITH DOT ABOVE +ENCODING 267 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +7E +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER C WITH CARON +ENCODING 268 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +7E +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER C WITH CARON +ENCODING 269 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +7E +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER D WITH CARON +ENCODING 270 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +FC +C6 +C6 +C6 +C6 +C6 +C6 +C6 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER D WITH CARON +ENCODING 271 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +16 +06 +06 +7E +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER D WITH STROKE +ENCODING 272 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +66 +66 +66 +F6 +66 +66 +66 +66 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER D WITH STROKE +ENCODING 273 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +06 +1F +06 +7E +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH MACRON +ENCODING 274 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +7C +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH MACRON +ENCODING 275 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH BREVE +ENCODING 276 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH BREVE +ENCODING 277 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +6C +38 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH DOT ABOVE +ENCODING 278 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH DOT ABOVE +ENCODING 279 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH OGONEK +ENCODING 280 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +C0 +7E +0C +08 +06 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH OGONEK +ENCODING 281 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +0C +08 +06 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH CARON +ENCODING 282 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH CARON +ENCODING 283 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER G WITH CIRCUMFLEX +ENCODING 284 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7E +C0 +C0 +C0 +DE +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER G WITH CIRCUMFLEX +ENCODING 285 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +00 +7E +C6 +C6 +C6 +C6 +C6 +7C +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER G WITH BREVE +ENCODING 286 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +00 +7E +C0 +C0 +C0 +DE +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER G WITH BREVE +ENCODING 287 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +6C +38 +00 +7E +C6 +C6 +C6 +C6 +C6 +7C +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER G WITH DOT ABOVE +ENCODING 288 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +00 +7E +C0 +C0 +C0 +DE +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER G WITH DOT ABOVE +ENCODING 289 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +7E +C6 +C6 +C6 +C6 +C6 +7C +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER G WITH CEDILLA +ENCODING 290 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +DE +C6 +C6 +C6 +C6 +7E +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER G WITH CEDILLA +ENCODING 291 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +18 +30 +00 +7E +C6 +C6 +C6 +C6 +C6 +7C +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER H WITH CIRCUMFLEX +ENCODING 292 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +C6 +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER H WITH CIRCUMFLEX +ENCODING 293 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +C0 +C0 +C0 +FC +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER H WITH STROKE +ENCODING 294 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +FF +C6 +C6 +FE +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER H WITH STROKE +ENCODING 295 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +F0 +C0 +FC +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH TILDE +ENCODING 296 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +32 +4C +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH TILDE +ENCODING 297 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +32 +7E +4C +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH MACRON +ENCODING 298 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +7E +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH MACRON +ENCODING 299 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7E +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH BREVE +ENCODING 300 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +66 +3C +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH BREVE +ENCODING 301 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +3C +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH OGONEK +ENCODING 302 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +18 +18 +18 +18 +18 +18 +18 +18 +7E +0C +08 +06 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH OGONEK +ENCODING 303 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +38 +18 +18 +18 +18 +18 +1C +0C +08 +06 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH DOT ABOVE +ENCODING 304 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER DOTLESS I +ENCODING 305 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LIGATURE IJ +ENCODING 306 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +F7 +66 +66 +66 +66 +66 +66 +66 +66 +EC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LIGATURE IJ +ENCODING 307 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +66 +66 +00 +E6 +66 +66 +66 +66 +66 +76 +06 +06 +1C +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER J WITH CIRCUMFLEX +ENCODING 308 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7E +18 +18 +18 +18 +18 +18 +18 +F0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER J WITH CIRCUMFLEX +ENCODING 309 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +3C +66 +00 +18 +18 +18 +18 +18 +18 +18 +18 +18 +70 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER K WITH CEDILLA +ENCODING 310 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +CC +F8 +CC +C6 +C6 +C6 +C6 +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER K WITH CEDILLA +ENCODING 311 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +C0 +C0 +CC +D8 +F0 +F0 +D8 +CC +C6 +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER KRA +ENCODING 312 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +CC +D8 +F0 +D8 +CC +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER L WITH ACUTE +ENCODING 313 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER L WITH ACUTE +ENCODING 314 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +30 +30 +30 +30 +30 +30 +30 +30 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER L WITH CEDILLA +ENCODING 315 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER L WITH CEDILLA +ENCODING 316 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +30 +30 +30 +30 +30 +30 +30 +30 +30 +1C +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER L WITH CARON +ENCODING 317 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER L WITH CARON +ENCODING 318 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +30 +30 +30 +30 +30 +30 +30 +30 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER L WITH MIDDLE DOT +ENCODING 319 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +C0 +C0 +C0 +CC +CC +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER L WITH MIDDLE DOT +ENCODING 320 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +30 +30 +30 +30 +36 +36 +30 +30 +30 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER L WITH STROKE +ENCODING 321 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +60 +60 +68 +78 +70 +E0 +E0 +60 +60 +3E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER L WITH STROKE +ENCODING 322 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +30 +30 +34 +3C +38 +70 +70 +30 +30 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER N WITH ACUTE +ENCODING 323 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +C6 +E6 +E6 +D6 +D6 +CE +CE +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER N WITH ACUTE +ENCODING 324 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +FC +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER N WITH CEDILLA +ENCODING 325 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +E6 +E6 +D6 +D6 +CE +CE +C6 +C6 +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER N WITH CEDILLA +ENCODING 326 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FC +C6 +C6 +C6 +C6 +C6 +C6 +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER N WITH CARON +ENCODING 327 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +C6 +E6 +E6 +D6 +D6 +CE +CE +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER N WITH CARON +ENCODING 328 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +FC +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER N PRECEDED BY APOSTROPHE +ENCODING 329 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +C0 +00 +FC +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER ENG +ENCODING 330 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +E6 +E6 +D6 +D6 +CE +CE +C6 +C6 +06 +06 +0C +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER ENG +ENCODING 331 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FC +C6 +C6 +C6 +C6 +C6 +C6 +06 +06 +0C +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH MACRON +ENCODING 332 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +7C +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH MACRON +ENCODING 333 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH BREVE +ENCODING 334 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH BREVE +ENCODING 335 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +6C +38 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH DOUBLE ACUTE +ENCODING 336 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +66 +CC +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH DOUBLE ACUTE +ENCODING 337 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +36 +6C +D8 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LIGATURE OE +ENCODING 338 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +D8 +D8 +D8 +DE +D8 +D8 +D8 +D8 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LIGATURE OE +ENCODING 339 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +6E +D6 +D6 +DE +D0 +D0 +6E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER R WITH ACUTE +ENCODING 340 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +FC +C6 +C6 +C6 +FC +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER R WITH ACUTE +ENCODING 341 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +7E +C6 +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER R WITH CEDILLA +ENCODING 342 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +C6 +C6 +C6 +FC +C6 +C6 +C6 +C6 +C6 +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER R WITH CEDILLA +ENCODING 343 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C0 +C0 +C0 +C0 +C0 +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER R WITH CARON +ENCODING 344 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +FC +C6 +C6 +C6 +FC +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER R WITH CARON +ENCODING 345 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +7E +C6 +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER S WITH ACUTE +ENCODING 346 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +7E +C0 +C0 +C0 +7C +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER S WITH ACUTE +ENCODING 347 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +7E +C0 +C0 +7C +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER S WITH CIRCUMFLEX +ENCODING 348 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7E +C0 +C0 +C0 +7C +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER S WITH CIRCUMFLEX +ENCODING 349 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +00 +7E +C0 +C0 +7C +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER S WITH CEDILLA +ENCODING 350 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +7C +06 +06 +06 +06 +FC +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER S WITH CEDILLA +ENCODING 351 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C0 +C0 +7C +06 +06 +FC +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER S WITH CARON +ENCODING 352 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +7E +C0 +C0 +C0 +7C +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER S WITH CARON +ENCODING 353 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +7E +C0 +C0 +7C +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER T WITH CEDILLA +ENCODING 354 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FF +18 +18 +18 +18 +18 +18 +18 +18 +18 +0C +0C +18 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER T WITH CEDILLA +ENCODING 355 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +30 +30 +30 +7C +30 +30 +30 +30 +30 +1E +0C +0C +18 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER T WITH CARON +ENCODING 356 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +FF +18 +18 +18 +18 +18 +18 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER T WITH CARON +ENCODING 357 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +30 +30 +7C +30 +30 +30 +30 +30 +1E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER T WITH STROKE +ENCODING 358 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FF +18 +18 +18 +18 +7E +18 +18 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER T WITH STROKE +ENCODING 359 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +30 +30 +30 +7C +30 +7C +30 +30 +30 +1E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH TILDE +ENCODING 360 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +32 +4C +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH TILDE +ENCODING 361 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +32 +7E +4C +00 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH MACRON +ENCODING 362 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +7C +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH MACRON +ENCODING 363 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH BREVE +ENCODING 364 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH BREVE +ENCODING 365 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +6C +38 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH RING ABOVE +ENCODING 366 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +38 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH RING ABOVE +ENCODING 367 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +6C +38 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH DOUBLE ACUTE +ENCODING 368 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +66 +CC +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH DOUBLE ACUTE +ENCODING 369 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +36 +6C +D8 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH OGONEK +ENCODING 370 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +0C +08 +06 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH OGONEK +ENCODING 371 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +0C +08 +06 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER W WITH CIRCUMFLEX +ENCODING 372 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +C6 +C6 +C6 +C6 +C6 +D6 +FE +EE +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER W WITH CIRCUMFLEX +ENCODING 373 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +00 +C6 +C6 +D6 +D6 +D6 +D6 +6E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER Y WITH CIRCUMFLEX +ENCODING 374 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +C6 +C6 +C6 +C6 +7E +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER Y WITH CIRCUMFLEX +ENCODING 375 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER Y WITH DIAERESIS +ENCODING 376 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +6C +00 +C6 +C6 +C6 +C6 +7E +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER Z WITH ACUTE +ENCODING 377 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +FE +06 +0C +18 +30 +60 +C0 +C0 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER Z WITH ACUTE +ENCODING 378 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +FE +06 +0C +18 +30 +60 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER Z WITH DOT ABOVE +ENCODING 379 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +00 +FE +06 +0C +18 +30 +60 +C0 +C0 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER Z WITH DOT ABOVE +ENCODING 380 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +FE +06 +0C +18 +30 +60 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER Z WITH CARON +ENCODING 381 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +FE +06 +0C +18 +30 +60 +C0 +C0 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER Z WITH CARON +ENCODING 382 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +FE +06 +0C +18 +30 +60 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER LONG S +ENCODING 383 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +1E +30 +30 +70 +30 +30 +30 +30 +30 +30 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER F WITH HOOK +ENCODING 402 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +0E +1B +1B +18 +18 +18 +7E +18 +18 +18 +D8 +D8 +70 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH CARON +ENCODING 461 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH CARON +ENCODING 462 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH CARON +ENCODING 463 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH CARON +ENCODING 464 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH CARON +ENCODING 465 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH CARON +ENCODING 466 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH CARON +ENCODING 467 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH CARON +ENCODING 468 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER AE WITH MACRON +ENCODING 482 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +7C +00 +7E +D8 +D8 +D8 +FE +D8 +D8 +D8 +DE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER AE WITH MACRON +ENCODING 483 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +00 +6E +16 +16 +7E +D0 +D0 +6E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER G WITH CARON +ENCODING 486 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +7E +C0 +C0 +C0 +DE +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER G WITH CARON +ENCODING 487 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +7E +C6 +C6 +C6 +C6 +C6 +7C +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER K WITH CARON +ENCODING 488 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +C6 +C6 +C6 +CC +F8 +CC +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER K WITH CARON +ENCODING 489 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +C0 +C0 +CC +D8 +F0 +F0 +D8 +CC +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH OGONEK +ENCODING 490 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +18 +10 +0C +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH OGONEK +ENCODING 491 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +18 +10 +0C +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH OGONEK AND MACRON +ENCODING 492 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +7C +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +18 +10 +0C +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH OGONEK AND MACRON +ENCODING 493 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +18 +10 +0C +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER J WITH CARON +ENCODING 496 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +18 +18 +18 +18 +18 +18 +18 +18 +18 +70 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER G WITH ACUTE +ENCODING 500 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +7E +C0 +C0 +C0 +DE +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER G WITH ACUTE +ENCODING 501 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +7E +C6 +C6 +C6 +C6 +C6 +7C +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER AE WITH ACUTE +ENCODING 508 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +7E +D8 +D8 +D8 +FE +D8 +D8 +D8 +DE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER AE WITH ACUTE +ENCODING 509 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +6E +16 +16 +7E +D0 +D0 +6E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH STROKE AND ACUTE +ENCODING 510 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +02 +7C +CE +CE +D6 +D6 +E6 +E6 +C6 +7C +80 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH STROKE AND ACUTE +ENCODING 511 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +02 +7C +C6 +CE +D6 +E6 +C6 +7C +80 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH DOUBLE GRAVE +ENCODING 512 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +CC +66 +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH DOUBLE GRAVE +ENCODING 513 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +D8 +6C +36 +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH INVERTED BREVE +ENCODING 514 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH INVERTED BREVE +ENCODING 515 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +6C +6C +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH DOUBLE GRAVE +ENCODING 516 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +CC +66 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH DOUBLE GRAVE +ENCODING 517 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +D8 +6C +36 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH INVERTED BREVE +ENCODING 518 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH INVERTED BREVE +ENCODING 519 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +6C +6C +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH DOUBLE GRAVE +ENCODING 520 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +CC +66 +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH DOUBLE GRAVE +ENCODING 521 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +D8 +6C +36 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH INVERTED BREVE +ENCODING 522 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +3C +66 +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH INVERTED BREVE +ENCODING 523 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +3C +66 +66 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH DOUBLE GRAVE +ENCODING 524 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +CC +66 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH DOUBLE GRAVE +ENCODING 525 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +D8 +6C +36 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH INVERTED BREVE +ENCODING 526 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH INVERTED BREVE +ENCODING 527 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +6C +6C +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER R WITH DOUBLE GRAVE +ENCODING 528 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +CC +66 +00 +FC +C6 +C6 +C6 +FC +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER R WITH DOUBLE GRAVE +ENCODING 529 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +D8 +6C +36 +00 +7E +C6 +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER R WITH INVERTED BREVE +ENCODING 530 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +FC +C6 +C6 +C6 +FC +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER R WITH INVERTED BREVE +ENCODING 531 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +6C +6C +00 +7E +C6 +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH DOUBLE GRAVE +ENCODING 532 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +CC +66 +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH DOUBLE GRAVE +ENCODING 533 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +D8 +6C +36 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH INVERTED BREVE +ENCODING 534 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH INVERTED BREVE +ENCODING 535 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +6C +6C +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER H WITH CARON +ENCODING 542 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +C6 +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER H WITH CARON +ENCODING 543 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +C0 +C0 +C0 +FC +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH DOT ABOVE +ENCODING 550 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH DOT ABOVE +ENCODING 551 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH CEDILLA +ENCODING 552 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +C0 +7E +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH CEDILLA +ENCODING 553 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH DOT ABOVE +ENCODING 558 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH DOT ABOVE +ENCODING 559 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR BREVE +ENCODING 728 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR DOT ABOVE +ENCODING 729 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR OGONEK +ENCODING 731 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +0C +08 +06 +00 +ENDCHAR +STARTCHAR SMALL TILDE +ENCODING 732 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +32 +4C +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR DOUBLE ACUTE ACCENT +ENCODING 733 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +66 +CC +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR COMBINING BREVE +ENCODING 774 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR COMBINING DIAERESIS +ENCODING 776 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +6C +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK CAPITAL LETTER GAMMA +ENCODING 915 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FE +C6 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK CAPITAL LETTER THETA +ENCODING 920 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +38 +6C +C6 +C6 +FE +C6 +C6 +C6 +6C +38 +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK CAPITAL LETTER SIGMA +ENCODING 931 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FE +C6 +60 +30 +18 +18 +30 +60 +C6 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK CAPITAL LETTER PHI +ENCODING 934 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +10 +7C +D6 +D6 +D6 +D6 +D6 +D6 +7C +10 +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK CAPITAL LETTER OMEGA +ENCODING 937 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +38 +6C +C6 +C6 +C6 +C6 +C6 +6C +6C +EE +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK SMALL LETTER ALPHA +ENCODING 945 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +76 +DC +D8 +D8 +D8 +DC +76 +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK SMALL LETTER DELTA +ENCODING 948 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +1E +30 +18 +0C +3C +66 +66 +66 +66 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK SMALL LETTER EPSILON +ENCODING 949 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7C +C6 +C0 +70 +C0 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK SMALL LETTER PI +ENCODING 960 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FE +6C +6C +6C +6C +6C +6C +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK SMALL LETTER SIGMA +ENCODING 963 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +D8 +D8 +D8 +D8 +D8 +70 +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK SMALL LETTER TAU +ENCODING 964 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +18 +18 +18 +18 +18 +0C +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK SMALL LETTER PHI +ENCODING 966 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +4C +D6 +D6 +D6 +D6 +D6 +7C +10 +10 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER IO +ENCODING 1025 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +6C +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I +ENCODING 1030 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +18 +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER YI +ENCODING 1031 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +66 +66 +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER SHORT U +ENCODING 1038 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +00 +C6 +C6 +C6 +C6 +7E +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER A +ENCODING 1040 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER BE +ENCODING 1041 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +C0 +C0 +C0 +FC +C6 +C6 +C6 +C6 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER VE +ENCODING 1042 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +C6 +C6 +C6 +FC +C6 +C6 +C6 +C6 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER GHE +ENCODING 1043 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER DE +ENCODING 1044 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +1C +3C +6C +6C +6C +6C +6C +6C +FE +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER IE +ENCODING 1045 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER ZHE +ENCODING 1046 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +D6 +D6 +D6 +7C +38 +7C +D6 +D6 +D6 +D6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER ZE +ENCODING 1047 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +06 +06 +06 +3C +06 +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER I +ENCODING 1048 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +CE +CE +D6 +D6 +E6 +E6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER SHORT I +ENCODING 1049 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +00 +C6 +CE +CE +D6 +D6 +E6 +E6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER KA +ENCODING 1050 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +CC +F8 +CC +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER EL +ENCODING 1051 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER EM +ENCODING 1052 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +EE +FE +D6 +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER EN +ENCODING 1053 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER O +ENCODING 1054 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER PE +ENCODING 1055 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FE +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER ER +ENCODING 1056 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +C6 +C6 +C6 +FC +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER ES +ENCODING 1057 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER TE +ENCODING 1058 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FF +18 +18 +18 +18 +18 +18 +18 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER U +ENCODING 1059 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +7E +06 +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER EF +ENCODING 1060 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +10 +7C +D6 +D6 +D6 +D6 +D6 +D6 +7C +10 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER HA +ENCODING 1061 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +6C +38 +6C +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER TSE +ENCODING 1062 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7F +03 +03 +06 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER CHE +ENCODING 1063 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +7E +06 +06 +06 +06 +06 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER SHA +ENCODING 1064 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +D6 +D6 +D6 +D6 +D6 +D6 +D6 +D6 +D6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER SHCHA +ENCODING 1065 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +D6 +D6 +D6 +D6 +D6 +D6 +D6 +D6 +D6 +7F +03 +03 +06 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER HARD SIGN +ENCODING 1066 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +F0 +F0 +30 +30 +3C +36 +36 +36 +36 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER YERU +ENCODING 1067 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +F6 +DE +DE +DE +DE +F6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER SOFT SIGN +ENCODING 1068 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +30 +30 +30 +30 +3C +36 +36 +36 +36 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER E +ENCODING 1069 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +06 +06 +06 +3E +06 +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER YU +ENCODING 1070 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +CC +D6 +D6 +D6 +F6 +F6 +D6 +D6 +D6 +CC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER YA +ENCODING 1071 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C6 +C6 +C6 +7E +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER A +ENCODING 1072 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER BE +ENCODING 1073 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7C +C0 +FC +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER VE +ENCODING 1074 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +70 +D8 +D8 +D8 +FC +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER GHE +ENCODING 1075 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FC +06 +06 +7C +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER DE +ENCODING 1076 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C6 +C6 +C6 +C6 +7E +06 +06 +FC +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER IE +ENCODING 1077 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER ZHE +ENCODING 1078 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +D6 +7C +38 +38 +7C +D6 +D6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER ZE +ENCODING 1079 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FC +06 +06 +3C +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER I +ENCODING 1080 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER SHORT I +ENCODING 1081 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +6C +38 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER KA +ENCODING 1082 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +CC +CC +D8 +F0 +F0 +D8 +CC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER EL +ENCODING 1083 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER EM +ENCODING 1084 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +EE +FE +D6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER EN +ENCODING 1085 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +C6 +FE +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER O +ENCODING 1086 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER PE +ENCODING 1087 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FC +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER ER +ENCODING 1088 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FC +C6 +C6 +C6 +C6 +C6 +FC +C0 +C0 +C0 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER ES +ENCODING 1089 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER TE +ENCODING 1090 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +EC +D6 +D6 +D6 +D6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER U +ENCODING 1091 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +06 +06 +FC +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER EF +ENCODING 1092 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7C +D6 +D6 +D6 +D6 +D6 +7C +10 +10 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER HA +ENCODING 1093 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +6C +38 +38 +6C +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER TSE +ENCODING 1094 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7F +03 +03 +06 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER CHE +ENCODING 1095 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +C6 +7E +06 +06 +06 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER SHA +ENCODING 1096 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +D6 +D6 +D6 +D6 +6E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER SHCHA +ENCODING 1097 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +D6 +D6 +D6 +D6 +6F +03 +03 +06 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER HARD SIGN +ENCODING 1098 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +F0 +30 +3C +36 +36 +36 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER YERU +ENCODING 1099 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +F6 +DE +DE +DE +F6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER SOFT SIGN +ENCODING 1100 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +30 +30 +3C +36 +36 +36 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER E +ENCODING 1101 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FC +06 +06 +3E +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER YU +ENCODING 1102 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +CC +D6 +D6 +F6 +D6 +D6 +CC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER YA +ENCODING 1103 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C6 +7E +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER IO +ENCODING 1105 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +6C +6C +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I +ENCODING 1110 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER YI +ENCODING 1111 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +66 +66 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER SHORT U +ENCODING 1118 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +6C +38 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +06 +06 +FC +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER GHE WITH UPTURN +ENCODING 1168 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +06 +06 +7C +C0 +C0 +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER GHE WITH UPTURN +ENCODING 1169 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +06 +06 +7C +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR DOUBLE VERTICAL LINE +ENCODING 8214 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +66 +66 +66 +66 +66 +66 +66 +66 +66 +66 +00 +00 +00 +ENDCHAR +STARTCHAR LEFT SINGLE QUOTATION MARK +ENCODING 8216 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +18 +18 +18 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR RIGHT SINGLE QUOTATION MARK +ENCODING 8217 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +18 +18 +18 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LEFT DOUBLE QUOTATION MARK +ENCODING 8220 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR RIGHT DOUBLE QUOTATION MARK +ENCODING 8221 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BULLET +ENCODING 8226 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +18 +3C +3C +18 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR HORIZONTAL ELLIPSIS +ENCODING 8230 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +DB +DB +00 +00 +00 +00 +ENDCHAR +STARTCHAR SINGLE LEFT-POINTING ANGLE QUOTATION MARK +ENCODING 8249 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +0C +18 +30 +18 +0C +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR SINGLE RIGHT-POINTING ANGLE QUOTATION MARK +ENCODING 8250 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +30 +18 +0C +18 +30 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR DOUBLE EXCLAMATION MARK +ENCODING 8252 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +66 +66 +66 +66 +66 +66 +66 +00 +66 +66 +00 +00 +00 +00 +ENDCHAR +STARTCHAR SUPERSCRIPT LATIN SMALL LETTER N +ENCODING 8319 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +78 +6C +6C +6C +6C +6C +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR PESETA SIGN +ENCODING 8359 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +F8 +CC +CC +F8 +C0 +CC +DE +CC +CC +CC +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR EURO SIGN +ENCODING 8364 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +1C +36 +60 +F8 +60 +F8 +60 +36 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LEFTWARDS ARROW +ENCODING 8592 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +20 +40 +FE +40 +20 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR UPWARDS ARROW +ENCODING 8593 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +3C +5A +18 +18 +18 +18 +18 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR RIGHTWARDS ARROW +ENCODING 8594 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +08 +04 +FE +04 +08 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR DOWNWARDS ARROW +ENCODING 8595 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +18 +18 +18 +18 +18 +5A +3C +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LEFT RIGHT ARROW +ENCODING 8596 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +28 +44 +FE +44 +28 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR UP DOWN ARROW +ENCODING 8597 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +3C +5A +18 +18 +18 +18 +5A +3C +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR UP DOWN ARROW WITH BASE +ENCODING 8616 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +3C +5A +18 +18 +18 +18 +5A +3C +18 +7E +00 +00 +00 +ENDCHAR +STARTCHAR BULLET OPERATOR +ENCODING 8729 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +38 +38 +38 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR SQUARE ROOT +ENCODING 8730 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0F +0C +0C +0C +0C +0C +CC +6C +3C +1C +0C +00 +00 +00 +00 +ENDCHAR +STARTCHAR INFINITY +ENCODING 8734 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +DB +DB +DB +7E +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR INTERSECTION +ENCODING 8745 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR UNION +ENCODING 8746 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR ALMOST EQUAL TO +ENCODING 8776 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +32 +4C +00 +32 +4C +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR IDENTICAL TO +ENCODING 8801 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +7E +00 +00 +7E +00 +00 +7E +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LESS-THAN OR EQUAL TO +ENCODING 8804 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +0C +18 +30 +18 +0C +00 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREATER-THAN OR EQUAL TO +ENCODING 8805 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +30 +18 +0C +18 +30 +00 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR HOUSE +ENCODING 8962 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +10 +38 +6C +C6 +C6 +C6 +FE +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR REVERSED NOT SIGN +ENCODING 8976 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +FE +C0 +C0 +C0 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TURNED NOT SIGN +ENCODING 8985 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +C0 +C0 +C0 +FE +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TOP HALF INTEGRAL +ENCODING 8992 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +0E +1B +1B +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOTTOM HALF INTEGRAL +ENCODING 8993 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +18 +18 +D8 +D8 +70 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT HORIZONTAL +ENCODING 9472 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY HORIZONTAL +ENCODING 9473 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +FF +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT VERTICAL +ENCODING 9474 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY VERTICAL +ENCODING 9475 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +1C +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT TRIPLE DASH HORIZONTAL +ENCODING 9476 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +DB +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY TRIPLE DASH HORIZONTAL +ENCODING 9477 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +DB +DB +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT TRIPLE DASH VERTICAL +ENCODING 9478 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +18 +18 +00 +00 +18 +18 +18 +00 +00 +18 +18 +18 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY TRIPLE DASH VERTICAL +ENCODING 9479 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +1C +1C +1C +00 +00 +1C +1C +1C +00 +00 +1C +1C +1C +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT QUADRUPLE DASH HORIZONTAL +ENCODING 9480 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +AA +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY QUADRUPLE DASH HORIZONTAL +ENCODING 9481 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +AA +AA +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT QUADRUPLE DASH VERTICAL +ENCODING 9482 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +18 +00 +00 +18 +18 +00 +00 +18 +18 +00 +00 +18 +18 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY QUADRUPLE DASH VERTICAL +ENCODING 9483 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +1C +1C +00 +00 +1C +1C +00 +00 +1C +1C +00 +00 +1C +1C +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT DOWN AND RIGHT +ENCODING 9484 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +1F +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN LIGHT AND RIGHT HEAVY +ENCODING 9485 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +1F +1F +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN HEAVY AND RIGHT LIGHT +ENCODING 9486 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +1F +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY DOWN AND RIGHT +ENCODING 9487 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +1F +1F +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT DOWN AND LEFT +ENCODING 9488 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +F8 +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN LIGHT AND LEFT HEAVY +ENCODING 9489 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +F8 +F8 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN HEAVY AND LEFT LIGHT +ENCODING 9490 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FC +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY DOWN AND LEFT +ENCODING 9491 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FC +FC +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT UP AND RIGHT +ENCODING 9492 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +1F +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP LIGHT AND RIGHT HEAVY +ENCODING 9493 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +1F +1F +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP HEAVY AND RIGHT LIGHT +ENCODING 9494 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +1F +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY UP AND RIGHT +ENCODING 9495 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +1F +1F +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT UP AND LEFT +ENCODING 9496 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +F8 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP LIGHT AND LEFT HEAVY +ENCODING 9497 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +F8 +F8 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP HEAVY AND LEFT LIGHT +ENCODING 9498 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FC +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY UP AND LEFT +ENCODING 9499 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FC +FC +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT VERTICAL AND RIGHT +ENCODING 9500 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +1F +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL LIGHT AND RIGHT HEAVY +ENCODING 9501 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +1F +1F +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS UP HEAVY AND RIGHT DOWN LIGHT +ENCODING 9502 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +1F +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN HEAVY AND RIGHT UP LIGHT +ENCODING 9503 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +1F +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL HEAVY AND RIGHT LIGHT +ENCODING 9504 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +1F +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN LIGHT AND RIGHT UP HEAVY +ENCODING 9505 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +1F +1F +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS UP LIGHT AND RIGHT DOWN HEAVY +ENCODING 9506 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +1F +1F +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY VERTICAL AND RIGHT +ENCODING 9507 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +1F +1F +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT VERTICAL AND LEFT +ENCODING 9508 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +F8 +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL LIGHT AND LEFT HEAVY +ENCODING 9509 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +F8 +F8 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS UP HEAVY AND LEFT DOWN LIGHT +ENCODING 9510 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FC +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN HEAVY AND LEFT UP LIGHT +ENCODING 9511 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FC +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL HEAVY AND LEFT LIGHT +ENCODING 9512 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FC +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN LIGHT AND LEFT UP HEAVY +ENCODING 9513 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FC +FC +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS UP LIGHT AND LEFT DOWN HEAVY +ENCODING 9514 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FC +FC +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY VERTICAL AND LEFT +ENCODING 9515 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FC +FC +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT DOWN AND HORIZONTAL +ENCODING 9516 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS LEFT HEAVY AND RIGHT DOWN LIGHT +ENCODING 9517 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +F8 +FF +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS RIGHT HEAVY AND LEFT DOWN LIGHT +ENCODING 9518 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +1F +FF +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN LIGHT AND HORIZONTAL HEAVY +ENCODING 9519 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +FF +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN HEAVY AND HORIZONTAL LIGHT +ENCODING 9520 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS RIGHT LIGHT AND LEFT DOWN HEAVY +ENCODING 9521 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +FC +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LEFT LIGHT AND RIGHT DOWN HEAVY +ENCODING 9522 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +1F +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY DOWN AND HORIZONTAL +ENCODING 9523 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +FF +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT UP AND HORIZONTAL +ENCODING 9524 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LEFT HEAVY AND RIGHT UP LIGHT +ENCODING 9525 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +F8 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS RIGHT HEAVY AND LEFT UP LIGHT +ENCODING 9526 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +1F +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP LIGHT AND HORIZONTAL HEAVY +ENCODING 9527 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +FF +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP HEAVY AND HORIZONTAL LIGHT +ENCODING 9528 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS RIGHT LIGHT AND LEFT UP HEAVY +ENCODING 9529 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +FC +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LEFT LIGHT AND RIGHT UP HEAVY +ENCODING 9530 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +1F +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY UP AND HORIZONTAL +ENCODING 9531 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +FF +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL +ENCODING 9532 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS LEFT HEAVY AND RIGHT VERTICAL LIGHT +ENCODING 9533 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +F8 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS RIGHT HEAVY AND LEFT VERTICAL LIGHT +ENCODING 9534 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +1F +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL LIGHT AND HORIZONTAL HEAVY +ENCODING 9535 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +FF +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS UP HEAVY AND DOWN HORIZONTAL LIGHT +ENCODING 9536 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN HEAVY AND UP HORIZONTAL LIGHT +ENCODING 9537 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL HEAVY AND HORIZONTAL LIGHT +ENCODING 9538 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LEFT UP HEAVY AND RIGHT DOWN LIGHT +ENCODING 9539 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +F8 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS RIGHT UP HEAVY AND LEFT DOWN LIGHT +ENCODING 9540 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +1F +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS LEFT DOWN HEAVY AND RIGHT UP LIGHT +ENCODING 9541 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +FC +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS RIGHT DOWN HEAVY AND LEFT UP LIGHT +ENCODING 9542 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +1F +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN LIGHT AND UP HORIZONTAL HEAVY +ENCODING 9543 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +FF +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS UP LIGHT AND DOWN HORIZONTAL HEAVY +ENCODING 9544 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +FF +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS RIGHT LIGHT AND LEFT VERTICAL HEAVY +ENCODING 9545 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +FC +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LEFT LIGHT AND RIGHT VERTICAL HEAVY +ENCODING 9546 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +1F +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY VERTICAL AND HORIZONTAL +ENCODING 9547 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +FF +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT DOUBLE DASH HORIZONTAL +ENCODING 9548 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +EE +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY DOUBLE DASH HORIZONTAL +ENCODING 9549 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +EE +EE +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT DOUBLE DASH VERTICAL +ENCODING 9550 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +18 +18 +18 +18 +18 +00 +00 +18 +18 +18 +18 +18 +18 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY DOUBLE DASH VERTICAL +ENCODING 9551 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +1C +1C +1C +1C +1C +1C +00 +00 +1C +1C +1C +1C +1C +1C +00 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE HORIZONTAL +ENCODING 9552 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +FF +00 +FF +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE VERTICAL +ENCODING 9553 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +36 +36 +36 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN SINGLE AND RIGHT DOUBLE +ENCODING 9554 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +1F +18 +1F +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN DOUBLE AND RIGHT SINGLE +ENCODING 9555 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +3F +36 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE DOWN AND RIGHT +ENCODING 9556 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +3F +30 +37 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN SINGLE AND LEFT DOUBLE +ENCODING 9557 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +F8 +18 +F8 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN DOUBLE AND LEFT SINGLE +ENCODING 9558 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FE +36 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE DOWN AND LEFT +ENCODING 9559 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +FE +06 +F6 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS UP SINGLE AND RIGHT DOUBLE +ENCODING 9560 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +1F +18 +1F +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP DOUBLE AND RIGHT SINGLE +ENCODING 9561 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +36 +3F +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE UP AND RIGHT +ENCODING 9562 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +37 +30 +3F +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP SINGLE AND LEFT DOUBLE +ENCODING 9563 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +F8 +18 +F8 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP DOUBLE AND LEFT SINGLE +ENCODING 9564 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +36 +FE +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE UP AND LEFT +ENCODING 9565 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +F6 +06 +FE +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL SINGLE AND RIGHT DOUBLE +ENCODING 9566 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +1F +18 +1F +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL DOUBLE AND RIGHT SINGLE +ENCODING 9567 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +36 +37 +36 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE VERTICAL AND RIGHT +ENCODING 9568 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +37 +30 +37 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL SINGLE AND LEFT DOUBLE +ENCODING 9569 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +F8 +18 +F8 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL DOUBLE AND LEFT SINGLE +ENCODING 9570 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +36 +F6 +36 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE VERTICAL AND LEFT +ENCODING 9571 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +F6 +06 +F6 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN SINGLE AND HORIZONTAL DOUBLE +ENCODING 9572 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +FF +00 +FF +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN DOUBLE AND HORIZONTAL SINGLE +ENCODING 9573 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +36 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE DOWN AND HORIZONTAL +ENCODING 9574 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +FF +00 +F7 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS UP SINGLE AND HORIZONTAL DOUBLE +ENCODING 9575 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +FF +00 +FF +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP DOUBLE AND HORIZONTAL SINGLE +ENCODING 9576 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +36 +FF +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE UP AND HORIZONTAL +ENCODING 9577 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +F7 +00 +FF +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL SINGLE AND HORIZONTAL DOUBLE +ENCODING 9578 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +FF +18 +FF +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL DOUBLE AND HORIZONTAL SINGLE +ENCODING 9579 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +36 +FF +36 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE VERTICAL AND HORIZONTAL +ENCODING 9580 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +F7 +00 +F7 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT ARC DOWN AND RIGHT +ENCODING 9581 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +0F +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT ARC DOWN AND LEFT +ENCODING 9582 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +F0 +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT ARC UP AND LEFT +ENCODING 9583 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +F0 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT ARC UP AND RIGHT +ENCODING 9584 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +0F +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT +ENCODING 9585 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +01 +03 +03 +06 +06 +0C +0C +18 +18 +30 +30 +60 +60 +C0 +C0 +80 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT DIAGONAL UPPER LEFT TO LOWER RIGHT +ENCODING 9586 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +80 +C0 +C0 +60 +60 +30 +30 +18 +18 +0C +0C +06 +06 +03 +03 +01 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT DIAGONAL CROSS +ENCODING 9587 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +81 +C3 +C3 +66 +66 +3C +3C +18 +18 +3C +3C +66 +66 +C3 +C3 +81 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT LEFT +ENCODING 9588 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +F0 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT UP +ENCODING 9589 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +18 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT RIGHT +ENCODING 9590 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +0F +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT DOWN +ENCODING 9591 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY LEFT +ENCODING 9592 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +F0 +F0 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY UP +ENCODING 9593 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +1C +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY RIGHT +ENCODING 9594 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +0F +0F +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY DOWN +ENCODING 9595 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT LEFT AND HEAVY RIGHT +ENCODING 9596 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +0F +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT UP AND HEAVY DOWN +ENCODING 9597 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +18 +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY LEFT AND LIGHT RIGHT +ENCODING 9598 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +F0 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY UP AND LIGHT DOWN +ENCODING 9599 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +1C +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR UPPER HALF BLOCK +ENCODING 9600 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FF +FF +FF +FF +FF +FF +FF +FF +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LOWER ONE EIGHTH BLOCK +ENCODING 9601 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +FF +FF +ENDCHAR +STARTCHAR LOWER ONE QUARTER BLOCK +ENCODING 9602 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +FF +FF +FF +FF +ENDCHAR +STARTCHAR LOWER THREE EIGHTHS BLOCK +ENCODING 9603 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +FF +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR LOWER HALF BLOCK +ENCODING 9604 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +FF +FF +FF +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR LOWER FIVE EIGHTHS BLOCK +ENCODING 9605 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR LOWER THREE QUARTERS BLOCK +ENCODING 9606 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR LOWER SEVEN EIGHTHS BLOCK +ENCODING 9607 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR FULL BLOCK +ENCODING 9608 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR LEFT SEVEN EIGHTHS BLOCK +ENCODING 9609 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FE +FE +FE +FE +FE +FE +FE +FE +FE +FE +FE +FE +FE +FE +FE +FE +ENDCHAR +STARTCHAR LEFT THREE QUARTERS BLOCK +ENCODING 9610 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FC +FC +FC +FC +FC +FC +FC +FC +FC +FC +FC +FC +FC +FC +FC +FC +ENDCHAR +STARTCHAR LEFT FIVE EIGHTHS BLOCK +ENCODING 9611 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +F8 +F8 +F8 +F8 +F8 +F8 +F8 +F8 +F8 +F8 +F8 +F8 +F8 +F8 +F8 +F8 +ENDCHAR +STARTCHAR LEFT HALF BLOCK +ENCODING 9612 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +ENDCHAR +STARTCHAR LEFT THREE EIGHTHS BLOCK +ENCODING 9613 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +E0 +E0 +E0 +E0 +E0 +E0 +E0 +E0 +E0 +E0 +E0 +E0 +E0 +E0 +E0 +E0 +ENDCHAR +STARTCHAR LEFT ONE QUARTER BLOCK +ENCODING 9614 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +ENDCHAR +STARTCHAR LEFT ONE EIGHTH BLOCK +ENCODING 9615 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +80 +80 +80 +80 +80 +80 +80 +80 +80 +80 +80 +80 +80 +80 +80 +80 +ENDCHAR +STARTCHAR RIGHT HALF BLOCK +ENCODING 9616 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +0F +0F +0F +0F +0F +0F +0F +0F +0F +0F +0F +0F +0F +0F +0F +0F +ENDCHAR +STARTCHAR LIGHT SHADE +ENCODING 9617 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +11 +44 +11 +44 +11 +44 +11 +44 +11 +44 +11 +44 +11 +44 +11 +44 +ENDCHAR +STARTCHAR MEDIUM SHADE +ENCODING 9618 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +55 +AA +55 +AA +55 +AA +55 +AA +55 +AA +55 +AA +55 +AA +55 +AA +ENDCHAR +STARTCHAR DARK SHADE +ENCODING 9619 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +DD +77 +DD +77 +DD +77 +DD +77 +DD +77 +DD +77 +DD +77 +DD +77 +ENDCHAR +STARTCHAR UPPER ONE EIGHTH BLOCK +ENCODING 9620 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FF +FF +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR RIGHT ONE EIGHTH BLOCK +ENCODING 9621 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +01 +01 +01 +01 +01 +01 +01 +01 +01 +01 +01 +01 +01 +01 +01 +01 +ENDCHAR +STARTCHAR QUADRANT LOWER LEFT +ENCODING 9622 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +ENDCHAR +STARTCHAR QUADRANT LOWER RIGHT +ENCODING 9623 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +0F +0F +0F +0F +0F +0F +0F +0F +ENDCHAR +STARTCHAR QUADRANT UPPER LEFT +ENCODING 9624 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR QUADRANT UPPER LEFT AND LOWER LEFT AND LOWER RIGHT +ENCODING 9625 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +FF +FF +FF +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR QUADRANT UPPER LEFT AND LOWER RIGHT +ENCODING 9626 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +0F +0F +0F +0F +0F +0F +0F +0F +ENDCHAR +STARTCHAR QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER LEFT +ENCODING 9627 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FF +FF +FF +FF +FF +FF +FF +FF +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +ENDCHAR +STARTCHAR QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER RIGHT +ENCODING 9628 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FF +FF +FF +FF +FF +FF +FF +FF +0F +0F +0F +0F +0F +0F +0F +0F +ENDCHAR +STARTCHAR QUADRANT UPPER RIGHT +ENCODING 9629 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +0F +0F +0F +0F +0F +0F +0F +0F +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR QUADRANT UPPER RIGHT AND LOWER LEFT +ENCODING 9630 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +0F +0F +0F +0F +0F +0F +0F +0F +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +ENDCHAR +STARTCHAR QUADRANT UPPER RIGHT AND LOWER LEFT AND LOWER RIGHT +ENCODING 9631 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +0F +0F +0F +0F +0F +0F +0F +0F +FF +FF +FF +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR BLACK SQUARE +ENCODING 9632 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +7C +7C +7C +7C +7C +7C +7C +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK RECTANGLE +ENCODING 9644 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +FE +FE +FE +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK UP-POINTING TRIANGLE +ENCODING 9650 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +10 +38 +38 +7C +7C +FE +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK DOWN-POINTING TRIANGLE +ENCODING 9660 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FE +FE +7C +7C +38 +38 +10 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK DIAMOND +ENCODING 9670 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +10 +38 +7C +FE +7C +38 +10 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LOZENGE +ENCODING 9674 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +10 +38 +6C +C6 +6C +38 +10 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR WHITE CIRCLE +ENCODING 9675 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +3C +66 +66 +66 +66 +3C +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK CIRCLE +ENCODING 9679 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +3C +7E +7E +7E +7E +3C +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR INVERSE BULLET +ENCODING 9688 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FF +FF +FF +FF +FF +FF +E7 +C3 +C3 +E7 +FF +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR INVERSE WHITE CIRCLE +ENCODING 9689 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FF +FF +FF +FF +FF +C3 +99 +99 +99 +99 +C3 +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR BLACK LOWER RIGHT TRIANGLE +ENCODING 9698 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +80 +C0 +E0 +F0 +F8 +FC +FE +FF +ENDCHAR +STARTCHAR BLACK LOWER LEFT TRIANGLE +ENCODING 9699 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +01 +03 +07 +0F +1F +3F +7F +FF +ENDCHAR +STARTCHAR BLACK UPPER LEFT TRIANGLE +ENCODING 9700 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FF +FE +FC +F8 +F0 +E0 +C0 +80 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK UPPER RIGHT TRIANGLE +ENCODING 9701 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FF +7F +3F +1F +0F +07 +03 +01 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TRIGRAM FOR HEAVEN +ENCODING 9776 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +7E +00 +00 +7E +00 +00 +7E +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TRIGRAM FOR LAKE +ENCODING 9777 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +66 +00 +00 +7E +00 +00 +7E +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TRIGRAM FOR FIRE +ENCODING 9778 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +7E +00 +00 +66 +00 +00 +7E +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TRIGRAM FOR THUNDER +ENCODING 9779 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +66 +00 +00 +66 +00 +00 +7E +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TRIGRAM FOR WIND +ENCODING 9780 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +7E +00 +00 +7E +00 +00 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TRIGRAM FOR WATER +ENCODING 9781 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +66 +00 +00 +7E +00 +00 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TRIGRAM FOR MOUNTAIN +ENCODING 9782 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +7E +00 +00 +66 +00 +00 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TRIGRAM FOR EARTH +ENCODING 9783 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +66 +00 +00 +66 +00 +00 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR WHITE SMILING FACE +ENCODING 9786 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +81 +A5 +81 +81 +A5 +99 +81 +81 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK SMILING FACE +ENCODING 9787 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +FF +DB +FF +FF +DB +E7 +FF +FF +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR WHITE SUN WITH RAYS +ENCODING 9788 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +18 +18 +DB +3C +E7 +3C +DB +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR FEMALE SIGN +ENCODING 9792 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +3C +66 +66 +66 +66 +3C +18 +3C +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR MALE SIGN +ENCODING 9794 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +0F +07 +0D +19 +3C +66 +66 +66 +66 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK SPADE SUIT +ENCODING 9824 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +18 +3C +7E +FF +FF +7E +18 +18 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK CLUB SUIT +ENCODING 9827 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +18 +3C +3C +E7 +E7 +E7 +18 +18 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK HEART SUIT +ENCODING 9829 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +6C +FE +FE +FE +7C +38 +10 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK DIAMOND SUIT +ENCODING 9830 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +10 +38 +7C +FE +7C +38 +10 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR EIGHTH NOTE +ENCODING 9834 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +66 +7E +60 +60 +60 +60 +60 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BEAMED EIGHTH NOTES +ENCODING 9835 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +66 +7E +66 +66 +66 +66 +66 +6E +CC +C0 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN BLANK +ENCODING 10240 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1 +ENCODING 10241 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2 +ENCODING 10242 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12 +ENCODING 10243 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3 +ENCODING 10244 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13 +ENCODING 10245 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23 +ENCODING 10246 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123 +ENCODING 10247 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-4 +ENCODING 10248 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-14 +ENCODING 10249 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-24 +ENCODING 10250 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-124 +ENCODING 10251 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-34 +ENCODING 10252 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-134 +ENCODING 10253 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-234 +ENCODING 10254 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1234 +ENCODING 10255 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-5 +ENCODING 10256 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-15 +ENCODING 10257 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-25 +ENCODING 10258 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-125 +ENCODING 10259 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-35 +ENCODING 10260 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-135 +ENCODING 10261 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-235 +ENCODING 10262 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1235 +ENCODING 10263 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-45 +ENCODING 10264 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-145 +ENCODING 10265 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-245 +ENCODING 10266 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1245 +ENCODING 10267 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-345 +ENCODING 10268 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1345 +ENCODING 10269 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2345 +ENCODING 10270 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12345 +ENCODING 10271 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-6 +ENCODING 10272 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-16 +ENCODING 10273 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-26 +ENCODING 10274 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-126 +ENCODING 10275 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-36 +ENCODING 10276 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-136 +ENCODING 10277 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-236 +ENCODING 10278 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1236 +ENCODING 10279 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-46 +ENCODING 10280 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-146 +ENCODING 10281 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-246 +ENCODING 10282 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1246 +ENCODING 10283 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-346 +ENCODING 10284 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1346 +ENCODING 10285 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2346 +ENCODING 10286 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12346 +ENCODING 10287 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-56 +ENCODING 10288 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-156 +ENCODING 10289 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-256 +ENCODING 10290 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1256 +ENCODING 10291 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-356 +ENCODING 10292 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1356 +ENCODING 10293 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2356 +ENCODING 10294 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12356 +ENCODING 10295 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-456 +ENCODING 10296 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1456 +ENCODING 10297 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2456 +ENCODING 10298 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12456 +ENCODING 10299 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3456 +ENCODING 10300 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13456 +ENCODING 10301 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23456 +ENCODING 10302 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123456 +ENCODING 10303 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-7 +ENCODING 10304 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-17 +ENCODING 10305 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-27 +ENCODING 10306 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-127 +ENCODING 10307 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-37 +ENCODING 10308 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-137 +ENCODING 10309 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-237 +ENCODING 10310 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1237 +ENCODING 10311 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-47 +ENCODING 10312 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-147 +ENCODING 10313 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-247 +ENCODING 10314 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1247 +ENCODING 10315 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-347 +ENCODING 10316 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1347 +ENCODING 10317 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2347 +ENCODING 10318 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12347 +ENCODING 10319 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-57 +ENCODING 10320 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-157 +ENCODING 10321 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-257 +ENCODING 10322 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1257 +ENCODING 10323 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-357 +ENCODING 10324 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1357 +ENCODING 10325 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2357 +ENCODING 10326 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12357 +ENCODING 10327 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-457 +ENCODING 10328 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1457 +ENCODING 10329 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2457 +ENCODING 10330 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12457 +ENCODING 10331 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3457 +ENCODING 10332 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13457 +ENCODING 10333 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23457 +ENCODING 10334 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123457 +ENCODING 10335 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-67 +ENCODING 10336 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-167 +ENCODING 10337 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-267 +ENCODING 10338 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1267 +ENCODING 10339 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-367 +ENCODING 10340 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1367 +ENCODING 10341 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2367 +ENCODING 10342 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12367 +ENCODING 10343 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-467 +ENCODING 10344 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1467 +ENCODING 10345 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2467 +ENCODING 10346 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12467 +ENCODING 10347 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3467 +ENCODING 10348 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13467 +ENCODING 10349 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23467 +ENCODING 10350 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123467 +ENCODING 10351 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-567 +ENCODING 10352 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1567 +ENCODING 10353 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2567 +ENCODING 10354 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12567 +ENCODING 10355 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3567 +ENCODING 10356 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13567 +ENCODING 10357 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23567 +ENCODING 10358 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123567 +ENCODING 10359 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-4567 +ENCODING 10360 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-14567 +ENCODING 10361 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-24567 +ENCODING 10362 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-124567 +ENCODING 10363 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-34567 +ENCODING 10364 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-134567 +ENCODING 10365 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-234567 +ENCODING 10366 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1234567 +ENCODING 10367 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-8 +ENCODING 10368 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-18 +ENCODING 10369 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-28 +ENCODING 10370 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-128 +ENCODING 10371 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-38 +ENCODING 10372 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-138 +ENCODING 10373 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-238 +ENCODING 10374 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1238 +ENCODING 10375 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-48 +ENCODING 10376 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-148 +ENCODING 10377 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-248 +ENCODING 10378 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1248 +ENCODING 10379 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-348 +ENCODING 10380 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1348 +ENCODING 10381 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2348 +ENCODING 10382 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12348 +ENCODING 10383 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-58 +ENCODING 10384 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-158 +ENCODING 10385 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-258 +ENCODING 10386 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1258 +ENCODING 10387 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-358 +ENCODING 10388 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1358 +ENCODING 10389 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2358 +ENCODING 10390 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12358 +ENCODING 10391 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-458 +ENCODING 10392 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1458 +ENCODING 10393 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2458 +ENCODING 10394 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12458 +ENCODING 10395 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3458 +ENCODING 10396 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13458 +ENCODING 10397 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23458 +ENCODING 10398 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123458 +ENCODING 10399 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-68 +ENCODING 10400 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-168 +ENCODING 10401 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-268 +ENCODING 10402 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1268 +ENCODING 10403 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-368 +ENCODING 10404 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1368 +ENCODING 10405 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2368 +ENCODING 10406 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12368 +ENCODING 10407 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-468 +ENCODING 10408 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1468 +ENCODING 10409 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2468 +ENCODING 10410 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12468 +ENCODING 10411 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3468 +ENCODING 10412 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13468 +ENCODING 10413 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23468 +ENCODING 10414 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123468 +ENCODING 10415 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-568 +ENCODING 10416 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1568 +ENCODING 10417 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2568 +ENCODING 10418 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12568 +ENCODING 10419 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3568 +ENCODING 10420 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13568 +ENCODING 10421 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23568 +ENCODING 10422 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123568 +ENCODING 10423 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-4568 +ENCODING 10424 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-14568 +ENCODING 10425 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-24568 +ENCODING 10426 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-124568 +ENCODING 10427 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-34568 +ENCODING 10428 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-134568 +ENCODING 10429 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-234568 +ENCODING 10430 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1234568 +ENCODING 10431 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-78 +ENCODING 10432 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-178 +ENCODING 10433 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-278 +ENCODING 10434 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1278 +ENCODING 10435 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-378 +ENCODING 10436 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1378 +ENCODING 10437 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2378 +ENCODING 10438 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12378 +ENCODING 10439 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-478 +ENCODING 10440 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1478 +ENCODING 10441 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2478 +ENCODING 10442 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12478 +ENCODING 10443 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3478 +ENCODING 10444 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13478 +ENCODING 10445 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23478 +ENCODING 10446 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123478 +ENCODING 10447 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-578 +ENCODING 10448 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1578 +ENCODING 10449 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2578 +ENCODING 10450 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12578 +ENCODING 10451 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3578 +ENCODING 10452 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13578 +ENCODING 10453 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23578 +ENCODING 10454 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123578 +ENCODING 10455 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-4578 +ENCODING 10456 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-14578 +ENCODING 10457 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-24578 +ENCODING 10458 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-124578 +ENCODING 10459 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-34578 +ENCODING 10460 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-134578 +ENCODING 10461 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-234578 +ENCODING 10462 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1234578 +ENCODING 10463 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-678 +ENCODING 10464 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1678 +ENCODING 10465 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2678 +ENCODING 10466 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12678 +ENCODING 10467 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3678 +ENCODING 10468 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13678 +ENCODING 10469 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23678 +ENCODING 10470 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123678 +ENCODING 10471 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-4678 +ENCODING 10472 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-14678 +ENCODING 10473 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-24678 +ENCODING 10474 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-124678 +ENCODING 10475 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-34678 +ENCODING 10476 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-134678 +ENCODING 10477 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-234678 +ENCODING 10478 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1234678 +ENCODING 10479 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-5678 +ENCODING 10480 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-15678 +ENCODING 10481 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-25678 +ENCODING 10482 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-125678 +ENCODING 10483 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-35678 +ENCODING 10484 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-135678 +ENCODING 10485 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-235678 +ENCODING 10486 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1235678 +ENCODING 10487 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-45678 +ENCODING 10488 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-145678 +ENCODING 10489 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-245678 +ENCODING 10490 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1245678 +ENCODING 10491 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-345678 +ENCODING 10492 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1345678 +ENCODING 10493 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2345678 +ENCODING 10494 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12345678 +ENCODING 10495 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR UPWARDS BLACK ARROW +ENCODING 11014 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +3C +7E +FF +3C +3C +3C +3C +3C +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR DOWNWARDS BLACK ARROW +ENCODING 11015 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +3C +3C +3C +3C +3C +3C +FF +7E +3C +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LEFTWARDS TRIANGLE-HEADED ARROW +ENCODING 11104 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +20 +60 +FE +60 +20 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR UPWARDS TRIANGLE-HEADED ARROW +ENCODING 11105 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +3C +7E +18 +18 +18 +18 +18 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR RIGHTWARDS TRIANGLE-HEADED ARROW +ENCODING 11106 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +08 +0C +FE +0C +08 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR DOWNWARDS TRIANGLE-HEADED ARROW +ENCODING 11107 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +18 +18 +18 +18 +18 +7E +3C +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LEFT RIGHT TRIANGLE-HEADED ARROW +ENCODING 11108 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +28 +6C +FE +6C +28 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR UP DOWN TRIANGLE-HEADED ARROW +ENCODING 11109 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +3C +7E +18 +18 +18 +18 +7E +3C +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR char57504 +ENCODING 57504 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +CC +DE +CC +CC +CC +98 +30 +60 +C0 +C0 +C0 +00 +00 +ENDCHAR +STARTCHAR char57505 +ENCODING 57505 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +40 +40 +40 +40 +40 +38 +00 +12 +1A +1A +16 +16 +12 +00 +00 +ENDCHAR +STARTCHAR char57506 +ENCODING 57506 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +24 +24 +24 +7E +7E +66 +66 +7E +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR char57520 +ENCODING 57520 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +80 +C0 +E0 +F0 +F8 +FC +FE +FF +FF +FE +FC +F8 +F0 +E0 +C0 +80 +ENDCHAR +STARTCHAR char57521 +ENCODING 57521 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +80 +C0 +60 +30 +18 +0C +06 +03 +03 +06 +0C +18 +30 +60 +C0 +80 +ENDCHAR +STARTCHAR char57522 +ENCODING 57522 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +01 +03 +07 +0F +1F +3F +7F +FF +FF +7F +3F +1F +0F +07 +03 +01 +ENDCHAR +STARTCHAR char57523 +ENCODING 57523 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +01 +03 +06 +0C +18 +30 +60 +C0 +C0 +60 +30 +18 +0C +06 +03 +01 +ENDCHAR +ENDFONT diff --git a/src/font/res/spleen-8x16.otb b/src/font/res/spleen-8x16.otb new file mode 100644 index 000000000..229b7ab02 Binary files /dev/null and b/src/font/res/spleen-8x16.otb differ diff --git a/src/font/res/spleen-8x16.pcf b/src/font/res/spleen-8x16.pcf new file mode 100644 index 000000000..1c4fbd3f7 Binary files /dev/null and b/src/font/res/spleen-8x16.pcf differ diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 97cb5cd89..ab3c6aaab 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -6,6 +6,7 @@ const macos = @import("macos"); const font = @import("../main.zig"); const os = @import("../../os/main.zig"); const terminal = @import("../../terminal/main.zig"); +const unicode = @import("../../unicode/main.zig"); const Feature = font.shape.Feature; const FeatureList = font.shape.FeatureList; const default_features = font.shape.default_features; @@ -52,7 +53,7 @@ pub const Shaper = struct { /// Cached attributes dict for creating CTTypesetter objects. /// The values in this never change so we can avoid overhead - /// by just creating it once and saving it for re-use. + /// by just creating it once and saving it for reuse. typesetter_attr_dict: *macos.foundation.Dictionary, /// List where we cache fonts, so we don't have to remake them for @@ -97,12 +98,17 @@ pub const Shaper = struct { self.unichars.deinit(alloc); } - fn reset(self: *RunState) !void { + fn reset(self: *RunState) void { self.codepoints.clearRetainingCapacity(); self.unichars.clearRetainingCapacity(); } }; + const Offset = struct { + cluster: u32 = 0, + x: f64 = 0, + }; + /// Create a CoreFoundation Dictionary suitable for /// settings the font features of a CoreText font. fn makeFeaturesDict(feats: []const Feature) !*macos.foundation.Dictionary { @@ -377,12 +383,16 @@ pub const Shaper = struct { const line = typesetter.createLine(.{ .location = 0, .length = 0 }); self.cf_release_pool.appendAssumeCapacity(line); - // This keeps track of the current offsets within a single cell. - var cell_offset: struct { - cluster: u32 = 0, - x: f64 = 0, - y: f64 = 0, - } = .{}; + // This keeps track of the current x offset (sum of advance.width) and + // the furthest cluster we've seen so far (max). + var run_offset: Offset = .{}; + + // This keeps track of the cell starting x and cluster. + var cell_offset: Offset = .{}; + + // For debugging positions, turn this on: + //var run_offset_y: f64 = 0.0; + //var cell_offset_y: f64 = 0.0; // Clear our cell buf and make sure we have enough room for the whole // line of glyphs, so that we can just assume capacity when appending @@ -402,8 +412,8 @@ pub const Shaper = struct { // other so we can iterate over them and just append to our // cell buffer. const runs = line.getGlyphRuns(); - for (0..runs.getCount()) |i| { - const ctrun = runs.getValueAtIndex(macos.text.Run, i); + for (0..runs.getCount()) |run_i| { + const ctrun = runs.getValueAtIndex(macos.text.Run, run_i); const status = ctrun.getStatus(); if (status.non_monotonic or status.right_to_left) non_ltr = true; @@ -411,40 +421,102 @@ pub const Shaper = struct { // Get our glyphs and positions const glyphs = ctrun.getGlyphsPtr() orelse try ctrun.getGlyphs(alloc); const advances = ctrun.getAdvancesPtr() orelse try ctrun.getAdvances(alloc); + const positions = ctrun.getPositionsPtr() orelse try ctrun.getPositions(alloc); const indices = ctrun.getStringIndicesPtr() orelse try ctrun.getStringIndices(alloc); assert(glyphs.len == advances.len); + assert(glyphs.len == positions.len); assert(glyphs.len == indices.len); for ( glyphs, advances, + positions, indices, - ) |glyph, advance, index| { + ) |glyph, advance, position, index| { // Our cluster is also our cell X position. If the cluster changes // then we need to reset our current cell offsets. const cluster = state.codepoints.items[index].cluster; - if (cell_offset.cluster != cluster) pad: { - // We previously asserted this but for rtl text this is - // not true. So we check for this and break out. In the - // future we probably need to reverse pad for rtl but - // I don't have a solid test case for this yet so let's - // wait for that. - if (cell_offset.cluster > cluster) break :pad; + if (cell_offset.cluster != cluster) { + // We previously asserted that the new cluster is greater + // than cell_offset.cluster, but this isn't always true. + // See e.g. the "shape Chakma vowel sign with ligature + // (vowel sign renders first)" test. - cell_offset = .{ .cluster = cluster }; + const is_after_glyph_from_current_or_next_clusters = + cluster <= run_offset.cluster; + + const is_first_codepoint_in_cluster = blk: { + var i = index; + while (i > 0) { + i -= 1; + const codepoint = state.codepoints.items[i]; + + // Skip surrogate pair padding + if (codepoint.codepoint == 0) continue; + break :blk codepoint.cluster != cluster; + } else break :blk true; + }; + + // We need to reset the `cell_offset` at the start of a new + // cluster, but we do that conditionally if the codepoint + // `is_first_codepoint_in_cluster` and the cluster is not + // `is_after_glyph_from_current_or_next_clusters`, which is + // a heuristic to detect ligatures and avoid positioning + // glyphs that mark ligatures incorrectly. The idea is that + // if the first codepoint in a cluster doesn't appear in + // the stream, it's very likely that it combined with + // codepoints from a previous cluster into a ligature. + // Then, the subsequent codepoints are very likely marking + // glyphs that are placed relative to that ligature, so if + // we were to reset the `cell_offset` to align it with the + // grid, the positions would be off. The + // `!is_after_glyph_from_current_or_next_clusters` check is + // needed in case these marking glyphs come from a later + // cluster but are rendered first (see the Chakma and + // Bengali tests). In that case when we get to the + // codepoint that `is_first_codepoint_in_cluster`, but in a + // cluster that + // `is_after_glyph_from_current_or_next_clusters`, we don't + // want to reset to the grid and cause the positions to be + // off. (Note that we could go back and align the cells to + // the grid starting from the one from the cluster that + // rendered out of order, but that is more complicated so + // we don't do that for now. Also, it's TBD if there are + // exceptions to this heuristic for detecting ligatures, + // but using the logging below seems to show it works + // well.) + if (is_first_codepoint_in_cluster and + !is_after_glyph_from_current_or_next_clusters) + { + cell_offset = .{ + .cluster = cluster, + .x = run_offset.x, + }; + + // For debugging positions, turn this on: + //cell_offset_y = run_offset_y; + } } + // For debugging positions, turn this on: + //try self.debugPositions(alloc, run_offset, run_offset_y, cell_offset, cell_offset_y, position, index); + + const x_offset = position.x - cell_offset.x; + self.cell_buf.appendAssumeCapacity(.{ - .x = @intCast(cluster), - .x_offset = @intFromFloat(@round(cell_offset.x)), - .y_offset = @intFromFloat(@round(cell_offset.y)), + .x = @intCast(cell_offset.cluster), + .x_offset = @intFromFloat(@round(x_offset)), + .y_offset = @intFromFloat(@round(position.y)), .glyph_index = glyph, }); - // Add our advances to keep track of our current cell offsets. + // Add our advances to keep track of our run offsets. // Advances apply to the NEXT cell. - cell_offset.x += advance.width; - cell_offset.y += advance.height; + run_offset.x += advance.width; + run_offset.cluster = @max(run_offset.cluster, cluster); + + // For debugging positions, turn this on: + //run_offset_y += advance.height; } } @@ -572,8 +644,8 @@ pub const Shaper = struct { pub const RunIteratorHook = struct { shaper: *Shaper, - pub fn prepare(self: *RunIteratorHook) !void { - try self.shaper.run_state.reset(); + pub fn prepare(self: *RunIteratorHook) void { + self.shaper.run_state.reset(); // log.warn("----------- run reset -------------", .{}); } @@ -609,10 +681,157 @@ pub const Shaper = struct { }); } - pub fn finalize(self: RunIteratorHook) !void { + pub fn finalize(self: RunIteratorHook) void { _ = self; } }; + + fn debugPositions( + self: *Shaper, + alloc: Allocator, + run_offset: Offset, + run_offset_y: f64, + cell_offset: Offset, + cell_offset_y: f64, + position: macos.graphics.Point, + index: usize, + ) !void { + const state = &self.run_state; + const x_offset = position.x - cell_offset.x; + const advance_x_offset = run_offset.x - cell_offset.x; + const advance_y_offset = run_offset_y - cell_offset_y; + const x_offset_diff = x_offset - advance_x_offset; + const y_offset_diff = position.y - advance_y_offset; + const positions_differ = @abs(x_offset_diff) > 0.0001 or @abs(y_offset_diff) > 0.0001; + const old_offset_y = position.y - cell_offset_y; + const position_y_differs = @abs(cell_offset_y) > 0.0001; + const cluster = state.codepoints.items[index].cluster; + const cluster_differs = cluster != cell_offset.cluster; + + // To debug every loop, flip this to true: + const extra_debugging = false; + + const is_previous_codepoint_prepend = if (cluster_differs or + extra_debugging) + blk: { + var i = index; + while (i > 0) { + i -= 1; + const codepoint = state.codepoints.items[i]; + + // Skip surrogate pair padding + if (codepoint.codepoint == 0) continue; + + break :blk unicode.table.get(@intCast(codepoint.codepoint)).grapheme_boundary_class == .prepend; + } + break :blk false; + } else false; + + const formatted_cps = if (positions_differ or + position_y_differs or + cluster_differs or + extra_debugging) + blk: { + var allocating = std.Io.Writer.Allocating.init(alloc); + const writer = &allocating.writer; + const codepoints = state.codepoints.items; + var last_cluster: ?u32 = null; + for (codepoints, 0..) |cp, i| { + if ((@as(i32, @intCast(cp.cluster)) >= @as(i32, @intCast(cell_offset.cluster)) - 1 and + cp.cluster <= cluster + 1) and + cp.codepoint != 0 // Skip surrogate pair padding + ) { + if (last_cluster) |last| { + if (cp.cluster != last) { + try writer.writeAll(" "); + } + } + if (i == index) { + try writer.writeAll("▸"); + } + // Using Python syntax for easier debugging + if (cp.codepoint > 0xFFFF) { + try writer.print("\\U{x:0>8}", .{cp.codepoint}); + } else { + try writer.print("\\u{x:0>4}", .{cp.codepoint}); + } + last_cluster = cp.cluster; + } + } + try writer.writeAll(" → "); + for (codepoints) |cp| { + if ((@as(i32, @intCast(cp.cluster)) >= @as(i32, @intCast(cell_offset.cluster)) - 1 and + cp.cluster <= cluster + 1) and + cp.codepoint != 0 // Skip surrogate pair padding + ) { + try writer.print("{u}", .{@as(u21, @intCast(cp.codepoint))}); + } + } + break :blk try allocating.toOwnedSlice(); + } else ""; + + if (extra_debugging) { + log.warn("extra debugging of positions index={d} cell_offset.cluster={d} cluster={d} run_offset.cluster={d} diff={d} pos=({d:.2},{d:.2}) run_offset=({d:.2},{d:.2}) cell_offset=({d:.2},{d:.2}) is_prev_prepend={} cps = {s}", .{ + index, + cell_offset.cluster, + cluster, + run_offset.cluster, + @as(isize, @intCast(cluster)) - @as(isize, @intCast(cell_offset.cluster)), + x_offset, + position.y, + run_offset.x, + run_offset_y, + cell_offset.x, + cell_offset_y, + is_previous_codepoint_prepend, + formatted_cps, + }); + } + + if (positions_differ) { + log.warn("position differs from advance: cluster={d} pos=({d:.2},{d:.2}) adv=({d:.2},{d:.2}) diff=({d:.2},{d:.2}) cps = {s}", .{ + cluster, + x_offset, + position.y, + advance_x_offset, + advance_y_offset, + x_offset_diff, + y_offset_diff, + formatted_cps, + }); + } + + if (position_y_differs) { + log.warn("position.y differs from old offset.y: cluster={d} pos=({d:.2},{d:.2}) run_offset=({d:.2},{d:.2}) cell_offset=({d:.2},{d:.2}) old offset.y={d:.2} cps = {s}", .{ + cluster, + x_offset, + position.y, + run_offset.x, + run_offset_y, + cell_offset.x, + cell_offset_y, + old_offset_y, + formatted_cps, + }); + } + + if (cluster_differs) { + log.warn("cell_offset.cluster differs from cluster (potential ligature detected) cell_offset.cluster={d} cluster={d} run_offset.cluster={d} diff={d} pos=({d:.2},{d:.2}) run_offset=({d:.2},{d:.2}) cell_offset=({d:.2},{d:.2}) is_prev_prepend={} cps = {s}", .{ + cell_offset.cluster, + cluster, + run_offset.cluster, + @as(isize, @intCast(cluster)) - @as(isize, @intCast(cell_offset.cluster)), + x_offset, + position.y, + run_offset.x, + run_offset_y, + cell_offset.x, + cell_offset_y, + is_previous_codepoint_prepend, + formatted_cps, + }); + } + } }; test "run iterator" { @@ -1268,7 +1487,7 @@ test "shape with empty cells in between" { } } -test "shape Chinese characters" { +test "shape Combining characters" { const testing = std.testing; const alloc = testing.allocator; @@ -1286,6 +1505,9 @@ test "shape Chinese characters" { var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); defer t.deinit(alloc); + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + var s = t.vtStream(); defer s.deinit(); try s.nextSlice(buf[0..buf_idx]); @@ -1333,6 +1555,9 @@ test "shape Devanagari string" { var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); defer t.deinit(alloc); + // Disable grapheme clustering + t.modes.set(.grapheme_cluster, false); + var s = t.vtStream(); defer s.deinit(); try s.nextSlice("अपार्टमेंट"); @@ -1352,11 +1577,13 @@ test "shape Devanagari string" { try testing.expect(run != null); const cells = try shaper.shape(run.?); + // To understand the `x`/`cluster` assertions here, run with the "For + // debugging positions" code turned on and `extra_debugging` set to true. try testing.expectEqual(@as(usize, 8), cells.len); try testing.expectEqual(@as(u16, 0), cells[0].x); try testing.expectEqual(@as(u16, 1), cells[1].x); try testing.expectEqual(@as(u16, 2), cells[2].x); - try testing.expectEqual(@as(u16, 3), cells[3].x); + try testing.expectEqual(@as(u16, 4), cells[3].x); try testing.expectEqual(@as(u16, 4), cells[4].x); try testing.expectEqual(@as(u16, 5), cells[5].x); try testing.expectEqual(@as(u16, 5), cells[6].x); @@ -1365,6 +1592,325 @@ test "shape Devanagari string" { try testing.expect(try it.next(alloc) == null); } +test "shape Tai Tham vowels (position differs from advance)" { + const testing = std.testing; + const alloc = testing.allocator; + + // We need a font that supports Tai Tham for this to work, if we can't find + // Noto Sans Tai Tham, which is a system font on macOS, we just skip the + // test. + var testdata = testShaperWithDiscoveredFont( + alloc, + "Noto Sans Tai Tham", + ) catch return error.SkipZigTest; + defer testdata.deinit(); + + var buf: [32]u8 = undefined; + var buf_idx: usize = 0; + buf_idx += try std.unicode.utf8Encode(0x1a2F, buf[buf_idx..]); // ᨯ + buf_idx += try std.unicode.utf8Encode(0x1a70, buf[buf_idx..]); // ᩰ + + // Make a screen with some data + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .cells = state.row_data.get(0).cells.slice(), + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + + const cells = try shaper.shape(run); + const cell_width = run.grid.metrics.cell_width; + try testing.expectEqual(@as(usize, 2), cells.len); + try testing.expectEqual(@as(u16, 0), cells[0].x); + try testing.expectEqual(@as(u16, 0), cells[1].x); + + // The first glyph renders in the next cell + try testing.expectEqual(@as(i16, @intCast(cell_width)), cells[0].x_offset); + try testing.expectEqual(@as(i16, 0), cells[1].x_offset); + } + try testing.expectEqual(@as(usize, 1), count); +} + +test "shape Tai Tham letters (position.y differs from advance)" { + const testing = std.testing; + const alloc = testing.allocator; + + // We need a font that supports Tai Tham for this to work, if we can't find + // Noto Sans Tai Tham, which is a system font on macOS, we just skip the + // test. + var testdata = testShaperWithDiscoveredFont( + alloc, + "Noto Sans Tai Tham", + ) catch return error.SkipZigTest; + defer testdata.deinit(); + + var buf: [32]u8 = undefined; + var buf_idx: usize = 0; + + // First grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0x1a49, buf[buf_idx..]); // HA + buf_idx += try std.unicode.utf8Encode(0x1a60, buf[buf_idx..]); // SAKOT + // Second grapheme cluster, combining with the first in a ligature: + buf_idx += try std.unicode.utf8Encode(0x1a3f, buf[buf_idx..]); // YA + buf_idx += try std.unicode.utf8Encode(0x1a69, buf[buf_idx..]); // U + + // Make a screen with some data + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .cells = state.row_data.get(0).cells.slice(), + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + + const cells = try shaper.shape(run); + try testing.expectEqual(@as(usize, 3), cells.len); + try testing.expectEqual(@as(u16, 0), cells[0].x); + try testing.expectEqual(@as(u16, 0), cells[1].x); + try testing.expectEqual(@as(u16, 0), cells[2].x); // U from second grapheme + + // The U glyph renders at a y below zero + try testing.expectEqual(@as(i16, -3), cells[2].y_offset); + } + try testing.expectEqual(@as(usize, 1), count); +} + +test "shape Javanese ligatures" { + const testing = std.testing; + const alloc = testing.allocator; + + // We need a font that supports Javanese for this to work, if we can't find + // Noto Sans Javanese Regular, which is a system font on macOS, we just + // skip the test. + var testdata = testShaperWithDiscoveredFont( + alloc, + "Noto Sans Javanese", + ) catch return error.SkipZigTest; + defer testdata.deinit(); + + var buf: [32]u8 = undefined; + var buf_idx: usize = 0; + + // First grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0xa9a4, buf[buf_idx..]); // NA + buf_idx += try std.unicode.utf8Encode(0xa9c0, buf[buf_idx..]); // PANGKON + // Second grapheme cluster, combining with the first in a ligature: + buf_idx += try std.unicode.utf8Encode(0xa9b2, buf[buf_idx..]); // HA + buf_idx += try std.unicode.utf8Encode(0xa9b8, buf[buf_idx..]); // Vowel sign SUKU + + // Make a screen with some data + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .cells = state.row_data.get(0).cells.slice(), + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + + const cells = try shaper.shape(run); + const cell_width = run.grid.metrics.cell_width; + try testing.expectEqual(@as(usize, 3), cells.len); + try testing.expectEqual(@as(u16, 0), cells[0].x); + try testing.expectEqual(@as(u16, 0), cells[1].x); + try testing.expectEqual(@as(u16, 0), cells[2].x); + + // The vowel sign SUKU renders with correct x_offset + try testing.expect(cells[2].x_offset > 3 * cell_width); + } + try testing.expectEqual(@as(usize, 1), count); +} + +test "shape Chakma vowel sign with ligature (vowel sign renders first)" { + const testing = std.testing; + const alloc = testing.allocator; + + // We need a font that supports Chakma for this to work, if we can't find + // Noto Sans Chakma Regular, which is a system font on macOS, we just skip + // the test. + var testdata = testShaperWithDiscoveredFont( + alloc, + "Noto Sans Chakma", + ) catch return error.SkipZigTest; + defer testdata.deinit(); + + var buf: [32]u8 = undefined; + var buf_idx: usize = 0; + + // First grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0x1111d, buf[buf_idx..]); // BAA + // Second grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0x11116, buf[buf_idx..]); // TAA + buf_idx += try std.unicode.utf8Encode(0x11133, buf[buf_idx..]); // Virama + // Third grapheme cluster, combining with the second in a ligature: + buf_idx += try std.unicode.utf8Encode(0x11120, buf[buf_idx..]); // YYAA + buf_idx += try std.unicode.utf8Encode(0x1112c, buf[buf_idx..]); // Vowel Sign U + + // Make a screen with some data + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .cells = state.row_data.get(0).cells.slice(), + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + + const cells = try shaper.shape(run); + try testing.expectEqual(@as(usize, 4), cells.len); + try testing.expectEqual(@as(u16, 0), cells[0].x); + // See the giant "We need to reset the `cell_offset`" comment, but here + // we should technically have the rest of these be `x` of 1, but that + // would require going back in the stream to adjust past cells, and + // we don't take on that complexity. + try testing.expectEqual(@as(u16, 0), cells[1].x); + try testing.expectEqual(@as(u16, 0), cells[2].x); + try testing.expectEqual(@as(u16, 0), cells[3].x); + + // The vowel sign U renders before the TAA: + try testing.expect(cells[1].x_offset < cells[2].x_offset); + } + try testing.expectEqual(@as(usize, 1), count); +} + +test "shape Bengali ligatures with out of order vowels" { + const testing = std.testing; + const alloc = testing.allocator; + + // We need a font that supports Bengali for this to work, if we can't find + // Arial Unicode MS, which is a system font on macOS, we just skip the + // test. + var testdata = testShaperWithDiscoveredFont( + alloc, + "Arial Unicode MS", + ) catch return error.SkipZigTest; + defer testdata.deinit(); + + var buf: [32]u8 = undefined; + var buf_idx: usize = 0; + + // First grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0x09b0, buf[buf_idx..]); // RA + buf_idx += try std.unicode.utf8Encode(0x09be, buf[buf_idx..]); // Vowel sign AA + // Second grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0x09b7, buf[buf_idx..]); // SSA + buf_idx += try std.unicode.utf8Encode(0x09cd, buf[buf_idx..]); // Virama + // Third grapheme cluster, combining with the second in a ligature: + buf_idx += try std.unicode.utf8Encode(0x099f, buf[buf_idx..]); // TTA + buf_idx += try std.unicode.utf8Encode(0x09cd, buf[buf_idx..]); // Virama + // Fourth grapheme cluster, combining with the previous two in a ligature: + buf_idx += try std.unicode.utf8Encode(0x09b0, buf[buf_idx..]); // RA + buf_idx += try std.unicode.utf8Encode(0x09c7, buf[buf_idx..]); // Vowel sign E + + // Make a screen with some data + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .cells = state.row_data.get(0).cells.slice(), + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + + const cells = try shaper.shape(run); + try testing.expectEqual(@as(usize, 8), cells.len); + try testing.expectEqual(@as(u16, 0), cells[0].x); + try testing.expectEqual(@as(u16, 0), cells[1].x); + // See the giant "We need to reset the `cell_offset`" comment, but here + // we should technically have the rest of these be `x` of 1, but that + // would require going back in the stream to adjust past cells, and + // we don't take on that complexity. + try testing.expectEqual(@as(u16, 0), cells[2].x); + try testing.expectEqual(@as(u16, 0), cells[3].x); + try testing.expectEqual(@as(u16, 0), cells[4].x); + try testing.expectEqual(@as(u16, 0), cells[5].x); + try testing.expectEqual(@as(u16, 0), cells[6].x); + try testing.expectEqual(@as(u16, 0), cells[7].x); + + // The vowel sign E renders before the SSA: + try testing.expect(cells[2].x_offset < cells[3].x_offset); + } + try testing.expectEqual(@as(usize, 1), count); +} + test "shape box glyphs" { const testing = std.testing; const alloc = testing.allocator; @@ -2102,7 +2648,7 @@ fn testShaperWithDiscoveredFont(alloc: Allocator, font_req: [:0]const u8) !TestS .monospace = false, }); defer disco_it.deinit(); - var face: font.DeferredFace = (try disco_it.next()).?; + var face: font.DeferredFace = (try disco_it.next()) orelse return error.FontNotFound; errdefer face.deinit(); _ = try c.add( alloc, diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index e4a9301e8..946611e79 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator; const harfbuzz = @import("harfbuzz"); const font = @import("../main.zig"); const terminal = @import("../../terminal/main.zig"); +const unicode = @import("../../unicode/main.zig"); const Feature = font.shape.Feature; const FeatureList = font.shape.FeatureList; const default_features = font.shape.default_features; @@ -19,7 +20,7 @@ const log = std.log.scoped(.font_shaper); /// Shaper that uses Harfbuzz. pub const Shaper = struct { - /// The allocated used for the feature list and cell buf. + /// The allocated used for the feature list, cell buf, and codepoints. alloc: Allocator, /// The buffer used for text shaping. We reuse it across multiple shaping @@ -32,8 +33,29 @@ pub const Shaper = struct { /// The features to use for shaping. hb_feats: []harfbuzz.Feature, + /// The codepoints added to the buffer before shaping. We need to keep + /// these separately because after shaping, HarfBuzz replaces codepoints + /// with glyph indices in the buffer. + codepoints: std.ArrayListUnmanaged(Codepoint) = .{}, + + const Codepoint = struct { + cluster: u32, + codepoint: u32, + }; + const CellBuf = std.ArrayListUnmanaged(font.shape.Cell); + const RunOffset = struct { + cluster: u32 = 0, + x: i32 = 0, + y: i32 = 0, + }; + + const CellOffset = struct { + cluster: u32 = 0, + x: i32 = 0, + }; + /// The cell_buf argument is the buffer to use for storing shaped results. /// This should be at least the number of columns in the terminal. pub fn init(alloc: Allocator, opts: font.shape.Options) !Shaper { @@ -74,6 +96,7 @@ pub const Shaper = struct { self.hb_buf.destroy(); self.cell_buf.deinit(self.alloc); self.alloc.free(self.hb_feats); + self.codepoints.deinit(self.alloc); } pub fn endFrame(self: *const Shaper) void { @@ -135,33 +158,97 @@ pub const Shaper = struct { // If it isn't true, I'd like to catch it and learn more. assert(info.len == pos.len); - // This keeps track of the current offsets within a single cell. - var cell_offset: struct { - cluster: u32 = 0, - x: i32 = 0, - y: i32 = 0, - } = .{}; + // This keeps track of the current x and y offsets (sum of advances) + // and the furthest cluster we've seen so far (max). + var run_offset: RunOffset = .{}; + + // This keeps track of the cell starting x and cluster. + var cell_offset: CellOffset = .{}; // Convert all our info/pos to cells and set it. self.cell_buf.clearRetainingCapacity(); for (info, pos) |info_v, pos_v| { - // If our cluster changed then we've moved to a new cell. - if (info_v.cluster != cell_offset.cluster) cell_offset = .{ - .cluster = info_v.cluster, - }; + // info_v.cluster is the index into our codepoints array. We use it + // to get the original cluster. + const index = info_v.cluster; + // Our cluster is also our cell X position. If the cluster changes + // then we need to reset our current cell offsets. + const cluster = self.codepoints.items[index].cluster; + if (cell_offset.cluster != cluster) { + const is_after_glyph_from_current_or_next_clusters = + cluster <= run_offset.cluster; - try self.cell_buf.append(self.alloc, .{ - .x = @intCast(info_v.cluster), - .x_offset = @intCast(cell_offset.x), - .y_offset = @intCast(cell_offset.y), - .glyph_index = info_v.codepoint, - }); + const is_first_codepoint_in_cluster = blk: { + var i = index; + while (i > 0) { + i -= 1; + const codepoint = self.codepoints.items[i]; + break :blk codepoint.cluster != cluster; + } else break :blk true; + }; + + // We need to reset the `cell_offset` at the start of a new + // cluster, but we do that conditionally if the codepoint + // `is_first_codepoint_in_cluster` and the cluster is not + // `is_after_glyph_from_current_or_next_clusters`, which is + // a heuristic to detect ligatures and avoid positioning + // glyphs that mark ligatures incorrectly. The idea is that + // if the first codepoint in a cluster doesn't appear in + // the stream, it's very likely that it combined with + // codepoints from a previous cluster into a ligature. + // Then, the subsequent codepoints are very likely marking + // glyphs that are placed relative to that ligature, so if + // we were to reset the `cell_offset` to align it with the + // grid, the positions would be off. The + // `!is_after_glyph_from_current_or_next_clusters` check is + // needed in case these marking glyphs come from a later + // cluster but are rendered first (see the Chakma and + // Bengali tests). In that case when we get to the + // codepoint that `is_first_codepoint_in_cluster`, but in a + // cluster that + // `is_after_glyph_from_current_or_next_clusters`, we don't + // want to reset to the grid and cause the positions to be + // off. (Note that we could go back and align the cells to + // the grid starting from the one from the cluster that + // rendered out of order, but that is more complicated so + // we don't do that for now. Also, it's TBD if there are + // exceptions to this heuristic for detecting ligatures, + // but using the logging below seems to show it works + // well.) + if (is_first_codepoint_in_cluster and + !is_after_glyph_from_current_or_next_clusters) + { + cell_offset = .{ + .cluster = cluster, + .x = run_offset.x, + }; + } + } // Under both FreeType and CoreText the harfbuzz scale is // in 26.6 fixed point units, so we round to the nearest // whole value here. - cell_offset.x += (pos_v.x_advance + 0b100_000) >> 6; - cell_offset.y += (pos_v.y_advance + 0b100_000) >> 6; + const x_offset = run_offset.x - cell_offset.x + ((pos_v.x_offset + 0b100_000) >> 6); + const y_offset = run_offset.y + ((pos_v.y_offset + 0b100_000) >> 6); + + // For debugging positions, turn this on: + //try self.debugPositions(run_offset, cell_offset, pos_v, index); + + try self.cell_buf.append(self.alloc, .{ + .x = @intCast(cell_offset.cluster), + .x_offset = @intCast(x_offset), + .y_offset = @intCast(y_offset), + .glyph_index = info_v.codepoint, + }); + + // Add our advances to keep track of our run offsets. + // Advances apply to the NEXT cell. + // Under both FreeType and CoreText the harfbuzz scale is + // in 26.6 fixed point units, so we round to the nearest + // whole value here. + run_offset.x += (pos_v.x_advance + 0b100_000) >> 6; + run_offset.y += (pos_v.y_advance + 0b100_000) >> 6; + run_offset.cluster = @max(run_offset.cluster, cluster); // const i = self.cell_buf.items.len - 1; // log.warn("i={} info={} pos={} cell={}", .{ i, info_v, pos_v, self.cell_buf.items[i] }); @@ -175,11 +262,18 @@ pub const Shaper = struct { pub const RunIteratorHook = struct { shaper: *Shaper, - pub fn prepare(self: RunIteratorHook) !void { + pub fn prepare(self: RunIteratorHook) void { // Reset the buffer for our current run self.shaper.hb_buf.reset(); self.shaper.hb_buf.setContentType(.unicode); + // We set the cluster level to `characters` to give us the most + // granularity, matching the CoreText shaper, and allowing us + // to use our same ligature detection heuristics. + self.shaper.hb_buf.setClusterLevel(.characters); + + self.shaper.codepoints.clearRetainingCapacity(); + // We don't support RTL text because RTL in terminals is messy. // Its something we want to improve. For now, we force LTR because // our renderers assume a strictly increasing X value. @@ -188,13 +282,156 @@ pub const Shaper = struct { pub fn addCodepoint(self: RunIteratorHook, cp: u32, cluster: u32) !void { // log.warn("cluster={} cp={x}", .{ cluster, cp }); - self.shaper.hb_buf.add(cp, cluster); + // We pass the index into codepoints as the cluster value to HarfBuzz. + // After shaping, we use info.cluster to get back the index, which + // lets us look up the original cluster value from codepoints. + const index: u32 = @intCast(self.shaper.codepoints.items.len); + self.shaper.hb_buf.add(cp, index); + try self.shaper.codepoints.append(self.shaper.alloc, .{ + .cluster = cluster, + .codepoint = cp, + }); } - pub fn finalize(self: RunIteratorHook) !void { + pub fn finalize(self: RunIteratorHook) void { self.shaper.hb_buf.guessSegmentProperties(); } }; + + fn debugPositions( + self: *Shaper, + run_offset: RunOffset, + cell_offset: CellOffset, + pos_v: harfbuzz.GlyphPosition, + index: u32, + ) !void { + const x_offset = run_offset.x - cell_offset.x + ((pos_v.x_offset + 0b100_000) >> 6); + const y_offset = run_offset.y + ((pos_v.y_offset + 0b100_000) >> 6); + const advance_x_offset = run_offset.x - cell_offset.x; + const advance_y_offset = run_offset.y; + const x_offset_diff = x_offset - advance_x_offset; + const y_offset_diff = y_offset - advance_y_offset; + const positions_differ = @abs(x_offset_diff) > 0 or @abs(y_offset_diff) > 0; + const y_offset_differs = run_offset.y != 0; + const cluster = self.codepoints.items[index].cluster; + const cluster_differs = cluster != cell_offset.cluster; + + // To debug every loop, flip this to true: + const extra_debugging = false; + + const is_previous_codepoint_prepend = if (cluster_differs or + extra_debugging) + blk: { + var i = index; + while (i > 0) { + i -= 1; + const codepoint = self.codepoints.items[i]; + break :blk unicode.table.get(@intCast(codepoint.codepoint)).grapheme_boundary_class == .prepend; + } + break :blk false; + } else false; + + const formatted_cps: ?[]u8 = if (positions_differ or + y_offset_differs or + cluster_differs or + extra_debugging) + blk: { + var allocating = std.Io.Writer.Allocating.init(self.alloc); + defer allocating.deinit(); + const writer = &allocating.writer; + const codepoints = self.codepoints.items; + var last_cluster: ?u32 = null; + for (codepoints, 0..) |cp, i| { + if (@as(i32, @intCast(cp.cluster)) >= @as(i32, @intCast(cell_offset.cluster)) - 1 and + cp.cluster <= cluster + 1) + { + if (last_cluster) |last| { + if (cp.cluster != last) { + try writer.writeAll(" "); + } + } + if (i == index) { + try writer.writeAll("▸"); + } + // Using Python syntax for easier debugging + if (cp.codepoint > 0xFFFF) { + try writer.print("\\U{x:0>8}", .{cp.codepoint}); + } else { + try writer.print("\\u{x:0>4}", .{cp.codepoint}); + } + last_cluster = cp.cluster; + } + } + try writer.writeAll(" → "); + for (codepoints) |cp| { + if (@as(i32, @intCast(cp.cluster)) >= @as(i32, @intCast(cell_offset.cluster)) - 1 and + cp.cluster <= cluster + 1) + { + try writer.print("{u}", .{@as(u21, @intCast(cp.codepoint))}); + } + } + break :blk try allocating.toOwnedSlice(); + } else null; + defer if (formatted_cps) |cps| self.alloc.free(cps); + + if (extra_debugging) { + log.warn("extra debugging of positions index={d} cell_offset.cluster={d} cluster={d} run_offset.cluster={d} diff={d} pos=({d},{d}) run_offset=({d},{d}) cell_offset.x={d} is_prev_prepend={} cps = {s}", .{ + index, + cell_offset.cluster, + cluster, + run_offset.cluster, + @as(isize, @intCast(cluster)) - @as(isize, @intCast(cell_offset.cluster)), + x_offset, + y_offset, + run_offset.x, + run_offset.y, + cell_offset.x, + is_previous_codepoint_prepend, + formatted_cps.?, + }); + } + + if (positions_differ) { + log.warn("position differs from advance: cluster={d} pos=({d},{d}) adv=({d},{d}) diff=({d},{d}) cps = {s}", .{ + cluster, + x_offset, + y_offset, + advance_x_offset, + advance_y_offset, + x_offset_diff, + y_offset_diff, + formatted_cps.?, + }); + } + + if (y_offset_differs) { + log.warn("run_offset.y differs from zero: cluster={d} pos=({d},{d}) run_offset=({d},{d}) cell_offset.x={d} cps = {s}", .{ + cluster, + x_offset, + y_offset, + run_offset.x, + run_offset.y, + cell_offset.x, + formatted_cps.?, + }); + } + + if (cluster_differs) { + log.warn("cell_offset.cluster differs from cluster (potential ligature detected) cell_offset.cluster={d} cluster={d} run_offset.cluster={d} diff={d} pos=({d},{d}) run_offset=({d},{d}) cell_offset.x={d} is_prev_prepend={} cps = {s}", .{ + cell_offset.cluster, + cluster, + run_offset.cluster, + @as(isize, @intCast(cluster)) - @as(isize, @intCast(cell_offset.cluster)), + x_offset, + y_offset, + run_offset.x, + run_offset.y, + cell_offset.x, + is_previous_codepoint_prepend, + formatted_cps.?, + }); + } + } }; test "run iterator" { @@ -737,7 +974,7 @@ test "shape with empty cells in between" { } } -test "shape Chinese characters" { +test "shape Combining characters" { const testing = std.testing; const alloc = testing.allocator; @@ -786,6 +1023,443 @@ test "shape Chinese characters" { try testing.expectEqual(@as(usize, 1), count); } +// This test exists because the string it uses causes HarfBuzz to output a +// non-monotonic run with our cluster level set to `characters`, which we need +// to handle by tracking the max cluster for the run. +test "shape Devanagari string" { + const testing = std.testing; + const alloc = testing.allocator; + + // We need a font that supports devanagari for this to work, if we can't + // find Arial Unicode MS, which is a system font on macOS, we just skip + // the test. + var testdata = testShaperWithDiscoveredFont( + alloc, + "Arial Unicode MS", + ) catch return error.SkipZigTest; + defer testdata.deinit(); + + // Make a screen with some data + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + // Disable grapheme clustering + t.modes.set(.grapheme_cluster, false); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice("अपार्टमेंट"); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .cells = state.row_data.get(0).cells.slice(), + }); + + const run = try it.next(alloc); + try testing.expect(run != null); + const cells = try shaper.shape(run.?); + + try testing.expectEqual(@as(usize, 8), cells.len); + try testing.expectEqual(@as(u16, 0), cells[0].x); + try testing.expectEqual(@as(u16, 1), cells[1].x); + try testing.expectEqual(@as(u16, 2), cells[2].x); + try testing.expectEqual(@as(u16, 4), cells[3].x); + try testing.expectEqual(@as(u16, 4), cells[4].x); + try testing.expectEqual(@as(u16, 5), cells[5].x); + try testing.expectEqual(@as(u16, 5), cells[6].x); + try testing.expectEqual(@as(u16, 6), cells[7].x); + + try testing.expect(try it.next(alloc) == null); +} + +test "shape Tai Tham vowels (position differs from advance)" { + // Note that while this test was necessary for CoreText, the old logic was + // working for HarfBuzz. Still we keep it to ensure it has the correct + // behavior. + const testing = std.testing; + const alloc = testing.allocator; + + // We need a font that supports Tai Tham for this to work, if we can't find + // Noto Sans Tai Tham, which is a system font on macOS, we just skip the + // test. + var testdata = testShaperWithDiscoveredFont( + alloc, + "Noto Sans Tai Tham", + ) catch return error.SkipZigTest; + defer testdata.deinit(); + + var buf: [32]u8 = undefined; + var buf_idx: usize = 0; + buf_idx += try std.unicode.utf8Encode(0x1a2F, buf[buf_idx..]); // ᨯ + buf_idx += try std.unicode.utf8Encode(0x1a70, buf[buf_idx..]); // ᩰ + + // Make a screen with some data + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .cells = state.row_data.get(0).cells.slice(), + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + + const cells = try shaper.shape(run); + try testing.expectEqual(@as(usize, 2), cells.len); + try testing.expectEqual(@as(u16, 0), cells[0].x); + try testing.expectEqual(@as(u16, 0), cells[1].x); + + // The first glyph renders in the next cell. We expect the x_offset + // to equal the cell width. However, with FreeType the cell_width is + // computed from ASCII glyphs, and Noto Sans Tai Tham only has the + // space character in ASCII (with a 3px advance), so the cell_width + // metric doesn't match the actual Tai Tham glyph positioning. + const expected_x_offset: i16 = if (comptime font.options.backend.hasFreetype()) 7 else @intCast(run.grid.metrics.cell_width); + try testing.expectEqual(expected_x_offset, cells[0].x_offset); + try testing.expectEqual(@as(i16, 0), cells[1].x_offset); + } + try testing.expectEqual(@as(usize, 1), count); +} + +test "shape Tibetan characters" { + const testing = std.testing; + const alloc = testing.allocator; + + // We need a font that has multiple glyphs for this codepoint to reproduce + // the old broken behavior, and Noto Serif Tibetan is one of them. It's not + // a default Mac font, and if we can't find it we just skip the test. + var testdata = testShaperWithDiscoveredFont( + alloc, + "Noto Serif Tibetan", + ) catch return error.SkipZigTest; + defer testdata.deinit(); + + var buf: [32]u8 = undefined; + var buf_idx: usize = 0; + buf_idx += try std.unicode.utf8Encode(0x0f00, buf[buf_idx..]); // ༀ + + // Make a screen with some data + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .cells = state.row_data.get(0).cells.slice(), + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + + const cells = try shaper.shape(run); + try testing.expectEqual(@as(usize, 2), cells.len); + try testing.expectEqual(@as(u16, 0), cells[0].x); + try testing.expectEqual(@as(u16, 0), cells[1].x); + + // The second glyph renders at the correct location + try testing.expect(cells[1].x_offset < 2); + } + try testing.expectEqual(@as(usize, 1), count); +} + +test "shape Tai Tham letters (run_offset.y differs from zero)" { + const testing = std.testing; + const alloc = testing.allocator; + + // We need a font that supports Tai Tham for this to work, if we can't find + // Noto Sans Tai Tham, which is a system font on macOS, we just skip the + // test. + var testdata = testShaperWithDiscoveredFont( + alloc, + "Noto Sans Tai Tham", + ) catch return error.SkipZigTest; + defer testdata.deinit(); + + var buf: [32]u8 = undefined; + var buf_idx: usize = 0; + + // First grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0x1a49, buf[buf_idx..]); // HA + buf_idx += try std.unicode.utf8Encode(0x1a60, buf[buf_idx..]); // SAKOT + // Second grapheme cluster, combining with the first in a ligature: + buf_idx += try std.unicode.utf8Encode(0x1a3f, buf[buf_idx..]); // YA + buf_idx += try std.unicode.utf8Encode(0x1a69, buf[buf_idx..]); // U + + // Make a screen with some data + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .cells = state.row_data.get(0).cells.slice(), + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + + const cells = try shaper.shape(run); + try testing.expectEqual(@as(usize, 3), cells.len); + try testing.expectEqual(@as(u16, 0), cells[0].x); + try testing.expectEqual(@as(u16, 0), cells[1].x); + try testing.expectEqual(@as(u16, 0), cells[2].x); // U from second grapheme + + // The U glyph renders at a y below zero + try testing.expectEqual(@as(i16, -3), cells[2].y_offset); + } + try testing.expectEqual(@as(usize, 1), count); +} + +test "shape Javanese ligatures" { + const testing = std.testing; + const alloc = testing.allocator; + + // We need a font that supports Javanese for this to work, if we can't find + // Noto Sans Javanese Regular, which is a system font on macOS, we just + // skip the test. + var testdata = testShaperWithDiscoveredFont( + alloc, + "Noto Sans Javanese", + ) catch return error.SkipZigTest; + defer testdata.deinit(); + + var buf: [32]u8 = undefined; + var buf_idx: usize = 0; + + // First grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0xa9a4, buf[buf_idx..]); // NA + buf_idx += try std.unicode.utf8Encode(0xa9c0, buf[buf_idx..]); // PANGKON + // Second grapheme cluster, combining with the first in a ligature: + buf_idx += try std.unicode.utf8Encode(0xa9b2, buf[buf_idx..]); // HA + buf_idx += try std.unicode.utf8Encode(0xa9b8, buf[buf_idx..]); // Vowel sign SUKU + + // Make a screen with some data + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .cells = state.row_data.get(0).cells.slice(), + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + + const cells = try shaper.shape(run); + const cell_width = run.grid.metrics.cell_width; + try testing.expectEqual(@as(usize, 3), cells.len); + try testing.expectEqual(@as(u16, 0), cells[0].x); + try testing.expectEqual(@as(u16, 0), cells[1].x); + try testing.expectEqual(@as(u16, 0), cells[2].x); + + // The vowel sign SUKU renders with correct x_offset + try testing.expect(cells[2].x_offset > 3 * cell_width); + } + try testing.expectEqual(@as(usize, 1), count); +} + +test "shape Chakma vowel sign with ligature (vowel sign renders first)" { + const testing = std.testing; + const alloc = testing.allocator; + + // We need a font that supports Chakma for this to work, if we can't find + // Noto Sans Chakma Regular, which is a system font on macOS, we just skip + // the test. + var testdata = testShaperWithDiscoveredFont( + alloc, + "Noto Sans Chakma", + ) catch return error.SkipZigTest; + defer testdata.deinit(); + + var buf: [32]u8 = undefined; + var buf_idx: usize = 0; + + // First grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0x1111d, buf[buf_idx..]); // BAA + // Second grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0x11116, buf[buf_idx..]); // TAA + buf_idx += try std.unicode.utf8Encode(0x11133, buf[buf_idx..]); // Virama + // Third grapheme cluster, combining with the second in a ligature: + buf_idx += try std.unicode.utf8Encode(0x11120, buf[buf_idx..]); // YYAA + buf_idx += try std.unicode.utf8Encode(0x1112c, buf[buf_idx..]); // Vowel Sign U + + // Make a screen with some data + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .cells = state.row_data.get(0).cells.slice(), + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + + const cells = try shaper.shape(run); + try testing.expectEqual(@as(usize, 4), cells.len); + try testing.expectEqual(@as(u16, 0), cells[0].x); + // See the giant "We need to reset the `cell_offset`" comment, but here + // we should technically have the rest of these be `x` of 1, but that + // would require going back in the stream to adjust past cells, and + // we don't take on that complexity. + try testing.expectEqual(@as(u16, 0), cells[1].x); + try testing.expectEqual(@as(u16, 0), cells[2].x); + try testing.expectEqual(@as(u16, 0), cells[3].x); + + // The vowel sign U renders before the TAA: + try testing.expect(cells[1].x_offset < cells[2].x_offset); + } + try testing.expectEqual(@as(usize, 1), count); +} + +test "shape Bengali ligatures with out of order vowels" { + // Whereas this test in CoreText had everything shaping into one giant + // ligature, HarfBuzz splits it into a few clusters. It still looks okay + // (see #10332). + + const testing = std.testing; + const alloc = testing.allocator; + + // We need a font that supports Bengali for this to work, if we can't find + // Arial Unicode MS, which is a system font on macOS, we just skip the + // test. + var testdata = testShaperWithDiscoveredFont( + alloc, + "Arial Unicode MS", + ) catch return error.SkipZigTest; + defer testdata.deinit(); + + var buf: [32]u8 = undefined; + var buf_idx: usize = 0; + + // First grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0x09b0, buf[buf_idx..]); // RA + buf_idx += try std.unicode.utf8Encode(0x09be, buf[buf_idx..]); // Vowel sign AA + // Second grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0x09b7, buf[buf_idx..]); // SSA + buf_idx += try std.unicode.utf8Encode(0x09cd, buf[buf_idx..]); // Virama + // Third grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0x099f, buf[buf_idx..]); // TTA + buf_idx += try std.unicode.utf8Encode(0x09cd, buf[buf_idx..]); // Virama + // Fourth grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0x09b0, buf[buf_idx..]); // RA + buf_idx += try std.unicode.utf8Encode(0x09c7, buf[buf_idx..]); // Vowel sign E + + // Make a screen with some data + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .cells = state.row_data.get(0).cells.slice(), + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + + const cells = try shaper.shape(run); + try testing.expectEqual(@as(usize, 8), cells.len); + try testing.expectEqual(@as(u16, 0), cells[0].x); + try testing.expectEqual(@as(u16, 0), cells[1].x); + + // Whereas CoreText puts everything all into the first cell (see the + // corresponding test), HarfBuzz splits into two clusters. + try testing.expectEqual(@as(u16, 1), cells[2].x); + try testing.expectEqual(@as(u16, 1), cells[3].x); + try testing.expectEqual(@as(u16, 1), cells[4].x); + try testing.expectEqual(@as(u16, 1), cells[5].x); + try testing.expectEqual(@as(u16, 1), cells[6].x); + try testing.expectEqual(@as(u16, 1), cells[7].x); + + // The vowel sign E renders before the SSA: + try testing.expect(cells[2].x_offset < cells[3].x_offset); + } + try testing.expectEqual(@as(usize, 1), count); +} + test "shape box glyphs" { const testing = std.testing; const alloc = testing.allocator; @@ -1432,3 +2106,58 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { .lib = lib, }; } + +fn testShaperWithDiscoveredFont(alloc: Allocator, font_req: [:0]const u8) !TestShaper { + if (font.Discover == void) return error.SkipZigTest; + var lib = try Library.init(alloc); + errdefer lib.deinit(); + + var c = Collection.init(); + c.load_options = .{ .library = lib }; + + // Discover and add our font to the collection. + { + var disco = font.Discover.init(); + defer disco.deinit(); + var disco_it = try disco.discover(alloc, .{ + .family = font_req, + .size = 12, + .monospace = false, + }); + defer disco_it.deinit(); + var face: font.DeferredFace = (try disco_it.next()) orelse return error.FontNotFound; + errdefer face.deinit(); + + // Check which font was discovered - skip if it doesn't match the request + var name_buf: [256]u8 = undefined; + const face_name = face.name(&name_buf) catch "(unknown)"; + if (std.mem.indexOf(u8, face_name, font_req) == null) { + return error.SkipZigTest; + } + + _ = try c.add( + alloc, + try face.load(lib, .{ .size = .{ .points = 12 } }), + .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }, + ); + } + + const grid_ptr = try alloc.create(SharedGrid); + errdefer alloc.destroy(grid_ptr); + grid_ptr.* = try .init(alloc, .{ .collection = c }); + errdefer grid_ptr.*.deinit(alloc); + + var shaper = try Shaper.init(alloc, .{}); + errdefer shaper.deinit(); + + return TestShaper{ + .alloc = alloc, + .shaper = shaper, + .grid = grid_ptr, + .lib = lib, + }; +} diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 85c5c410b..45c5d38ca 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -73,7 +73,7 @@ pub const RunIterator = struct { var current_font: font.Collection.Index = .{}; // Allow the hook to prepare - try self.hooks.prepare(); + self.hooks.prepare(); // Initialize our hash for this run. var hasher = Hasher.init(0); @@ -283,7 +283,7 @@ pub const RunIterator = struct { } // Finalize our buffer - try self.hooks.finalize(); + self.hooks.finalize(); // Add our length to the hash as an additional mechanism to avoid collisions autoHash(&hasher, j - self.i); diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index 94bfa2f0b..596a92044 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -405,7 +405,7 @@ fn testDrawRanges( const padding_x = width / 4; const padding_y = height / 4; - // Canvas to draw glyphs on, we'll re-use this for all glyphs. + // Canvas to draw glyphs on, we'll reuse this for all glyphs. var canvas = try font.sprite.Canvas.init( alloc, width, diff --git a/src/font/sprite/canvas.zig b/src/font/sprite/canvas.zig index 19d27eb45..7904f20a5 100644 --- a/src/font/sprite/canvas.zig +++ b/src/font/sprite/canvas.zig @@ -360,7 +360,7 @@ pub const Canvas = struct { pub fn strokePath( self: *Canvas, path: z2d.Path, - opts: z2d.painter.StrokeOpts, + opts: z2d.painter.StrokeOptions, color: Color, ) z2d.painter.StrokeError!void { try z2d.painter.stroke( @@ -380,7 +380,7 @@ pub const Canvas = struct { pub fn innerStrokePath( self: *Canvas, path: z2d.Path, - opts: z2d.painter.StrokeOpts, + opts: z2d.painter.StrokeOptions, color: Color, ) (z2d.painter.StrokeError || z2d.painter.FillError)!void { // On one surface we fill the shape, this will be a mask we @@ -459,7 +459,7 @@ pub const Canvas = struct { pub fn fillPath( self: *Canvas, path: z2d.Path, - opts: z2d.painter.FillOpts, + opts: z2d.painter.FillOptions, color: Color, ) z2d.painter.FillError!void { try z2d.painter.fill( diff --git a/src/global.zig b/src/global.zig index 8034fabe0..29eaf5f36 100644 --- a/src/global.zig +++ b/src/global.zig @@ -39,9 +39,13 @@ pub const GlobalState = struct { resources_dir: internal_os.ResourcesDir, /// Where logging should go - pub const Logging = union(enum) { - disabled: void, - stderr: void, + pub const Logging = packed struct { + /// Whether to log to stderr. For lib mode we always disable stderr + /// logging by default. Otherwise it's enabled by default. + stderr: bool = build_config.app_runtime != .none, + /// Whether to log to macOS's unified logging. Enabled by default + /// on macOS. + macos: bool = builtin.os.tag.isDarwin(), }; /// Initialize the global state. @@ -61,7 +65,7 @@ pub const GlobalState = struct { .gpa = null, .alloc = undefined, .action = null, - .logging = .{ .stderr = {} }, + .logging = .{}, .rlimits = .{}, .resources_dir = .{}, }; @@ -100,12 +104,7 @@ pub const GlobalState = struct { // If we have an action executing, we disable logging by default // since we write to stderr we don't want logs messing up our // output. - if (self.action != null) self.logging = .{ .disabled = {} }; - - // For lib mode we always disable stderr logging by default. - if (comptime build_config.app_runtime == .none) { - self.logging = .{ .disabled = {} }; - } + if (self.action != null) self.logging.stderr = false; // I don't love the env var name but I don't have it in my heart // to parse CLI args 3 times (once for actions, once for config, @@ -114,9 +113,7 @@ pub const GlobalState = struct { // easy to set. if ((try internal_os.getenv(self.alloc, "GHOSTTY_LOG"))) |v| { defer v.deinit(self.alloc); - if (v.value.len > 0) { - self.logging = .{ .stderr = {} }; - } + self.logging = cli.args.parsePackedStruct(Logging, v.value) catch .{}; } // Setup our signal handlers before logging diff --git a/src/input.zig b/src/input.zig index be84a60d6..bad3ac1f3 100644 --- a/src/input.zig +++ b/src/input.zig @@ -4,6 +4,7 @@ const builtin = @import("builtin"); const config = @import("input/config.zig"); const mouse = @import("input/mouse.zig"); const key = @import("input/key.zig"); +const key_mods = @import("input/key_mods.zig"); const keyboard = @import("input/keyboard.zig"); pub const command = @import("input/command.zig"); @@ -21,8 +22,9 @@ pub const Link = @import("input/Link.zig"); pub const Key = key.Key; pub const KeyboardLayout = keyboard.Layout; pub const KeyEvent = key.KeyEvent; +pub const KeyRemapSet = key_mods.RemapSet; pub const InspectorMode = Binding.Action.InspectorMode; -pub const Mods = key.Mods; +pub const Mods = key_mods.Mods; pub const MouseButton = mouse.Button; pub const MouseButtonState = mouse.ButtonState; pub const MousePressureStage = mouse.PressureStage; diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 1e7db3592..57414d764 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -8,7 +8,9 @@ const assert = @import("../quirks.zig").inlineAssert; const build_config = @import("../build_config.zig"); const uucode = @import("uucode"); const EntryFormatter = @import("../config/formatter.zig").EntryFormatter; +const deepEqual = @import("../datastruct/comparison.zig").deepEqual; const key = @import("key.zig"); +const key_mods = @import("key_mods.zig"); const KeyEvent = key.KeyEvent; /// The trigger that needs to be performed to execute the action. @@ -44,6 +46,27 @@ pub const Flags = packed struct { /// performed. If the action can't be performed then the binding acts as /// if it doesn't exist. performable: bool = false, + + /// C type + pub const C = u8; + + /// Converts this to a C-compatible value. + /// + /// Sync with ghostty.h for enums. + pub fn cval(self: Flags) C { + const Backing = @typeInfo(Flags).@"struct".backing_integer.?; + return @as(Backing, @bitCast(self)); + } + + test "cval" { + const testing = std.testing; + try testing.expectEqual(@as(u8, 0b0001), (Flags{}).cval()); + try testing.expectEqual(@as(u8, 0b0000), (Flags{ .consumed = false }).cval()); + try testing.expectEqual(@as(u8, 0b0011), (Flags{ .all = true }).cval()); + try testing.expectEqual(@as(u8, 0b0101), (Flags{ .global = true }).cval()); + try testing.expectEqual(@as(u8, 0b1001), (Flags{ .performable = true }).cval()); + try testing.expectEqual(@as(u8, 0b1111), (Flags{ .consumed = true, .all = true, .global = true, .performable = true }).cval()); + } }; /// Full binding parser. The binding parser is implemented as an iterator @@ -52,6 +75,7 @@ pub const Parser = struct { trigger_it: SequenceIterator, action: Action, flags: Flags = .{}, + chain: bool, pub const Elem = union(enum) { /// A leader trigger in a sequence. @@ -59,6 +83,12 @@ pub const Parser = struct { /// The final trigger and action in a sequence. binding: Binding, + + /// A chained action `chain=` that should be appended + /// to the previous binding. Note that any action is parsed, including + /// invalid actions for chains such as `unbind`. We expect downstream + /// consumers to validate that the action is valid for chaining. + chain: Action, }; pub fn init(raw_input: []const u8) Error!Parser { @@ -95,12 +125,23 @@ pub const Parser = struct { return Error.InvalidFormat; }; + // Detect chains. Chains must not have flags. + const chain = std.mem.eql(u8, input[0..eql_idx], "chain"); + if (chain and start_idx > 0) return Error.InvalidFormat; + // Sequence iterator goes up to the equal, action is after. We can // parse the action now. return .{ - .trigger_it = .{ .input = input[0..eql_idx] }, + .trigger_it = .{ + // This is kind of hacky but we put a dummy trigger + // for chained inputs. The `next` will never yield this + // because we have chain set. When we find a nicer way to + // do this we can remove it, the e2e is tested. + .input = if (chain) "a" else input[0..eql_idx], + }, .action = try .parse(input[eql_idx + 1 ..]), .flags = flags, + .chain = chain, }; } @@ -156,6 +197,9 @@ pub const Parser = struct { return .{ .leader = trigger }; } + // If we're a chain then return it as-is. + if (self.chain) return .{ .chain = self.action }; + // Out of triggers, yield the final action. return .{ .binding = .{ .trigger = trigger, @@ -198,12 +242,19 @@ const SequenceIterator = struct { /// Parse a single, non-sequenced binding. To support sequences you must /// use parse. This is a convenience function for single bindings aimed /// primarily at tests. -fn parseSingle(raw_input: []const u8) (Error || error{UnexpectedSequence})!Binding { +/// +/// This doesn't support `chain` either, since chaining requires some +/// stateful concept of a prior binding. +fn parseSingle(raw_input: []const u8) (Error || error{ + UnexpectedChain, + UnexpectedSequence, +})!Binding { var p = try Parser.init(raw_input); const elem = (try p.next()) orelse return Error.InvalidFormat; return switch (elem) { .leader => error.UnexpectedSequence, .binding => elem.binding, + .chain => error.UnexpectedChain, }; } @@ -340,6 +391,11 @@ pub const Action = union(enum) { /// If a previous search is active, it is replaced. search: []const u8, + /// Start a search for the current text selection. If there is no + /// selection, this does nothing. If a search is already active, this + /// changes the search terms. + search_selection, + /// Navigate the search results. If there is no active search, this /// is not performed. navigate_search: NavigateSearch, @@ -519,6 +575,11 @@ pub const Action = union(enum) { /// version can be found by running `ghostty +version`. prompt_surface_title, + /// Change the title of the current tab/window via a pop-up prompt. The + /// title set via this prompt overrides any title set by the terminal + /// and persists across focus changes within the tab. + prompt_tab_title, + /// Create a new split in the specified direction. /// /// Valid arguments: @@ -540,6 +601,9 @@ pub const Action = union(enum) { /// (`previous` and `next`). goto_split: SplitFocusDirection, + /// Focus on either the previous window or the next one ('previous', 'next') + goto_window: GotoWindow, + /// Zoom in or out of the current split. /// /// When a split is zoomed into, it will take up the entire space in @@ -547,6 +611,16 @@ pub const Action = union(enum) { /// reflect this by displaying an icon indicating the zoomed state. toggle_split_zoom, + /// Toggle read-only mode for the current surface. + /// + /// When a surface is in read-only mode: + /// - No input is sent to the PTY (mouse events, key encoding) + /// - Input can still be used at the terminal level to make selections, + /// copy/paste (keybinds), scroll, etc. + /// - Warn before quit is always enabled in this state even if an active + /// process is not running + toggle_readonly, + /// Resize the current split in the specified direction and amount in /// pixels. The two arguments should be joined with a comma (`,`), /// like in `resize_split:up,10`. @@ -600,9 +674,8 @@ pub const Action = union(enum) { /// of the `confirm-close-surface` configuration setting. close_surface, - /// Close the current tab and all splits therein _or_ close all tabs and - /// splits thein of tabs _other_ than the current tab, depending on the - /// mode. + /// Close the current tab and all splits therein, close all other tabs, or + /// close every tab to the right of the current one depending on the mode. /// /// If the mode is not specified, defaults to closing the current tab. /// @@ -738,6 +811,16 @@ pub const Action = union(enum) { /// Only implemented on macOS. toggle_visibility, + /// Toggle the window background opacity between transparent and opaque. + /// + /// This does nothing when `background-opacity` is set to 1 or above. + /// + /// When `background-opacity` is less than 1, this action will either make + /// the window transparent or not depending on its current transparency state. + /// + /// Only implemented on macOS. + toggle_background_opacity, + /// Check for updates. /// /// Only implemented on macOS. @@ -772,6 +855,51 @@ pub const Action = union(enum) { /// be undone or redone. redo, + /// End the currently active key sequence, if any, and flush the + /// keys up to this point to the terminal, excluding the key that + /// triggered this action. + /// + /// For example: `ctrl+w>escape=end_key_sequence` would encode + /// `ctrl+w` to the terminal and exit the key sequence. + /// + /// Normally, an invalid sequence will reset the key sequence and + /// flush all data including the invalid key. This action allows + /// you to flush only the prior keys, which is useful when you want + /// to bind something like a control key (`ctrl+w`) but not send + /// additional inputs. + end_key_sequence, + + /// Activate a named key table (see `keybind` configuration documentation). + /// The named key table will remain active until `deactivate_key_table` + /// is called. If you want a one-shot key table activation, use the + /// `activate_key_table_once` action instead. + /// + /// If the named key table does not exist, this action has no effect + /// and performable will report false. + /// + /// If the named key table is already the currently active key table, + /// this action has no effect and performable will report false. + activate_key_table: []const u8, + + /// Same as activate_key_table, but the key table will only be active + /// until the first valid keybinding from that table is used (including + /// any defined `catch_all` bindings). + /// + /// The "once" check is only done if this is the currently active + /// key table. If another key table is activated later, then this + /// table will remain active until it pops back out to being the + /// active key table. + activate_key_table_once: []const u8, + + /// Deactivate the currently active key table, if any. The next most + /// recently activated key table (if any) will become active again. + /// If no key table is active, this action has no effect. + deactivate_key_table, + + /// Deactivate all active key tables. If no active key table exists, + /// this will report performable as false. + deactivate_all_key_tables, + /// Quit Ghostty. quit, @@ -917,6 +1045,11 @@ pub const Action = union(enum) { right, }; + pub const GotoWindow = enum { + previous, + next, + }; + pub const SplitResizeParameter = struct { SplitResizeDirection, u16, @@ -1005,6 +1138,7 @@ pub const Action = union(enum) { pub const CloseTabMode = enum { this, other, + right, pub const default: CloseTabMode = .this; }; @@ -1178,6 +1312,7 @@ pub const Action = union(enum) { .cursor_key, .search, .navigate_search, + .search_selection, .start_search, .end_search, .reset, @@ -1191,6 +1326,7 @@ pub const Action = union(enum) { .reset_font_size, .set_font_size, .prompt_surface_title, + .prompt_tab_title, .clear_screen, .select_all, .scroll_to_top, @@ -1216,8 +1352,14 @@ pub const Action = union(enum) { .toggle_secure_input, .toggle_mouse_reporting, .toggle_command_palette, + .toggle_background_opacity, .show_on_screen_keyboard, .reset_window_size, + .activate_key_table, + .activate_key_table_once, + .deactivate_key_table, + .deactivate_all_key_tables, + .end_key_sequence, .crash, => .surface, @@ -1234,7 +1376,9 @@ pub const Action = union(enum) { .toggle_tab_overview, .new_split, .goto_split, + .goto_window, .toggle_split_zoom, + .toggle_readonly, .resize_split, .equalize_splits, .inspector, @@ -1444,6 +1588,24 @@ pub const Action = union(enum) { }, } } + + /// Compares two actions for equality. + pub fn equal(self: Action, other: Action) bool { + if (std.meta.activeTag(self) != std.meta.activeTag(other)) return false; + return switch (self) { + inline else => |field_self, tag| { + const field_other = @field(other, @tagName(tag)); + return deepEqual( + @TypeOf(field_self), + field_self, + field_other, + ); + }, + }; + } + + /// For the Set.Context + const bindingSetEqual = equal; }; /// Trigger is the associated key state that can trigger an action. @@ -1468,6 +1630,10 @@ pub const Trigger = struct { /// codepoint. This is useful for binding to keys that don't have a /// registered keycode with Ghostty. unicode: u21, + + /// A catch-all key that matches any key press that is otherwise + /// unbound. + catch_all, }; /// The extern struct used for triggers in the C API. @@ -1479,6 +1645,7 @@ pub const Trigger = struct { pub const Tag = enum(c_int) { physical, unicode, + catch_all, }; pub const Key = extern union { @@ -1514,18 +1681,12 @@ pub const Trigger = struct { } // Alias modifiers - const alias_mods = .{ - .{ "cmd", "super" }, - .{ "command", "super" }, - .{ "opt", "alt" }, - .{ "option", "alt" }, - .{ "control", "ctrl" }, - }; - inline for (alias_mods) |pair| { + inline for (key_mods.alias) |pair| { if (std.mem.eql(u8, part, pair[0])) { // Repeat not allowed - if (@field(result.mods, pair[1])) return Error.InvalidFormat; - @field(result.mods, pair[1]) = true; + const field = @tagName(pair[1]); + if (@field(result.mods, field)) return Error.InvalidFormat; + @field(result.mods, field) = true; continue :loop; } } @@ -1574,6 +1735,13 @@ pub const Trigger = struct { continue :loop; } + // Check for catch_all. We do this near the end since its unlikely + // in most cases that we're setting a catch-all key. + if (std.mem.eql(u8, part, "catch_all")) { + result.key = .catch_all; + continue :loop; + } + // If we're still unset then we look for backwards compatible // keys with Ghostty 1.1.x. We do this last so its least likely // to impact performance for modern users. @@ -1714,7 +1882,7 @@ pub const Trigger = struct { pub fn isKeyUnset(self: Trigger) bool { return switch (self.key) { .physical => |v| v == .unidentified, - else => false, + .unicode, .catch_all => false, }; } @@ -1734,6 +1902,7 @@ pub const Trigger = struct { hasher, foldedCodepoint(cp), ), + .catch_all => {}, } std.hash.autoHash(hasher, self.mods.binding()); } @@ -1757,6 +1926,39 @@ pub const Trigger = struct { return array; } + /// Returns true if two triggers are equal. + pub fn equal(self: Trigger, other: Trigger) bool { + if (self.mods != other.mods) return false; + const self_tag = std.meta.activeTag(self.key); + const other_tag = std.meta.activeTag(other.key); + if (self_tag != other_tag) return false; + return switch (self.key) { + .physical => |v| v == other.key.physical, + .unicode => |v| v == other.key.unicode, + .catch_all => true, + }; + } + + /// Returns true if two triggers are equal using folded codepoints. + pub fn foldedEqual(self: Trigger, other: Trigger) bool { + if (self.mods != other.mods) return false; + const self_tag = std.meta.activeTag(self.key); + const other_tag = std.meta.activeTag(other.key); + if (self_tag != other_tag) return false; + return switch (self.key) { + .physical => |v| v == other.key.physical, + .unicode => |v| deepEqual( + [3]u21, + foldedCodepoint(v), + foldedCodepoint(other.key.unicode), + ), + .catch_all => true, + }; + } + + /// For the Set.Context + const bindingSetEqual = foldedEqual; + /// Convert the trigger to a C API compatible trigger. pub fn cval(self: Trigger) C { return .{ @@ -1764,6 +1966,9 @@ pub const Trigger = struct { .key = switch (self.key) { .physical => |v| .{ .physical = v }, .unicode => |v| .{ .unicode = @intCast(v) }, + // catch_all has no associated value so its an error + // for a C consumer to look at it. + .catch_all => undefined, }, .mods = self.mods, }; @@ -1784,6 +1989,7 @@ pub const Trigger = struct { switch (self.key) { .physical => |k| try writer.print("{t}", .{k}), .unicode => |c| try writer.print("{u}", .{c}), + .catch_all => try writer.writeAll("catch_all"), } } }; @@ -1792,18 +1998,18 @@ pub const Trigger = struct { /// The use case is that this will be called on EVERY key input to look /// for an associated action so it must be fast. pub const Set = struct { - const HashMap = std.HashMapUnmanaged( + const HashMap = std.ArrayHashMapUnmanaged( Trigger, Value, Context(Trigger), - std.hash_map.default_max_load_percentage, + true, ); - const ReverseMap = std.HashMapUnmanaged( + const ReverseMap = std.ArrayHashMapUnmanaged( Action, Trigger, Context(Action), - std.hash_map.default_max_load_percentage, + true, ); /// The set of bindings. @@ -1826,6 +2032,23 @@ pub const Set = struct { /// integration with GUI toolkits. reverse: ReverseMap = .{}, + /// The chain parent is the information necessary to attach a chained + /// action to the proper location in our mapping. It tracks both the + /// entry in the hashmap and the set it belongs to, which is needed + /// to properly update reverse mappings when converting a leaf to + /// a chained action. + chain_parent: ?ChainParent = null, + + /// Information about a chain parent entry, including which set it + /// belongs to. This is needed because reverse mappings are only + /// maintained in the root set, but the chain parent entry may be + /// in a nested set (for leader key sequences). + const ChainParent = struct { + key_ptr: *Trigger, + value_ptr: *Value, + set: *Set, + }; + /// The entry type for the forward mapping of trigger to action. pub const Value = union(enum) { /// This key is a leader key in a sequence. You must follow the given @@ -1836,6 +2059,9 @@ pub const Set = struct { /// to take along with the flags that may define binding behavior. leaf: Leaf, + /// A set of actions to take in response to a trigger. + leaf_chained: LeafChained, + /// Implements the formatter for the fmt package. This encodes the /// action back into the format used by parse. pub fn format( @@ -1870,7 +2096,7 @@ pub const Set = struct { /// /// `buffer_stream` is a FixedBufferStream used for temporary storage /// that is shared between calls to nested levels of the set. - /// For example, 'a>b>c=x' and 'a>b>d=y' will re-use the 'a>b' written + /// For example, 'a>b>c=x' and 'a>b>d=y' will reuse the 'a>b' written /// to the buffer before flushing it to the formatter with 'c=x' and 'd=y'. pub fn formatEntries( self: Value, @@ -1901,6 +2127,20 @@ pub const Set = struct { buffer.print("={f}", .{leaf.action}) catch return error.OutOfMemory; try formatter.formatEntry([]const u8, buffer.buffer[0..buffer.end]); }, + + .leaf_chained => |leaf| { + const pos = buffer.end; + for (leaf.actions.items, 0..) |action, i| { + if (i == 0) { + buffer.print("={f}", .{action}) catch return error.OutOfMemory; + } else { + buffer.end = 0; + buffer.print("chain={f}", .{action}) catch return error.OutOfMemory; + } + try formatter.formatEntry([]const u8, buffer.buffer[0..buffer.end]); + buffer.end = pos; + } + }, } } }; @@ -1927,6 +2167,62 @@ pub const Set = struct { std.hash.autoHash(&hasher, self.flags); return hasher.final(); } + + pub fn generic(self: *const Leaf) GenericLeaf { + return .{ + .flags = self.flags, + .actions = .{ .single = .{self.action} }, + }; + } + }; + + /// Leaf node of a set that triggers multiple actions in sequence. + pub const LeafChained = struct { + actions: std.ArrayList(Action), + flags: Flags, + + pub fn clone( + self: LeafChained, + alloc: Allocator, + ) Allocator.Error!LeafChained { + var cloned_actions = try self.actions.clone(alloc); + errdefer cloned_actions.deinit(alloc); + for (cloned_actions.items) |*action| { + action.* = try action.clone(alloc); + } + return .{ + .actions = cloned_actions, + .flags = self.flags, + }; + } + + pub fn deinit(self: *LeafChained, alloc: Allocator) void { + self.actions.deinit(alloc); + } + + pub fn generic(self: *const LeafChained) GenericLeaf { + return .{ + .flags = self.flags, + .actions = .{ .many = self.actions.items }, + }; + } + }; + + /// A generic leaf node that can be used to unify the handling of + /// leaf and leaf_chained. + pub const GenericLeaf = struct { + flags: Flags, + actions: union(enum) { + single: [1]Action, + many: []const Action, + }, + + pub fn actionsSlice(self: *const GenericLeaf) []const Action { + return switch (self.actions) { + .single => |*arr| arr, + .many => |slice| slice, + }; + } }; /// A full key-value entry for the set. @@ -1940,6 +2236,9 @@ pub const Set = struct { s.deinit(alloc); alloc.destroy(s); }, + + .leaf_chained => |*l| l.deinit(alloc), + .leaf => {}, }; @@ -1969,26 +2268,65 @@ pub const Set = struct { // We use recursion so that we can utilize the stack as our state // for cleanup. - self.parseAndPutRecurse(alloc, &it) catch |err| switch (err) { - // If this gets sent up to the root then we've unbound - // all the way up and this put was a success. - error.SequenceUnbind => {}, + const updated_set_ = self.parseAndPutRecurse( + self, + alloc, + &it, + ) catch |err| err: { + switch (err) { + // If this gets sent up to the root then we've unbound + // all the way up and this put was a success. + error.SequenceUnbind => break :err null, - // Unrecoverable - error.OutOfMemory => return error.OutOfMemory, + // If our parser input was too short then the format + // is invalid because we handle all valid cases. + error.UnexpectedEndOfInput => return error.InvalidFormat, + + // If we had a chain without a parent then the format is wrong. + error.NoChainParent => return error.InvalidFormat, + + // If we had an invalid action for a chain (e.g. unbind). + error.InvalidChainAction => return error.InvalidFormat, + + // Unrecoverable + error.OutOfMemory => return error.OutOfMemory, + } + + // Errors must never fall through. + unreachable; }; + + // If we have an updated set (a binding was added) then we store + // it for our chain parent. If we didn't update a set then we clear + // our chain parent since chaining is no longer valid until a + // valid binding is saved. + if (updated_set_) |updated_set| { + // A successful addition must have recorded a chain parent. + assert(updated_set.chain_parent != null); + if (updated_set != self) self.chain_parent = updated_set.chain_parent; + assert(self.chain_parent != null); + } else { + self.chain_parent = null; + } } const ParseAndPutRecurseError = Allocator.Error || error{ SequenceUnbind, + NoChainParent, + UnexpectedEndOfInput, + InvalidChainAction, }; + /// Returns the set that was ultimately updated if a binding was + /// added. Unbind does not return a set since nothing was added. fn parseAndPutRecurse( + root: *Set, set: *Set, alloc: Allocator, it: *Parser, - ) ParseAndPutRecurseError!void { - const elem = (it.next() catch unreachable) orelse return; + ) ParseAndPutRecurseError!?*Set { + const elem = (it.next() catch unreachable) orelse + return error.UnexpectedEndOfInput; switch (elem) { .leader => |t| { // If we have a leader, we need to upsert a set for it. @@ -2000,7 +2338,7 @@ pub const Set = struct { if (old) |entry| switch (entry) { // We have an existing leader for this key already // so recurse into this set. - .leader => |s| return parseAndPutRecurse( + .leader => |s| return root.parseAndPutRecurse( s, alloc, it, @@ -2011,12 +2349,16 @@ pub const Set = struct { error.SequenceUnbind => if (s.bindings.count() == 0) { set.remove(alloc, t); return error.SequenceUnbind; - }, + } else null, - error.OutOfMemory => return error.OutOfMemory, + error.NoChainParent, + error.UnexpectedEndOfInput, + error.InvalidChainAction, + error.OutOfMemory, + => err, }, - .leaf => { + .leaf, .leaf_chained => { // Remove the existing action. Fallthrough as if // we don't have a leader. set.remove(alloc, t); @@ -2033,7 +2375,7 @@ pub const Set = struct { try set.bindings.put(alloc, t, .{ .leader = next }); // Recurse - parseAndPutRecurse(next, alloc, it) catch |err| switch (err) { + return root.parseAndPutRecurse(next, alloc, it) catch |err| switch (err) { // If our action was to unbind, we restore the old // action if we have it. error.SequenceUnbind => { @@ -2046,10 +2388,35 @@ pub const Set = struct { leaf.action, leaf.flags, ) catch {}, + + .leaf_chained => |leaf| chain: { + // Rebuild our chain + set.putFlags( + alloc, + t, + leaf.actions.items[0], + leaf.flags, + ) catch break :chain; + for (leaf.actions.items[1..]) |action| { + set.appendChain( + alloc, + action, + ) catch { + set.remove(alloc, t); + break :chain; + }; + } + }, }; + + return null; }, - error.OutOfMemory => return error.OutOfMemory, + error.NoChainParent, + error.UnexpectedEndOfInput, + error.InvalidChainAction, + error.OutOfMemory, + => return err, }; }, @@ -2059,12 +2426,24 @@ pub const Set = struct { return error.SequenceUnbind; }, - else => try set.putFlags( - alloc, - b.trigger, - b.action, - b.flags, - ), + else => { + try set.putFlags( + alloc, + b.trigger, + b.action, + b.flags, + ); + return set; + }, + }, + + .chain => |action| { + // Chains can only happen on the root. + assert(set == root); + // Unbind is not valid for chains. + if (action == .unbind) return error.InvalidChainAction; + try set.appendChain(alloc, action); + return set; }, } } @@ -2096,7 +2475,22 @@ pub const Set = struct { // See the reverse map docs for more information. const track_reverse: bool = !flags.performable; + // No matter what our chained parent becomes invalid because + // getOrPut invalidates pointers. + self.chain_parent = null; + const gop = try self.bindings.getOrPut(alloc, t); + self.chain_parent = .{ + .key_ptr = gop.key_ptr, + .value_ptr = gop.value_ptr, + .set = self, + }; + errdefer { + // If we have any errors we can't trust our values here. And + // we can't restore the old values because they're also invalidated + // by getOrPut so we just disable chaining. + self.chain_parent = null; + } if (gop.found_existing) switch (gop.value_ptr.*) { // If we have a leader we need to clean up the memory @@ -2109,24 +2503,93 @@ pub const Set = struct { // update the reverse mapping to remove the old action. .leaf => if (track_reverse) { const t_hash = t.hash(); - var it = self.reverse.iterator(); - while (it.next()) |reverse_entry| it: { - if (t_hash == reverse_entry.value_ptr.hash()) { - self.reverse.removeByPtr(reverse_entry.key_ptr); - break :it; + for (0.., self.reverse.values()) |i, *value| { + if (t_hash == value.hash()) { + self.reverse.swapRemoveAt(i); + break; } } }, + + // Chained leaves aren't in the reverse mapping so we just + // clear it out. + .leaf_chained => |*l| { + l.deinit(alloc); + }, }; gop.value_ptr.* = .{ .leaf = .{ .action = action, .flags = flags, } }; - errdefer _ = self.bindings.remove(t); + errdefer _ = self.bindings.swapRemove(t); if (track_reverse) try self.reverse.put(alloc, action, t); errdefer if (track_reverse) self.reverse.remove(action); + + // Invariant: after successful put, chain_parent must be valid and point + // to the entry we just added/updated. + assert(self.chain_parent != null); + assert(self.chain_parent.?.key_ptr == gop.key_ptr); + assert(self.chain_parent.?.value_ptr == gop.value_ptr); + assert(self.chain_parent.?.value_ptr.* == .leaf); + } + + /// Append a chained action to the prior set action. + /// + /// It is an error if there is no valid prior chain parent. + pub fn appendChain( + self: *Set, + alloc: Allocator, + action: Action, + ) (Allocator.Error || error{NoChainParent})!void { + // Unbind is not a valid chain action; callers must check this. + assert(action != .unbind); + + const parent = self.chain_parent orelse return error.NoChainParent; + switch (parent.value_ptr.*) { + // Leader can never be a chain parent. Verified through various + // assertions and unit tests. + .leader => unreachable, + + // If it is already a chained action, we just append the + // action. Easy! + .leaf_chained => |*leaf| try leaf.actions.append( + alloc, + action, + ), + + // If it is a leaf, we need to convert it to a leaf_chained. + // We also need to be careful to remove any prior reverse + // mappings for this action since chained actions are not + // part of the reverse mapping. + .leaf => |leaf| { + // Setup our failable actions list first. + var actions: std.ArrayList(Action) = .empty; + try actions.ensureTotalCapacity(alloc, 2); + errdefer actions.deinit(alloc); + actions.appendAssumeCapacity(leaf.action); + actions.appendAssumeCapacity(action); + + // Convert to leaf_chained first, before fixing up reverse + // mapping. This is important because fixupReverseForAction + // searches for other bindings with the same action, and we + // don't want to find this entry (which is now chained). + parent.value_ptr.* = .{ .leaf_chained = .{ + .actions = actions, + .flags = leaf.flags, + } }; + + // Clean up our reverse mapping. Chained actions are not + // part of the reverse mapping, so we need to fix up the + // reverse map (possibly restoring another trigger for the + // same action). + parent.set.fixupReverseForAction( + leaf.action, + parent.key_ptr.*, + ); + }, + } } /// Get a binding for a given trigger. @@ -2176,6 +2639,14 @@ pub const Set = struct { if (self.get(trigger)) |v| return v; } + // Fallback to catch_all with modifiers first, then without modifiers. + trigger.key = .catch_all; + if (self.get(trigger)) |v| return v; + if (!trigger.mods.empty()) { + trigger.mods = .{}; + if (self.get(trigger)) |v| return v; + } + return null; } @@ -2185,8 +2656,13 @@ pub const Set = struct { } fn removeExact(self: *Set, alloc: Allocator, t: Trigger) void { - const entry = self.bindings.get(t) orelse return; - _ = self.bindings.remove(t); + // Removal always resets our chain parent. We could make this + // finer grained but the way it is documented is that chaining + // must happen directly after sets so this works. + self.chain_parent = null; + + var entry = self.bindings.get(t) orelse return; + _ = self.bindings.swapRemove(t); switch (entry) { // For a leader removal, we need to deallocate our child set. @@ -2198,32 +2674,67 @@ pub const Set = struct { }, // For an action we need to fix up the reverse mapping. - // Note: we'd LIKE to replace this with the most recent binding but - // our hash map obviously has no concept of ordering so we have to - // choose whatever. Maybe a switch to an array hash map here. - .leaf => |leaf| { - const action_hash = leaf.action.hash(); + .leaf => |leaf| self.fixupReverseForAction( + leaf.action, + t, + ), - var it = self.bindings.iterator(); - while (it.next()) |it_entry| { - switch (it_entry.value_ptr.*) { - .leader => {}, - .leaf => |leaf_search| { - if (leaf_search.action.hash() == action_hash) { - self.reverse.putAssumeCapacity(leaf.action, it_entry.key_ptr.*); - break; - } - }, - } - } else { - // No other trigger points to this action so we remove - // the reverse mapping completely. - _ = self.reverse.remove(leaf.action); - } + // Chained leaves are never in our reverse mapping so no + // cleanup is required. + .leaf_chained => |*l| { + l.deinit(alloc); }, } } + /// Fix up the reverse mapping after removing an action. + /// + /// When an action is removed from a binding (either by removal or by + /// converting to a chained action), we need to update the reverse mapping. + /// If another binding has the same action, we update the reverse mapping + /// to point to that binding. Otherwise, we remove the action from the + /// reverse mapping entirely. + /// + /// The `old` parameter is the trigger that was previously bound to this + /// action. It is used to check if the reverse mapping still points to + /// this trigger; if not, no fixup is needed since the reverse map already + /// points to a different trigger for this action. + /// + /// Note: we'd LIKE to replace this with the most recent binding but + /// our hash map obviously has no concept of ordering so we have to + /// choose whatever. Maybe a switch to an array hash map here. + fn fixupReverseForAction( + self: *Set, + action: Action, + old: Trigger, + ) void { + const entry = self.reverse.getEntry(action) orelse return; + + // If our value is not the same as the old trigger, we can + // ignore it because our reverse mapping points somewhere else. + if (!entry.value_ptr.equal(old)) return; + + // It is the same trigger, so let's now go through our bindings + // and try to find another trigger that maps to the same action. + const action_hash = action.hash(); + var it = self.bindings.iterator(); + while (it.next()) |it_entry| { + switch (it_entry.value_ptr.*) { + .leader, .leaf_chained => {}, + .leaf => |leaf_search| { + if (leaf_search.action.hash() == action_hash) { + entry.value_ptr.* = it_entry.key_ptr.*; + return; + } + }, + } + } + + // No other trigger points to this action so we remove + // the reverse mapping completely. + _ = self.reverse.swapRemove(action); + } + /// Deep clone the set. pub fn clone(self: *const Set, alloc: Allocator) !Set { var result: Set = .{ @@ -2239,6 +2750,8 @@ pub const Set = struct { // contain allocated strings). .leaf => |*s| s.* = try s.clone(alloc), + .leaf_chained => |*s| s.* = try s.clone(alloc), + // Must be deep cloned. .leader => |*s| { const ptr = try alloc.create(Set); @@ -2252,9 +2765,8 @@ pub const Set = struct { // We need to clone the action keys in the reverse map since // they may contain allocated values. - { - var it = result.reverse.keyIterator(); - while (it.next()) |action| action.* = try action.clone(alloc); + for (result.reverse.keys()) |*action| { + action.* = try action.clone(alloc); } return result; @@ -2264,13 +2776,23 @@ pub const Set = struct { /// gets the hash key and checks for equality. fn Context(comptime KeyType: type) type { return struct { - pub fn hash(ctx: @This(), k: KeyType) u64 { + pub fn hash(ctx: @This(), k: KeyType) u32 { _ = ctx; - return k.hash(); + // This seems crazy at first glance but this is also how + // the Zig standard library handles hashing for array + // hash maps! + return @truncate(k.hash()); } - pub fn eql(ctx: @This(), a: KeyType, b: KeyType) bool { - return ctx.hash(a) == ctx.hash(b); + pub fn eql( + ctx: @This(), + a: KeyType, + b: KeyType, + b_index: usize, + ) bool { + _ = ctx; + _ = b_index; + return a.bindingSetEqual(b); } }; } @@ -2396,6 +2918,31 @@ test "parse: w3c key names" { try testing.expectError(Error.InvalidFormat, parseSingle("Keya=ignore")); } +test "parse: catch_all" { + const testing = std.testing; + + // Basic catch_all + try testing.expectEqual( + Binding{ + .trigger = .{ .key = .catch_all }, + .action = .{ .ignore = {} }, + }, + try parseSingle("catch_all=ignore"), + ); + + // catch_all with modifiers + try testing.expectEqual( + Binding{ + .trigger = .{ + .mods = .{ .ctrl = true }, + .key = .catch_all, + }, + .action = .{ .ignore = {} }, + }, + try parseSingle("ctrl+catch_all=ignore"), + ); +} + test "parse: plus sign" { const testing = std.testing; @@ -2610,6 +3157,66 @@ test "parse: all triggers" { } } +test "Trigger: equal" { + const testing = std.testing; + + // Equal physical keys + { + const t1: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true } }; + const t2: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true } }; + try testing.expect(t1.equal(t2)); + } + + // Different physical keys + { + const t1: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true } }; + const t2: Trigger = .{ .key = .{ .physical = .arrow_down }, .mods = .{ .ctrl = true } }; + try testing.expect(!t1.equal(t2)); + } + + // Different mods + { + const t1: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true } }; + const t2: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .shift = true } }; + try testing.expect(!t1.equal(t2)); + } + + // Equal unicode keys + { + const t1: Trigger = .{ .key = .{ .unicode = 'a' }, .mods = .{} }; + const t2: Trigger = .{ .key = .{ .unicode = 'a' }, .mods = .{} }; + try testing.expect(t1.equal(t2)); + } + + // Different unicode keys + { + const t1: Trigger = .{ .key = .{ .unicode = 'a' }, .mods = .{} }; + const t2: Trigger = .{ .key = .{ .unicode = 'b' }, .mods = .{} }; + try testing.expect(!t1.equal(t2)); + } + + // Different key types + { + const t1: Trigger = .{ .key = .{ .unicode = 'a' }, .mods = .{} }; + const t2: Trigger = .{ .key = .{ .physical = .key_a }, .mods = .{} }; + try testing.expect(!t1.equal(t2)); + } + + // catch_all + { + const t1: Trigger = .{ .key = .catch_all, .mods = .{} }; + const t2: Trigger = .{ .key = .catch_all, .mods = .{} }; + try testing.expect(t1.equal(t2)); + } + + // catch_all with different mods + { + const t1: Trigger = .{ .key = .catch_all, .mods = .{} }; + const t2: Trigger = .{ .key = .catch_all, .mods = .{ .alt = true } }; + try testing.expect(!t1.equal(t2)); + } +} + test "parse: modifier aliases" { const testing = std.testing; @@ -2765,6 +3372,29 @@ test "parse: action with a tuple" { try testing.expectError(Error.InvalidFormat, parseSingle("a=resize_split:up,four")); } +test "parse: chain" { + const testing = std.testing; + + // Valid + { + var p = try Parser.init("chain=new_tab"); + try testing.expectEqual(Parser.Elem{ + .chain = .new_tab, + }, try p.next()); + try testing.expect(try p.next() == null); + } + + // Chain can't have flags + try testing.expectError(error.InvalidFormat, Parser.init("global:chain=ignore")); + + // Chain can't be part of a sequence + { + var p = try Parser.init("a>chain=ignore"); + _ = try p.next(); + try testing.expectError(error.InvalidFormat, p.next()); + } +} + test "sequence iterator" { const testing = std.testing; @@ -2856,6 +3486,15 @@ test "set: parseAndPut typical binding" { const trigger = s.getTrigger(.{ .new_window = {} }).?; try testing.expect(trigger.key.unicode == 'a'); } + + // Sets up the chain parent properly + try testing.expect(s.chain_parent != null); + { + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try s.chain_parent.?.key_ptr.format(&buf.writer); + try testing.expectEqualStrings("a", buf.written()); + } } test "set: parseAndPut unconsumed binding" { @@ -2880,6 +3519,15 @@ test "set: parseAndPut unconsumed binding" { const trigger = s.getTrigger(.{ .new_window = {} }).?; try testing.expect(trigger.key.unicode == 'a'); } + + // Sets up the chain parent properly + try testing.expect(s.chain_parent != null); + { + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try s.chain_parent.?.key_ptr.format(&buf.writer); + try testing.expectEqualStrings("a", buf.written()); + } } test "set: parseAndPut removed binding" { @@ -2898,6 +3546,264 @@ test "set: parseAndPut removed binding" { try testing.expect(s.get(trigger) == null); } try testing.expect(s.getTrigger(.{ .new_window = {} }) == null); + + // Sets up the chain parent properly + try testing.expect(s.chain_parent == null); +} + +test "set: put sets chain_parent" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); + + // chain_parent should be set + try testing.expect(s.chain_parent != null); + { + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try s.chain_parent.?.key_ptr.format(&buf.writer); + try testing.expectEqualStrings("a", buf.written()); + } + + // chain_parent value should be a leaf + try testing.expect(s.chain_parent.?.value_ptr.* == .leaf); +} + +test "set: putFlags sets chain_parent" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.putFlags( + alloc, + .{ .key = .{ .unicode = 'a' } }, + .{ .new_window = {} }, + .{ .consumed = false }, + ); + + // chain_parent should be set + try testing.expect(s.chain_parent != null); + { + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try s.chain_parent.?.key_ptr.format(&buf.writer); + try testing.expectEqualStrings("a", buf.written()); + } + + // chain_parent value should be a leaf with correct flags + try testing.expect(s.chain_parent.?.value_ptr.* == .leaf); + try testing.expect(!s.chain_parent.?.value_ptr.*.leaf.flags.consumed); +} + +test "set: sequence sets chain_parent to final leaf" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a>b=new_window"); + + // chain_parent should be set and point to 'b' (the final leaf) + try testing.expect(s.chain_parent != null); + { + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try s.chain_parent.?.key_ptr.format(&buf.writer); + try testing.expectEqualStrings("b", buf.written()); + } + + // chain_parent value should be a leaf + try testing.expect(s.chain_parent.?.value_ptr.* == .leaf); + try testing.expect(s.chain_parent.?.value_ptr.*.leaf.action == .new_window); +} + +test "set: multiple leaves under leader updates chain_parent" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a>b=new_window"); + + // After first binding, chain_parent should be 'b' + try testing.expect(s.chain_parent != null); + { + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try s.chain_parent.?.key_ptr.format(&buf.writer); + try testing.expectEqualStrings("b", buf.written()); + } + + try s.parseAndPut(alloc, "a>c=new_tab"); + + // After second binding, chain_parent should be updated to 'c' + try testing.expect(s.chain_parent != null); + { + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try s.chain_parent.?.key_ptr.format(&buf.writer); + try testing.expectEqualStrings("c", buf.written()); + } + try testing.expect(s.chain_parent.?.value_ptr.*.leaf.action == .new_tab); +} + +test "set: sequence unbind clears chain_parent" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a>b=new_window"); + try testing.expect(s.chain_parent != null); + + try s.parseAndPut(alloc, "a>b=unbind"); + + // After unbind, chain_parent should be cleared + try testing.expect(s.chain_parent == null); +} + +test "set: sequence unbind with remaining leaves clears chain_parent" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a>b=new_window"); + try s.parseAndPut(alloc, "a>c=new_tab"); + try s.parseAndPut(alloc, "a>b=unbind"); + + // After unbind, chain_parent should be cleared even though 'c' remains + try testing.expect(s.chain_parent == null); + + // But 'c' should still exist + const a_entry = s.get(.{ .key = .{ .unicode = 'a' } }).?; + try testing.expect(a_entry.value_ptr.* == .leader); + const inner_set = a_entry.value_ptr.*.leader; + try testing.expect(inner_set.get(.{ .key = .{ .unicode = 'c' } }) != null); +} + +test "set: direct remove clears chain_parent" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); + try testing.expect(s.chain_parent != null); + + s.remove(alloc, .{ .key = .{ .unicode = 'a' } }); + + // After removal, chain_parent should be cleared + try testing.expect(s.chain_parent == null); +} + +test "set: invalid format preserves chain_parent" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a=new_window"); + const before_key = s.chain_parent.?.key_ptr; + const before_value = s.chain_parent.?.value_ptr; + + // Try an invalid parse - should fail + try testing.expectError(error.InvalidAction, s.parseAndPut(alloc, "a=invalid_action_xyz")); + + // chain_parent should be unchanged + try testing.expect(s.chain_parent != null); + try testing.expect(s.chain_parent.?.key_ptr == before_key); + try testing.expect(s.chain_parent.?.value_ptr == before_value); +} + +test "set: clone produces null chain_parent" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a=new_window"); + try testing.expect(s.chain_parent != null); + + var cloned = try s.clone(alloc); + defer cloned.deinit(alloc); + + // Clone should have null chain_parent + try testing.expect(cloned.chain_parent == null); + + // But should have the binding + try testing.expect(cloned.get(.{ .key = .{ .unicode = 'a' } }) != null); +} + +test "set: clone with leaf_chained" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + // Create a chained binding using parseAndPut with chain= + try s.parseAndPut(alloc, "a=new_window"); + try s.parseAndPut(alloc, "chain=new_tab"); + + // Verify we have a leaf_chained + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?; + try testing.expect(entry.value_ptr.* == .leaf_chained); + try testing.expectEqual(@as(usize, 2), entry.value_ptr.leaf_chained.actions.items.len); + + // Clone the set + var cloned = try s.clone(alloc); + defer cloned.deinit(alloc); + + // Verify the cloned set has the leaf_chained with same actions + const cloned_entry = cloned.get(.{ .key = .{ .unicode = 'a' } }).?; + try testing.expect(cloned_entry.value_ptr.* == .leaf_chained); + try testing.expectEqual(@as(usize, 2), cloned_entry.value_ptr.leaf_chained.actions.items.len); + try testing.expect(cloned_entry.value_ptr.leaf_chained.actions.items[0] == .new_window); + try testing.expect(cloned_entry.value_ptr.leaf_chained.actions.items[1] == .new_tab); +} + +test "set: clone with leaf_chained containing allocated data" { + const testing = std.testing; + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var s: Set = .{}; + + // Create a chained binding with text actions (which have allocated strings) + try s.parseAndPut(alloc, "a=text:hello"); + try s.parseAndPut(alloc, "chain=text:world"); + + // Verify we have a leaf_chained + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?; + try testing.expect(entry.value_ptr.* == .leaf_chained); + + // Clone the set + const cloned = try s.clone(alloc); + + // Verify the cloned set has independent copies of the text + const cloned_entry = cloned.get(.{ .key = .{ .unicode = 'a' } }).?; + try testing.expect(cloned_entry.value_ptr.* == .leaf_chained); + try testing.expectEqualStrings("hello", cloned_entry.value_ptr.leaf_chained.actions.items[0].text); + try testing.expectEqualStrings("world", cloned_entry.value_ptr.leaf_chained.actions.items[1].text); + + // Verify the pointers are different (truly cloned, not shared) + try testing.expect(entry.value_ptr.leaf_chained.actions.items[0].text.ptr != + cloned_entry.value_ptr.leaf_chained.actions.items[0].text.ptr); } test "set: parseAndPut sequence" { @@ -3181,6 +4087,143 @@ test "set: consumed state" { try testing.expect(s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*.leaf.flags.consumed); } +test "set: parseAndPut chain" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a=new_window"); + try s.parseAndPut(alloc, "chain=new_tab"); + + // Creates forward mapping as leaf_chained + { + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*; + try testing.expect(entry == .leaf_chained); + const chained = entry.leaf_chained; + try testing.expectEqual(@as(usize, 2), chained.actions.items.len); + try testing.expect(chained.actions.items[0] == .new_window); + try testing.expect(chained.actions.items[1] == .new_tab); + } + + // Does not create reverse mapping, because reverse mappings are only for + // non-chained actions. + { + try testing.expect(s.getTrigger(.{ .new_window = {} }) == null); + } +} + +test "set: parseAndPut chain without parent is error" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + // Chain without a prior binding should fail + try testing.expectError(error.InvalidFormat, s.parseAndPut(alloc, "chain=new_tab")); +} + +test "set: parseAndPut chain multiple times" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a=new_window"); + try s.parseAndPut(alloc, "chain=new_tab"); + try s.parseAndPut(alloc, "chain=close_surface"); + + // Should have 3 actions chained + { + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*; + try testing.expect(entry == .leaf_chained); + const chained = entry.leaf_chained; + try testing.expectEqual(@as(usize, 3), chained.actions.items.len); + try testing.expect(chained.actions.items[0] == .new_window); + try testing.expect(chained.actions.items[1] == .new_tab); + try testing.expect(chained.actions.items[2] == .close_surface); + } +} + +test "set: parseAndPut chain preserves flags" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "unconsumed:a=new_window"); + try s.parseAndPut(alloc, "chain=new_tab"); + + // Should preserve unconsumed flag + { + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*; + try testing.expect(entry == .leaf_chained); + const chained = entry.leaf_chained; + try testing.expect(!chained.flags.consumed); + try testing.expectEqual(@as(usize, 2), chained.actions.items.len); + } +} + +test "set: parseAndPut chain after unbind is error" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a=new_window"); + try s.parseAndPut(alloc, "a=unbind"); + + // Chain after unbind should fail because chain_parent is cleared + try testing.expectError(error.InvalidFormat, s.parseAndPut(alloc, "chain=new_tab")); +} + +test "set: parseAndPut chain on sequence" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a>b=new_window"); + try s.parseAndPut(alloc, "chain=new_tab"); + + // Navigate to the inner set + const a_entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*; + try testing.expect(a_entry == .leader); + const inner_set = a_entry.leader; + + // Check the chained binding + const b_entry = inner_set.get(.{ .key = .{ .unicode = 'b' } }).?.value_ptr.*; + try testing.expect(b_entry == .leaf_chained); + const chained = b_entry.leaf_chained; + try testing.expectEqual(@as(usize, 2), chained.actions.items.len); + try testing.expect(chained.actions.items[0] == .new_window); + try testing.expect(chained.actions.items[1] == .new_tab); +} + +test "set: parseAndPut chain with unbind is error" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a=new_window"); + + // chain=unbind is not valid + try testing.expectError(error.InvalidFormat, s.parseAndPut(alloc, "chain=unbind")); + + // Original binding should still exist + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*; + try testing.expect(entry == .leaf); + try testing.expect(entry.leaf.action == .new_window); +} + test "set: getEvent physical" { const testing = std.testing; const alloc = testing.allocator; @@ -3292,6 +4335,83 @@ test "set: getEvent codepoint case folding" { } } +test "set: getEvent catch_all fallback" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "catch_all=ignore"); + + // Matches unbound key without modifiers + { + const action = s.getEvent(.{ + .key = .key_a, + .mods = .{}, + }).?.value_ptr.*.leaf; + try testing.expect(action.action == .ignore); + } + + // Matches unbound key with modifiers (falls back to catch_all without mods) + { + const action = s.getEvent(.{ + .key = .key_a, + .mods = .{ .ctrl = true }, + }).?.value_ptr.*.leaf; + try testing.expect(action.action == .ignore); + } + + // Specific binding takes precedence over catch_all + try s.parseAndPut(alloc, "ctrl+b=new_window"); + { + const action = s.getEvent(.{ + .key = .key_b, + .mods = .{ .ctrl = true }, + .unshifted_codepoint = 'b', + }).?.value_ptr.*.leaf; + try testing.expect(action.action == .new_window); + } +} + +test "set: getEvent catch_all with modifiers" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "ctrl+catch_all=close_surface"); + try s.parseAndPut(alloc, "catch_all=ignore"); + + // Key with ctrl matches catch_all with ctrl modifier + { + const action = s.getEvent(.{ + .key = .key_a, + .mods = .{ .ctrl = true }, + }).?.value_ptr.*.leaf; + try testing.expect(action.action == .close_surface); + } + + // Key without mods matches catch_all without mods + { + const action = s.getEvent(.{ + .key = .key_a, + .mods = .{}, + }).?.value_ptr.*.leaf; + try testing.expect(action.action == .ignore); + } + + // Key with different mods falls back to catch_all without mods + { + const action = s.getEvent(.{ + .key = .key_a, + .mods = .{ .alt = true }, + }).?.value_ptr.*.leaf; + try testing.expect(action.action == .ignore); + } +} + test "Action: clone" { const testing = std.testing; var arena = std.heap.ArenaAllocator.init(testing.allocator); @@ -3439,3 +4559,258 @@ test "action: format" { try a.format(&buf.writer); try testing.expectEqualStrings("text:\\xf0\\x9f\\x91\\xbb", buf.written()); } + +test "set: appendChain with no parent returns error" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try testing.expectError(error.NoChainParent, s.appendChain(alloc, .{ .new_tab = {} })); +} + +test "set: appendChain after put converts to leaf_chained" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); + + // First appendChain converts leaf to leaf_chained and appends the new action + try s.appendChain(alloc, .{ .new_tab = {} }); + + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?; + try testing.expect(entry.value_ptr.* == .leaf_chained); + + const chained = entry.value_ptr.*.leaf_chained; + try testing.expectEqual(@as(usize, 2), chained.actions.items.len); + try testing.expect(chained.actions.items[0] == .new_window); + try testing.expect(chained.actions.items[1] == .new_tab); +} + +test "set: appendChain after putFlags preserves flags" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.putFlags( + alloc, + .{ .key = .{ .unicode = 'a' } }, + .{ .new_window = {} }, + .{ .consumed = false }, + ); + try s.appendChain(alloc, .{ .new_tab = {} }); + + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?; + try testing.expect(entry.value_ptr.* == .leaf_chained); + + const chained = entry.value_ptr.*.leaf_chained; + try testing.expect(!chained.flags.consumed); + try testing.expectEqual(@as(usize, 2), chained.actions.items.len); + try testing.expect(chained.actions.items[0] == .new_window); + try testing.expect(chained.actions.items[1] == .new_tab); +} + +test "set: appendChain multiple times" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); + try s.appendChain(alloc, .{ .new_tab = {} }); + try s.appendChain(alloc, .{ .close_surface = {} }); + + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?; + try testing.expect(entry.value_ptr.* == .leaf_chained); + + const chained = entry.value_ptr.*.leaf_chained; + try testing.expectEqual(@as(usize, 3), chained.actions.items.len); + try testing.expect(chained.actions.items[0] == .new_window); + try testing.expect(chained.actions.items[1] == .new_tab); + try testing.expect(chained.actions.items[2] == .close_surface); +} + +test "set: appendChain removes reverse mapping" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); + + // Verify reverse mapping exists before chaining + try testing.expect(s.getTrigger(.{ .new_window = {} }) != null); + + // Chaining should remove the reverse mapping + try s.appendChain(alloc, .{ .new_tab = {} }); + + // Reverse mapping should be gone since chained actions are not in reverse map + try testing.expect(s.getTrigger(.{ .new_window = {} }) == null); +} + +test "set: appendChain with performable does not affect reverse mapping" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + // Add a non-performable binding first + try s.put(alloc, .{ .key = .{ .unicode = 'b' } }, .{ .new_window = {} }); + try testing.expect(s.getTrigger(.{ .new_window = {} }) != null); + + // Add a performable binding (not in reverse map) and chain it + try s.putFlags( + alloc, + .{ .key = .{ .unicode = 'a' } }, + .{ .close_surface = {} }, + .{ .performable = true }, + ); + + // close_surface was performable, so not in reverse map + try testing.expect(s.getTrigger(.{ .close_surface = {} }) == null); + + // Chaining the performable binding should not crash or affect anything + try s.appendChain(alloc, .{ .new_tab = {} }); + + // The non-performable new_window binding should still be in reverse map + try testing.expect(s.getTrigger(.{ .new_window = {} }) != null); +} + +test "set: appendChain restores next valid reverse mapping" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + // Add two bindings for the same action + try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); + try s.put(alloc, .{ .key = .{ .unicode = 'b' } }, .{ .new_window = {} }); + + // Reverse mapping should point to 'b' (most recent) + { + const trigger = s.getTrigger(.{ .new_window = {} }).?; + try testing.expect(trigger.key.unicode == 'b'); + } + + // Chain an action to 'b', which should restore 'a' in the reverse map + try s.appendChain(alloc, .{ .new_tab = {} }); + + // Now reverse mapping should point to 'a' + { + const trigger = s.getTrigger(.{ .new_window = {} }).?; + try testing.expect(trigger.key.unicode == 'a'); + } +} + +test "set: formatEntries leaf_chained" { + const testing = std.testing; + const alloc = testing.allocator; + const formatterpkg = @import("../config/formatter.zig"); + + var s: Set = .{}; + defer s.deinit(alloc); + + // Create a chained binding + try s.parseAndPut(alloc, "a=new_window"); + try s.parseAndPut(alloc, "chain=new_tab"); + + // Verify it's a leaf_chained + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?; + try testing.expect(entry.value_ptr.* == .leaf_chained); + + // Format the entries + var output: std.Io.Writer.Allocating = .init(alloc); + defer output.deinit(); + + var buf: [1024]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + + // Write the trigger first (as formatEntry in Config.zig does) + try entry.key_ptr.format(&writer); + try entry.value_ptr.formatEntries(&writer, formatterpkg.entryFormatter("keybind", &output.writer)); + + const expected = + \\keybind = a=new_window + \\keybind = chain=new_tab + \\ + ; + try testing.expectEqualStrings(expected, output.written()); +} + +test "set: formatEntries leaf_chained multiple chains" { + const testing = std.testing; + const alloc = testing.allocator; + const formatterpkg = @import("../config/formatter.zig"); + + var s: Set = .{}; + defer s.deinit(alloc); + + // Create a chained binding with 3 actions + try s.parseAndPut(alloc, "ctrl+a=new_window"); + try s.parseAndPut(alloc, "chain=new_tab"); + try s.parseAndPut(alloc, "chain=close_surface"); + + // Verify it's a leaf_chained with 3 actions + const entry = s.get(.{ .key = .{ .unicode = 'a' }, .mods = .{ .ctrl = true } }).?; + try testing.expect(entry.value_ptr.* == .leaf_chained); + try testing.expectEqual(@as(usize, 3), entry.value_ptr.leaf_chained.actions.items.len); + + // Format the entries + var output: std.Io.Writer.Allocating = .init(alloc); + defer output.deinit(); + + var buf: [1024]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + + try entry.key_ptr.format(&writer); + try entry.value_ptr.formatEntries(&writer, formatterpkg.entryFormatter("keybind", &output.writer)); + + const expected = + \\keybind = ctrl+a=new_window + \\keybind = chain=new_tab + \\keybind = chain=close_surface + \\ + ; + try testing.expectEqualStrings(expected, output.written()); +} + +test "set: formatEntries leaf_chained with text action" { + const testing = std.testing; + const alloc = testing.allocator; + const formatterpkg = @import("../config/formatter.zig"); + + var s: Set = .{}; + defer s.deinit(alloc); + + // Create a chained binding with text actions + try s.parseAndPut(alloc, "a=text:hello"); + try s.parseAndPut(alloc, "chain=text:world"); + + // Format the entries + var output: std.Io.Writer.Allocating = .init(alloc); + defer output.deinit(); + + var buf: [1024]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?; + try entry.key_ptr.format(&writer); + try entry.value_ptr.formatEntries(&writer, formatterpkg.entryFormatter("keybind", &output.writer)); + + const expected = + \\keybind = a=text:hello + \\keybind = chain=text:world + \\ + ; + try testing.expectEqualStrings(expected, output.written()); +} diff --git a/src/input/command.zig b/src/input/command.zig index 72fb7f4ee..d6d2b0247 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -43,7 +43,7 @@ pub const Command = struct { return true; } - /// Convert this command to a C struct. + /// Convert this command to a C struct at comptime. pub fn comptimeCval(self: Command) C { assert(@inComptime()); @@ -55,6 +55,27 @@ pub const Command = struct { }; } + /// Convert this command to a C struct at runtime. + /// + /// This shares memory with the original command. + /// + /// The action string is allocated using the provided allocator. You can + /// free the slice directly if you need to but we recommend an arena + /// for this. + pub fn cval(self: Command, alloc: Allocator) Allocator.Error!C { + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + self.action.format(&buf.writer) catch return error.OutOfMemory; + const action = try buf.toOwnedSliceSentinel(0); + + return .{ + .action_key = @tagName(self.action), + .action = action.ptr, + .title = self.title, + .description = self.description, + }; + } + /// Implements a comparison function for std.mem.sortUnstable /// and similar functions. The sorting is defined by Ghostty /// to be what we prefer. If a caller wants some other sorting, @@ -168,6 +189,12 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Start a search if one isn't already active.", }}, + .search_selection => comptime &.{.{ + .action = .search_selection, + .title = "Search Selection", + .description = "Start a search for the current text selection.", + }}, + .end_search => comptime &.{.{ .action = .end_search, .title = "End Search", @@ -413,10 +440,16 @@ fn actionCommands(action: Action.Key) []const Command { .prompt_surface_title => comptime &.{.{ .action = .prompt_surface_title, - .title = "Change Title...", + .title = "Change Terminal Title...", .description = "Prompt for a new title for the current terminal.", }}, + .prompt_tab_title => comptime &.{.{ + .action = .prompt_tab_title, + .title = "Change Tab Title...", + .description = "Prompt for a new title for the current tab.", + }}, + .new_split => comptime &.{ .{ .action = .{ .new_split = .left }, @@ -473,12 +506,31 @@ fn actionCommands(action: Action.Key) []const Command { }, }, + .goto_window => comptime &.{ + .{ + .action = .{ .goto_window = .previous }, + .title = "Focus Window: Previous", + .description = "Focus the previous window, if any.", + }, + .{ + .action = .{ .goto_window = .next }, + .title = "Focus Window: Next", + .description = "Focus the next window, if any.", + }, + }, + .toggle_split_zoom => comptime &.{.{ .action = .toggle_split_zoom, .title = "Toggle Split Zoom", .description = "Toggle the zoom state of the current split.", }}, + .toggle_readonly => comptime &.{.{ + .action = .toggle_readonly, + .title = "Toggle Read-Only Mode", + .description = "Toggle read-only mode for the current surface.", + }}, + .equalize_splits => comptime &.{.{ .action = .equalize_splits, .title = "Equalize Splits", @@ -538,6 +590,11 @@ fn actionCommands(action: Action.Key) []const Command { .title = "Close Other Tabs", .description = "Close all tabs in this window except the current one.", }, + .{ + .action = .{ .close_tab = .right }, + .title = "Close Tabs to the Right", + .description = "Close all tabs to the right of the current one.", + }, }, .close_window => comptime &.{.{ @@ -588,6 +645,12 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Toggle whether mouse events are reported to terminal applications.", }}, + .toggle_background_opacity => comptime &.{.{ + .action = .toggle_background_opacity, + .title = "Toggle Background Opacity", + .description = "Toggle the background opacity of a window that started transparent.", + }}, + .check_for_updates => comptime &.{.{ .action = .check_for_updates, .title = "Check for Updates", @@ -635,6 +698,11 @@ fn actionCommands(action: Action.Key) []const Command { .write_scrollback_file, .goto_tab, .resize_split, + .activate_key_table, + .activate_key_table_once, + .deactivate_key_table, + .deactivate_all_key_tables, + .end_key_sequence, .crash, => comptime &.{}, diff --git a/src/input/key.zig b/src/input/key.zig index 54c7491ae..a929a0323 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -1,9 +1,11 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; -const cimgui = @import("cimgui"); +const cimgui = @import("dcimgui"); const OptionAsAlt = @import("config.zig").OptionAsAlt; +pub const Mods = @import("key_mods.zig").Mods; + /// A generic key input event. This is the information that is necessary /// regardless of apprt in order to generate the proper terminal /// control sequences for a given key press. @@ -76,161 +78,6 @@ pub const KeyEvent = struct { } }; -/// A bitmask for all key modifiers. -/// -/// IMPORTANT: Any changes here update include/ghostty.h -pub const Mods = packed struct(Mods.Backing) { - pub const Backing = u16; - - shift: bool = false, - ctrl: bool = false, - alt: bool = false, - super: bool = false, - caps_lock: bool = false, - num_lock: bool = false, - sides: side = .{}, - _padding: u6 = 0, - - /// Tracks the side that is active for any given modifier. Note - /// that this doesn't confirm a modifier is pressed; you must check - /// the bool for that in addition to this. - /// - /// Not all platforms support this, check apprt for more info. - pub const side = packed struct(u4) { - shift: Side = .left, - ctrl: Side = .left, - alt: Side = .left, - super: Side = .left, - }; - - pub const Side = enum(u1) { left, right }; - - /// Integer value of this struct. - pub fn int(self: Mods) Backing { - return @bitCast(self); - } - - /// Returns true if no modifiers are set. - pub fn empty(self: Mods) bool { - return self.int() == 0; - } - - /// Returns true if two mods are equal. - pub fn equal(self: Mods, other: Mods) bool { - return self.int() == other.int(); - } - - /// Return mods that are only relevant for bindings. - pub fn binding(self: Mods) Mods { - return .{ - .shift = self.shift, - .ctrl = self.ctrl, - .alt = self.alt, - .super = self.super, - }; - } - - /// Perform `self &~ other` to remove the other mods from self. - pub fn unset(self: Mods, other: Mods) Mods { - return @bitCast(self.int() & ~other.int()); - } - - /// Returns the mods without locks set. - pub fn withoutLocks(self: Mods) Mods { - var copy = self; - copy.caps_lock = false; - copy.num_lock = false; - return copy; - } - - /// Return the mods to use for key translation. This handles settings - /// like macos-option-as-alt. The translation mods should be used for - /// translation but never sent back in for the key callback. - pub fn translation(self: Mods, option_as_alt: OptionAsAlt) Mods { - var result = self; - - // macos-option-as-alt for darwin - if (comptime builtin.target.os.tag.isDarwin()) alt: { - // Alt has to be set only on the correct side - switch (option_as_alt) { - .false => break :alt, - .true => {}, - .left => if (self.sides.alt == .right) break :alt, - .right => if (self.sides.alt == .left) break :alt, - } - - // Unset alt - result.alt = false; - } - - return result; - } - - /// Checks to see if super is on (MacOS) or ctrl. - pub fn ctrlOrSuper(self: Mods) bool { - if (comptime builtin.target.os.tag.isDarwin()) { - return self.super; - } - return self.ctrl; - } - - // For our own understanding - test { - const testing = std.testing; - try testing.expectEqual(@as(Backing, @bitCast(Mods{})), @as(Backing, 0b0)); - try testing.expectEqual( - @as(Backing, @bitCast(Mods{ .shift = true })), - @as(Backing, 0b0000_0001), - ); - } - - test "translation macos-option-as-alt" { - if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest; - - const testing = std.testing; - - // Unset - { - const mods: Mods = .{}; - const result = mods.translation(.true); - try testing.expectEqual(result, mods); - } - - // Set - { - const mods: Mods = .{ .alt = true }; - const result = mods.translation(.true); - try testing.expectEqual(Mods{}, result); - } - - // Set but disabled - { - const mods: Mods = .{ .alt = true }; - const result = mods.translation(.false); - try testing.expectEqual(result, mods); - } - - // Set wrong side - { - const mods: Mods = .{ .alt = true, .sides = .{ .alt = .right } }; - const result = mods.translation(.left); - try testing.expectEqual(result, mods); - } - { - const mods: Mods = .{ .alt = true, .sides = .{ .alt = .left } }; - const result = mods.translation(.right); - try testing.expectEqual(result, mods); - } - - // Set with other mods - { - const mods: Mods = .{ .alt = true, .shift = true }; - const result = mods.translation(.true); - try testing.expectEqual(Mods{ .shift = true }, result); - } - } -}; - /// The action associated with an input event. This is backed by a c_int /// so that we can use the enum as-is for our embedding API. /// @@ -696,7 +543,7 @@ pub const Key = enum(c_int) { } /// Returns the cimgui key constant for this key. - pub fn imguiKey(self: Key) ?c_uint { + pub fn imguiKey(self: Key) ?c_int { return switch (self) { .key_a => cimgui.c.ImGuiKey_A, .key_b => cimgui.c.ImGuiKey_B, diff --git a/src/input/key_encode.zig b/src/input/key_encode.zig index b63de6f6d..3716c226e 100644 --- a/src/input/key_encode.zig +++ b/src/input/key_encode.zig @@ -153,7 +153,7 @@ fn kitty( // IME confirmation still sends an enter key so if we have enter // and UTF8 text we just send it directly since we assume that is - // whats happening. See legacy()'s similar logic for more details + // what's happening. See legacy()'s similar logic for more details // on how to verify this. if (event.utf8.len > 0) utf8: { switch (event.key) { @@ -178,7 +178,7 @@ fn kitty( // Quote ("report all" mode): // Note that all keys are reported as escape codes, including Enter, // Tab, Backspace etc. - if (effective_mods.empty()) { + if (binding_mods.empty()) { switch (event.key) { .enter => return try writer.writeByte('\r'), .tab => return try writer.writeByte('\t'), @@ -1311,7 +1311,48 @@ test "kitty: enter, backspace, tab" { try testing.expectEqualStrings("\x1b[9;1:3u", writer.buffered()); } } -// + +test "kitty: shift+backspace emits CSI u" { + // Backspace with shift modifier should emit CSI u sequence, not raw 0x7F. + // This is important for programs that want to distinguish shift+backspace. + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .backspace, + .mods = .{ .shift = true }, + .utf8 = "", + }, .{ + .kitty_flags = .{ .disambiguate = true }, + }); + try testing.expectEqualStrings("\x1b[127;2u", writer.buffered()); +} + +test "kitty: shift+enter emits CSI u" { + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .enter, + .mods = .{ .shift = true }, + .utf8 = "", + }, .{ + .kitty_flags = .{ .disambiguate = true }, + }); + try testing.expectEqualStrings("\x1b[13;2u", writer.buffered()); +} + +test "kitty: shift+tab emits CSI u" { + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .tab, + .mods = .{ .shift = true }, + .utf8 = "", + }, .{ + .kitty_flags = .{ .disambiguate = true }, + }); + try testing.expectEqualStrings("\x1b[9;2u", writer.buffered()); +} + test "kitty: enter with all flags" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); diff --git a/src/input/key_mods.zig b/src/input/key_mods.zig new file mode 100644 index 000000000..35e1c1038 --- /dev/null +++ b/src/input/key_mods.zig @@ -0,0 +1,914 @@ +const std = @import("std"); +const assert = @import("../quirks.zig").inlineAssert; +const Allocator = std.mem.Allocator; +const builtin = @import("builtin"); +const OptionAsAlt = @import("config.zig").OptionAsAlt; + +/// Aliases for modifier names. +pub const alias: []const struct { []const u8, Mod } = &.{ + .{ "cmd", .super }, + .{ "command", .super }, + .{ "opt", .alt }, + .{ "option", .alt }, + .{ "control", .ctrl }, +}; + +/// Single modifier +pub const Mod = enum { + shift, + ctrl, + alt, + super, + + pub const Side = enum(u1) { left, right }; +}; + +/// A bitmask for all key modifiers. +/// +/// IMPORTANT: Any changes here update include/ghostty.h +pub const Mods = packed struct(Mods.Backing) { + pub const Backing = u16; + + shift: bool = false, + ctrl: bool = false, + alt: bool = false, + super: bool = false, + caps_lock: bool = false, + num_lock: bool = false, + sides: Side = .{}, + _padding: u6 = 0, + + /// The standard modifier keys only. Does not include the lock keys, + /// only standard bindable keys. + pub const Keys = packed struct(u4) { + shift: bool = false, + ctrl: bool = false, + alt: bool = false, + super: bool = false, + + pub const Backing = @typeInfo(Keys).@"struct".backing_integer.?; + + pub inline fn int(self: Keys) Keys.Backing { + return @bitCast(self); + } + }; + + /// Tracks the side that is active for any given modifier. Note + /// that this doesn't confirm a modifier is pressed; you must check + /// the bool for that in addition to this. + /// + /// Not all platforms support this, check apprt for more info. + pub const Side = packed struct(u4) { + shift: Mod.Side = .left, + ctrl: Mod.Side = .left, + alt: Mod.Side = .left, + super: Mod.Side = .left, + + pub const Backing = @typeInfo(Side).@"struct".backing_integer.?; + }; + + /// The mask that has all the side bits set. + pub const side_mask: Mods = .{ + .sides = .{ + .shift = .right, + .ctrl = .right, + .alt = .right, + .super = .right, + }, + }; + + /// Integer value of this struct. + pub fn int(self: Mods) Backing { + return @bitCast(self); + } + + /// Returns true if no modifiers are set. + pub fn empty(self: Mods) bool { + return self.int() == 0; + } + + /// Returns true if two mods are equal. + pub fn equal(self: Mods, other: Mods) bool { + return self.int() == other.int(); + } + + /// Returns only the keys. + /// + /// In the future I want to remove `binding` for this. I didn't want + /// to do that all in one PR where I added this because its a bigger + /// change. + pub fn keys(self: Mods) Keys { + const backing: Keys.Backing = @truncate(self.int()); + return @bitCast(backing); + } + + /// Return mods that are only relevant for bindings. + pub fn binding(self: Mods) Mods { + return .{ + .shift = self.shift, + .ctrl = self.ctrl, + .alt = self.alt, + .super = self.super, + }; + } + + /// Perform `self &~ other` to remove the other mods from self. + pub fn unset(self: Mods, other: Mods) Mods { + return @bitCast(self.int() & ~other.int()); + } + + /// Returns the mods without locks set. + pub fn withoutLocks(self: Mods) Mods { + var copy = self; + copy.caps_lock = false; + copy.num_lock = false; + return copy; + } + + /// Return the mods to use for key translation. This handles settings + /// like macos-option-as-alt. The translation mods should be used for + /// translation but never sent back in for the key callback. + pub fn translation(self: Mods, option_as_alt: OptionAsAlt) Mods { + var result = self; + + // macos-option-as-alt for darwin + if (comptime builtin.target.os.tag.isDarwin()) alt: { + // Alt has to be set only on the correct side + switch (option_as_alt) { + .false => break :alt, + .true => {}, + .left => if (self.sides.alt == .right) break :alt, + .right => if (self.sides.alt == .left) break :alt, + } + + // Unset alt + result.alt = false; + } + + return result; + } + + /// Checks to see if super is on (MacOS) or ctrl. + pub fn ctrlOrSuper(self: Mods) bool { + if (comptime builtin.target.os.tag.isDarwin()) { + return self.super; + } + return self.ctrl; + } + + // For our own understanding + test { + const testing = std.testing; + try testing.expectEqual(@as(Backing, @bitCast(Mods{})), @as(Backing, 0b0)); + try testing.expectEqual( + @as(Backing, @bitCast(Mods{ .shift = true })), + @as(Backing, 0b0000_0001), + ); + } + + test "translation macos-option-as-alt" { + if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest; + + const testing = std.testing; + + // Unset + { + const mods: Mods = .{}; + const result = mods.translation(.true); + try testing.expectEqual(result, mods); + } + + // Set + { + const mods: Mods = .{ .alt = true }; + const result = mods.translation(.true); + try testing.expectEqual(Mods{}, result); + } + + // Set but disabled + { + const mods: Mods = .{ .alt = true }; + const result = mods.translation(.false); + try testing.expectEqual(result, mods); + } + + // Set wrong side + { + const mods: Mods = .{ .alt = true, .sides = .{ .alt = .right } }; + const result = mods.translation(.left); + try testing.expectEqual(result, mods); + } + { + const mods: Mods = .{ .alt = true, .sides = .{ .alt = .left } }; + const result = mods.translation(.right); + try testing.expectEqual(result, mods); + } + + // Set with other mods + { + const mods: Mods = .{ .alt = true, .shift = true }; + const result = mods.translation(.true); + try testing.expectEqual(Mods{ .shift = true }, result); + } + } +}; + +/// Modifier remapping. See `key-remap` in Config.zig for detailed docs. +pub const RemapSet = struct { + /// Available mappings. + map: std.AutoArrayHashMapUnmanaged(Mods, Mods), + + /// The mask of remapped modifiers that can be used to quickly + /// check if some input mods need remapping. + mask: Mask, + + pub const empty: RemapSet = .{ + .map = .{}, + .mask = .{}, + }; + + pub const ParseError = Allocator.Error || error{ + MissingAssignment, + InvalidMod, + }; + + /// Parse from CLI input. Required by Config. + pub fn parseCLI(self: *RemapSet, alloc: Allocator, input: ?[]const u8) !void { + const value = input orelse ""; + + // Empty value resets the set + if (value.len == 0) { + self.map.clearRetainingCapacity(); + self.mask = .{}; + return; + } + + self.parse(alloc, value) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + error.MissingAssignment, error.InvalidMod => return error.InvalidValue, + }; + } + + /// Parse a modifier remap and add it to the set. + pub fn parse( + self: *RemapSet, + alloc: Allocator, + input: []const u8, + ) ParseError!void { + // Find the assignment point ('=') + const eql_idx = std.mem.indexOfScalar( + u8, + input, + '=', + ) orelse return error.MissingAssignment; + + // The to side defaults to "left" if no explicit side is given. + // This is because this is the default unsided value provided by + // the apprts in the current Mods layout. + const to: Mods = to: { + const raw = try parseMod(input[eql_idx + 1 ..]); + break :to initMods(raw[0], raw[1] orelse .left); + }; + + // The from side, if sided, is easy and we put it directly into + // the map. + const from_raw = try parseMod(input[0..eql_idx]); + if (from_raw[1]) |from_side| { + const from: Mods = initMods(from_raw[0], from_side); + try self.map.put( + alloc, + from, + to, + ); + errdefer comptime unreachable; + self.mask.update(from); + return; + } + + // We need to do some combinatorial explosion here for unsided + // from in order to assign all possible sides. + const from_left = initMods(from_raw[0], .left); + const from_right = initMods(from_raw[0], .right); + try self.map.put( + alloc, + from_left, + to, + ); + errdefer _ = self.map.swapRemove(from_left); + try self.map.put( + alloc, + from_right, + to, + ); + errdefer _ = self.map.swapRemove(from_right); + + errdefer comptime unreachable; + self.mask.update(from_left); + self.mask.update(from_right); + } + + pub fn deinit(self: *RemapSet, alloc: Allocator) void { + self.map.deinit(alloc); + } + + /// Must be called prior to any remappings so that the mapping + /// is sorted properly. Otherwise, you will get invalid results. + pub fn finalize(self: *RemapSet) void { + const Context = struct { + keys: []const Mods, + + pub fn lessThan( + ctx: @This(), + a_index: usize, + b_index: usize, + ) bool { + _ = b_index; + + // Mods with any right sides prioritize + const side_mask = comptime Mods.side_mask.int(); + const a = ctx.keys[a_index]; + return a.int() & side_mask != 0; + } + }; + + self.map.sort(Context{ .keys = self.map.keys() }); + } + + /// Deep copy of the struct. Required by Config. + pub fn clone(self: *const RemapSet, alloc: Allocator) Allocator.Error!RemapSet { + return .{ + .map = try self.map.clone(alloc), + .mask = self.mask, + }; + } + + /// Compare if two RemapSets are equal. Required by Config. + pub fn equal(self: RemapSet, other: RemapSet) bool { + if (self.map.count() != other.map.count()) return false; + + var it = self.map.iterator(); + while (it.next()) |entry| { + const other_value = other.map.get(entry.key_ptr.*) orelse return false; + if (!entry.value_ptr.equal(other_value)) return false; + } + + return true; + } + + /// Used by Formatter. Required by Config. + pub fn formatEntry(self: RemapSet, formatter: anytype) !void { + if (self.map.count() == 0) { + try formatter.formatEntry(void, {}); + return; + } + + var it = self.map.iterator(); + while (it.next()) |entry| { + const from = entry.key_ptr.*; + const to = entry.value_ptr.*; + + var buf: [64]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + const writer = fbs.writer(); + formatMod(writer, from) catch return error.OutOfMemory; + writer.writeByte('=') catch return error.OutOfMemory; + formatMod(writer, to) catch return error.OutOfMemory; + try formatter.formatEntry([]const u8, fbs.getWritten()); + } + } + + fn formatMod(writer: anytype, mods: Mods) !void { + // Check which mod is set and format it with optional side prefix + inline for (.{ "shift", "ctrl", "alt", "super" }) |name| { + if (@field(mods, name)) { + const side = @field(mods.sides, name); + if (side == .right) { + try writer.writeAll("right_"); + } else { + // Only write left_ if we need to distinguish + // For now, always write left_ if it's a sided mapping + try writer.writeAll("left_"); + } + try writer.writeAll(name); + return; + } + } + } + + /// Parses a single mode in a single remapping string. E.g. + /// `ctrl` or `left_shift`. + fn parseMod(input: []const u8) error{InvalidMod}!struct { Mod, ?Mod.Side } { + const side_str, const mod_str = if (std.mem.indexOfScalar( + u8, + input, + '_', + )) |idx| .{ + input[0..idx], + input[idx + 1 ..], + } else .{ + "", + input, + }; + + const mod: Mod = if (std.meta.stringToEnum( + Mod, + mod_str, + )) |mod| mod else mod: { + inline for (alias) |pair| { + if (std.mem.eql(u8, mod_str, pair[0])) { + break :mod pair[1]; + } + } + + return error.InvalidMod; + }; + + return .{ + mod, + if (side_str.len > 0) std.meta.stringToEnum( + Mod.Side, + side_str, + ) orelse return error.InvalidMod else null, + }; + } + + fn initMods(mod: Mod, side: Mod.Side) Mods { + switch (mod) { + inline else => |tag| { + var mods: Mods = .{}; + @field(mods, @tagName(tag)) = true; + @field(mods.sides, @tagName(tag)) = side; + return mods; + }, + } + } + + /// Returns true if the given mods need remapping. + pub fn isRemapped(self: *const RemapSet, mods: Mods) bool { + return self.mask.match(mods); + } + + /// Apply a remap to the given mods. + pub fn apply(self: *const RemapSet, mods: Mods) Mods { + if (!self.isRemapped(mods)) return mods; + + const mods_binding: Mods.Keys.Backing = @truncate(mods.int()); + const mods_sides: Mods.Side.Backing = @bitCast(mods.sides); + + var it = self.map.iterator(); + while (it.next()) |entry| { + const from = entry.key_ptr.*; + const from_binding: Mods.Keys.Backing = @truncate(from.int()); + if (mods_binding & from_binding != from_binding) continue; + const from_sides: Mods.Side.Backing = @bitCast(from.sides); + if ((mods_sides ^ from_sides) & from_binding != 0) continue; + + var mods_int = mods.int(); + mods_int &= ~from.int(); + mods_int |= entry.value_ptr.int(); + return @bitCast(mods_int); + } + + unreachable; + } + + /// Tracks which modifier keys and sides have remappings registered. + /// Used as a fast pre-check before doing expensive map lookups. + /// + /// The mask uses separate tracking for left and right sides because + /// remappings can be side-specific (e.g., only remap left_ctrl). + /// + /// Note: `left_sides` uses inverted logic where 1 means "left is remapped" + /// even though `Mod.Side.left = 0`. This allows efficient bitwise matching + /// since we can AND directly with the side bits. + pub const Mask = packed struct(u12) { + /// Which modifier keys (shift/ctrl/alt/super) have any remapping. + keys: Mods.Keys = .{}, + /// Which modifiers have left-side remappings (inverted: 1 = left remapped). + left_sides: Mods.Side = .{}, + /// Which modifiers have right-side remappings (1 = right remapped). + right_sides: Mods.Side = .{}, + + /// Adds a modifier to the mask, marking it as having a remapping. + pub fn update(self: *Mask, mods: Mods) void { + const keys_int: Mods.Keys.Backing = mods.keys().int(); + + // OR the new keys into our existing keys mask. + // Example: keys=0b0000, new ctrl → keys=0b0010 + self.keys = @bitCast(self.keys.int() | keys_int); + + // Both Keys and Side are u4 with matching bit positions. + // This lets us use keys_int to select which side bits to update. + const sides: Mods.Side.Backing = @bitCast(mods.sides); + const left_int: Mods.Side.Backing = @bitCast(self.left_sides); + const right_int: Mods.Side.Backing = @bitCast(self.right_sides); + + // Update left_sides: set bit if this key is active AND side is left. + // Since Side.left=0, we invert sides (~sides) so left becomes 1. + // keys_int masks to only affect the modifier being added. + // Example: left_ctrl → keys_int=0b0010, ~sides=0b1111 (left=0 inverted) + // result: left_int | (0b0010 & 0b1111) = left_int | 0b0010 + self.left_sides = @bitCast(left_int | (keys_int & ~sides)); + + // Update right_sides: set bit if this key is active AND side is right. + // Since Side.right=1, we use sides directly. + // Example: right_ctrl → keys_int=0b0010, sides=0b0010 (right=1) + // result: right_int | (0b0010 & 0b0010) = right_int | 0b0010 + self.right_sides = @bitCast(right_int | (keys_int & sides)); + } + + /// Returns true if the given mods match any remapping in this mask. + /// This is a fast check to avoid expensive map lookups when no + /// remapping could possibly apply. + /// + /// Checks both that the modifier key is remapped AND that the + /// specific side (left/right) being pressed has a remapping. + pub fn match(self: *const Mask, mods: Mods) bool { + // Find which pressed keys have remappings registered. + // Example: pressed={ctrl,alt}, mask={ctrl} → active=0b0010 (just ctrl) + const active = mods.keys().int() & self.keys.int(); + if (active == 0) return false; + + // Check if the pressed side matches a remapped side. + // For left (sides bit = 0): check against left_int (where 1 = left remapped) + // ~sides inverts so left becomes 1, then AND with left_int + // For right (sides bit = 1): check against right_int directly + // + // Example: pressing left_ctrl (sides.ctrl=0, left_int.ctrl=1) + // ~sides = 0b1111, left_int = 0b0010 + // (~sides & left_int) = 0b0010 ✓ matches + // + // Example: pressing right_ctrl but only left_ctrl is remapped + // sides = 0b0010, left_int = 0b0010, right_int = 0b0000 + // (~0b0010 & 0b0010) | (0b0010 & 0b0000) = 0b0000 ✗ no match + const sides: Mods.Side.Backing = @bitCast(mods.sides); + const left_int: Mods.Side.Backing = @bitCast(self.left_sides); + const right_int: Mods.Side.Backing = @bitCast(self.right_sides); + const side_match = (~sides & left_int) | (sides & right_int); + + // Final check: is any active (pressed + remapped) key also side-matched? + return (active & side_match) != 0; + } + }; +}; + +test "RemapSet: unsided remap creates both left and right mappings" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + try set.parse(alloc, "ctrl=super"); + set.finalize(); + try testing.expectEqual( + Mods{ + .super = true, + .sides = .{ .super = .left }, + }, + set.apply(.{ + .ctrl = true, + .sides = .{ .ctrl = .left }, + }), + ); + try testing.expectEqual( + Mods{ + .super = true, + .sides = .{ .super = .left }, + }, + set.apply(.{ + .ctrl = true, + .sides = .{ .ctrl = .right }, + }), + ); +} + +test "RemapSet: sided from only maps that side" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "left_alt=ctrl"); + set.finalize(); + + const left_alt: Mods = .{ .alt = true, .sides = .{ .alt = .left } }; + const left_ctrl: Mods = .{ .ctrl = true, .sides = .{ .ctrl = .left } }; + try testing.expectEqual(left_ctrl, set.apply(left_alt)); + + const right_alt: Mods = .{ .alt = true, .sides = .{ .alt = .right } }; + try testing.expectEqual(right_alt, set.apply(right_alt)); +} + +test "RemapSet: sided to" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "ctrl=right_super"); + set.finalize(); + + const left_ctrl: Mods = .{ .ctrl = true, .sides = .{ .ctrl = .left } }; + const right_super: Mods = .{ .super = true, .sides = .{ .super = .right } }; + try testing.expectEqual(right_super, set.apply(left_ctrl)); +} + +test "RemapSet: both sides specified" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "left_shift=right_ctrl"); + set.finalize(); + + const left_shift: Mods = .{ .shift = true, .sides = .{ .shift = .left } }; + const right_ctrl: Mods = .{ .ctrl = true, .sides = .{ .ctrl = .right } }; + try testing.expectEqual(right_ctrl, set.apply(left_shift)); +} + +test "RemapSet: multiple parses accumulate" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "left_ctrl=super"); + try set.parse(alloc, "left_alt=ctrl"); + set.finalize(); + + const left_ctrl: Mods = .{ .ctrl = true, .sides = .{ .ctrl = .left } }; + const left_super: Mods = .{ .super = true, .sides = .{ .super = .left } }; + try testing.expectEqual(left_super, set.apply(left_ctrl)); + + const left_alt: Mods = .{ .alt = true, .sides = .{ .alt = .left } }; + const left_ctrl_result: Mods = .{ .ctrl = true, .sides = .{ .ctrl = .left } }; + try testing.expectEqual(left_ctrl_result, set.apply(left_alt)); +} + +test "RemapSet: error on missing assignment" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try testing.expectError(error.MissingAssignment, set.parse(alloc, "ctrl")); + try testing.expectError(error.MissingAssignment, set.parse(alloc, "")); +} + +test "RemapSet: error on invalid modifier" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try testing.expectError(error.InvalidMod, set.parse(alloc, "invalid=ctrl")); + try testing.expectError(error.InvalidMod, set.parse(alloc, "ctrl=invalid")); + try testing.expectError(error.InvalidMod, set.parse(alloc, "middle_ctrl=super")); +} + +test "RemapSet: isRemapped checks mask" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "ctrl=super"); + set.finalize(); + + try testing.expect(set.isRemapped(.{ .ctrl = true })); + try testing.expect(!set.isRemapped(.{ .alt = true })); + try testing.expect(!set.isRemapped(.{ .shift = true })); +} + +test "RemapSet: clone creates independent copy" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "ctrl=super"); + set.finalize(); + + var cloned = try set.clone(alloc); + defer cloned.deinit(alloc); + + try testing.expect(set.equal(cloned)); + try testing.expect(cloned.isRemapped(.{ .ctrl = true })); +} + +test "RemapSet: equal compares correctly" { + const testing = std.testing; + const alloc = testing.allocator; + + var set1: RemapSet = .empty; + defer set1.deinit(alloc); + + var set2: RemapSet = .empty; + defer set2.deinit(alloc); + + try testing.expect(set1.equal(set2)); + + try set1.parse(alloc, "ctrl=super"); + try testing.expect(!set1.equal(set2)); + + try set2.parse(alloc, "ctrl=super"); + try testing.expect(set1.equal(set2)); + + try set1.parse(alloc, "alt=shift"); + try testing.expect(!set1.equal(set2)); +} + +test "RemapSet: parseCLI basic" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parseCLI(alloc, "ctrl=super"); + try testing.expectEqual(@as(usize, 2), set.map.count()); +} + +test "RemapSet: parseCLI empty clears" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parseCLI(alloc, "ctrl=super"); + try testing.expectEqual(@as(usize, 2), set.map.count()); + + try set.parseCLI(alloc, ""); + try testing.expectEqual(@as(usize, 0), set.map.count()); +} + +test "RemapSet: parseCLI invalid" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try testing.expectError(error.InvalidValue, set.parseCLI(alloc, "foo=bar")); + try testing.expectError(error.InvalidValue, set.parseCLI(alloc, "ctrl")); +} + +test "RemapSet: parse aliased modifiers" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "cmd=ctrl"); + set.finalize(); + + const left_super: Mods = .{ .super = true, .sides = .{ .super = .left } }; + const left_ctrl: Mods = .{ .ctrl = true, .sides = .{ .ctrl = .left } }; + try testing.expectEqual(left_ctrl, set.apply(left_super)); +} + +test "RemapSet: parse aliased modifiers command" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "command=alt"); + set.finalize(); + + const left_super: Mods = .{ .super = true, .sides = .{ .super = .left } }; + const left_alt: Mods = .{ .alt = true, .sides = .{ .alt = .left } }; + try testing.expectEqual(left_alt, set.apply(left_super)); +} + +test "RemapSet: parse aliased modifiers opt and option" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "opt=super"); + set.finalize(); + + const left_alt: Mods = .{ .alt = true, .sides = .{ .alt = .left } }; + const left_super: Mods = .{ .super = true, .sides = .{ .super = .left } }; + try testing.expectEqual(left_super, set.apply(left_alt)); + + set.deinit(alloc); + set = .empty; + + try set.parse(alloc, "option=shift"); + set.finalize(); + + const left_shift: Mods = .{ .shift = true, .sides = .{ .shift = .left } }; + try testing.expectEqual(left_shift, set.apply(left_alt)); +} + +test "RemapSet: parse aliased modifiers control" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "control=super"); + set.finalize(); + + const left_ctrl: Mods = .{ .ctrl = true, .sides = .{ .ctrl = .left } }; + const left_super: Mods = .{ .super = true, .sides = .{ .super = .left } }; + try testing.expectEqual(left_super, set.apply(left_ctrl)); +} + +test "RemapSet: parse aliased modifiers on target side" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "alt=cmd"); + set.finalize(); + + const left_alt: Mods = .{ .alt = true, .sides = .{ .alt = .left } }; + const left_super: Mods = .{ .super = true, .sides = .{ .super = .left } }; + try testing.expectEqual(left_super, set.apply(left_alt)); +} + +test "RemapSet: formatEntry empty" { + const testing = std.testing; + const formatterpkg = @import("../config/formatter.zig"); + + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + const set: RemapSet = .empty; + try set.formatEntry(formatterpkg.entryFormatter("key-remap", &buf.writer)); + try testing.expectEqualSlices(u8, "key-remap = \n", buf.written()); +} + +test "RemapSet: formatEntry single sided" { + const testing = std.testing; + const formatterpkg = @import("../config/formatter.zig"); + + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + var set: RemapSet = .empty; + defer set.deinit(testing.allocator); + + try set.parse(testing.allocator, "left_ctrl=super"); + set.finalize(); + + try set.formatEntry(formatterpkg.entryFormatter("key-remap", &buf.writer)); + try testing.expectEqualSlices(u8, "key-remap = left_ctrl=left_super\n", buf.written()); +} + +test "RemapSet: formatEntry unsided creates two entries" { + const testing = std.testing; + const formatterpkg = @import("../config/formatter.zig"); + + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + var set: RemapSet = .empty; + defer set.deinit(testing.allocator); + + try set.parse(testing.allocator, "ctrl=super"); + set.finalize(); + + try set.formatEntry(formatterpkg.entryFormatter("key-remap", &buf.writer)); + // Unsided creates both left and right mappings + const written = buf.written(); + try testing.expect(std.mem.indexOf(u8, written, "left_ctrl=left_super") != null); + try testing.expect(std.mem.indexOf(u8, written, "right_ctrl=left_super") != null); +} + +test "RemapSet: formatEntry right sided" { + const testing = std.testing; + const formatterpkg = @import("../config/formatter.zig"); + + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + var set: RemapSet = .empty; + defer set.deinit(testing.allocator); + + try set.parse(testing.allocator, "left_alt=right_ctrl"); + set.finalize(); + + try set.formatEntry(formatterpkg.entryFormatter("key-remap", &buf.writer)); + try testing.expectEqualSlices(u8, "key-remap = left_alt=right_ctrl\n", buf.written()); +} diff --git a/src/input/mouse.zig b/src/input/mouse.zig index 2be2b9a26..bdf967ed2 100644 --- a/src/input/mouse.zig +++ b/src/input/mouse.zig @@ -10,7 +10,7 @@ pub const ButtonState = enum(c_int) { press, }; -/// Possible mouse buttons. We only track up to 11 because thats the maximum +/// Possible mouse buttons. We only track up to 11 because that's the maximum /// button input that terminal mouse tracking handles without becoming /// ambiguous. /// diff --git a/src/inspector/Inspector.zig b/src/inspector/Inspector.zig index 86a7b473c..156e2cb18 100644 --- a/src/inspector/Inspector.zig +++ b/src/inspector/Inspector.zig @@ -7,7 +7,7 @@ const std = @import("std"); const assert = @import("../quirks.zig").inlineAssert; const Allocator = std.mem.Allocator; const builtin = @import("builtin"); -const cimgui = @import("cimgui"); +const cimgui = @import("dcimgui"); const Surface = @import("../Surface.zig"); const font = @import("../font/main.zig"); const input = @import("../input.zig"); @@ -126,7 +126,7 @@ const CellInspect = union(enum) { /// Setup the ImGui state. This requires an ImGui context to be set. pub fn setup() void { - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); // Enable docking, which we use heavily for the UI. io.ConfigFlags |= cimgui.c.ImGuiConfigFlags_DockingEnable; @@ -144,15 +144,15 @@ pub fn setup() void { // This is currently hardcoded to a 2x content scale. const font_size = 16 * 2; - const font_config: *cimgui.c.ImFontConfig = cimgui.c.ImFontConfig_ImFontConfig(); - defer cimgui.c.ImFontConfig_destroy(font_config); + var font_config: cimgui.c.ImFontConfig = undefined; + cimgui.ext.ImFontConfig_ImFontConfig(&font_config); font_config.FontDataOwnedByAtlas = false; _ = cimgui.c.ImFontAtlas_AddFontFromMemoryTTF( io.Fonts, - @ptrCast(@constCast(font.embedded.regular)), - font.embedded.regular.len, + @ptrCast(@constCast(font.embedded.regular.ptr)), + @intCast(font.embedded.regular.len), font_size, - font_config, + &font_config, null, ); } @@ -221,11 +221,7 @@ pub fn recordPtyRead(self: *Inspector, data: []const u8) !void { /// Render the frame. pub fn render(self: *Inspector) void { - const dock_id = cimgui.c.igDockSpaceOverViewport( - cimgui.c.igGetMainViewport(), - cimgui.c.ImGuiDockNodeFlags_None, - null, - ); + const dock_id = cimgui.c.ImGui_DockSpaceOverViewport(); // Render all of our data. We hold the mutex for this duration. This is // expensive but this is an initial implementation until it doesn't work @@ -245,7 +241,7 @@ pub fn render(self: *Inspector) void { // widgets and such. if (builtin.mode == .Debug) { var show: bool = true; - cimgui.c.igShowDemoWindow(&show); + cimgui.c.ImGui_ShowDemoWindow(&show); } // On first render we set up the layout. We can actually do this at @@ -261,7 +257,7 @@ fn setupLayout(self: *Inspector, dock_id_main: cimgui.c.ImGuiID) void { _ = self; // Our initial focus - cimgui.c.igSetWindowFocus_Str(window_screen); + cimgui.c.ImGui_SetWindowFocusStr(window_screen); // Setup our initial layout. const dock_id: struct { @@ -270,7 +266,7 @@ fn setupLayout(self: *Inspector, dock_id_main: cimgui.c.ImGuiID) void { } = dock_id: { var dock_id_left: cimgui.c.ImGuiID = undefined; var dock_id_right: cimgui.c.ImGuiID = undefined; - _ = cimgui.c.igDockBuilderSplitNode( + _ = cimgui.ImGui_DockBuilderSplitNode( dock_id_main, cimgui.c.ImGuiDir_Left, 0.7, @@ -284,20 +280,20 @@ fn setupLayout(self: *Inspector, dock_id_main: cimgui.c.ImGuiID) void { }; }; - cimgui.c.igDockBuilderDockWindow(window_cell, dock_id.left); - cimgui.c.igDockBuilderDockWindow(window_modes, dock_id.left); - cimgui.c.igDockBuilderDockWindow(window_keyboard, dock_id.left); - cimgui.c.igDockBuilderDockWindow(window_termio, dock_id.left); - cimgui.c.igDockBuilderDockWindow(window_screen, dock_id.left); - cimgui.c.igDockBuilderDockWindow(window_imgui_demo, dock_id.left); - cimgui.c.igDockBuilderDockWindow(window_size, dock_id.right); - cimgui.c.igDockBuilderFinish(dock_id_main); + cimgui.ImGui_DockBuilderDockWindow(window_cell, dock_id.left); + cimgui.ImGui_DockBuilderDockWindow(window_modes, dock_id.left); + cimgui.ImGui_DockBuilderDockWindow(window_keyboard, dock_id.left); + cimgui.ImGui_DockBuilderDockWindow(window_termio, dock_id.left); + cimgui.ImGui_DockBuilderDockWindow(window_screen, dock_id.left); + cimgui.ImGui_DockBuilderDockWindow(window_imgui_demo, dock_id.left); + cimgui.ImGui_DockBuilderDockWindow(window_size, dock_id.right); + cimgui.ImGui_DockBuilderFinish(dock_id_main); } fn renderScreenWindow(self: *Inspector) void { // Start our window. If we're collapsed we do nothing. - defer cimgui.c.igEnd(); - if (!cimgui.c.igBegin( + defer cimgui.c.ImGui_End(); + if (!cimgui.c.ImGui_Begin( window_screen, null, cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing, @@ -307,76 +303,70 @@ fn renderScreenWindow(self: *Inspector) void { const screen: *terminal.Screen = t.screens.active; { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_screen", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Active Screen"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Active Screen"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%s", @tagName(t.screens.active_key).ptr); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%s", @tagName(t.screens.active_key).ptr); } } } - if (cimgui.c.igCollapsingHeader_TreeNodeFlags( + if (cimgui.c.ImGui_CollapsingHeader( "Cursor", cimgui.c.ImGuiTreeNodeFlags_DefaultOpen, )) { { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_cursor", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); inspector.cursor.renderInTable( self.surface.renderer_state.terminal, &screen.cursor, ); } // table - cimgui.c.igTextDisabled("(Any styles not shown are not currently set)"); + cimgui.c.ImGui_TextDisabled("(Any styles not shown are not currently set)"); } // cursor - if (cimgui.c.igCollapsingHeader_TreeNodeFlags( + if (cimgui.c.ImGui_CollapsingHeader( "Keyboard", cimgui.c.ImGuiTreeNodeFlags_DefaultOpen, )) { { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_keyboard", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); const kitty_flags = screen.kitty_keyboard.current(); { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Mode"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Mode"); } { - _ = cimgui.c.igTableSetColumnIndex(1); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); const mode = if (kitty_flags.int() != 0) "kitty" else "legacy"; - cimgui.c.igText("%s", mode.ptr); + cimgui.c.ImGui_Text("%s", mode.ptr); } } @@ -386,15 +376,15 @@ fn renderScreenWindow(self: *Inspector) void { { const value = @field(kitty_flags, field.name); - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); const name = std.fmt.comptimePrint("{s}", .{field.name}); - cimgui.c.igText("%s", name.ptr); + cimgui.c.ImGui_Text("%s", name.ptr); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "%s", if (value) "true".ptr else "false".ptr, ); @@ -403,14 +393,14 @@ fn renderScreenWindow(self: *Inspector) void { } } else { { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Xterm modify keys"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Xterm modify keys"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "%s", if (t.flags.modify_other_keys_2) "true".ptr else "false".ptr, ); @@ -420,143 +410,139 @@ fn renderScreenWindow(self: *Inspector) void { } // table } // keyboard - if (cimgui.c.igCollapsingHeader_TreeNodeFlags( + if (cimgui.c.ImGui_CollapsingHeader( "Kitty Graphics", cimgui.c.ImGuiTreeNodeFlags_DefaultOpen, )) kitty_gfx: { if (!screen.kitty_images.enabled()) { - cimgui.c.igTextDisabled("(Kitty graphics are disabled)"); + cimgui.c.ImGui_TextDisabled("(Kitty graphics are disabled)"); break :kitty_gfx; } { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "##kitty_graphics", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); const kitty_images = &screen.kitty_images; { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Memory Usage"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Memory Usage"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d bytes (%d KiB)", kitty_images.total_bytes, units.toKibiBytes(kitty_images.total_bytes)); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d bytes (%d KiB)", kitty_images.total_bytes, units.toKibiBytes(kitty_images.total_bytes)); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Memory Limit"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Memory Limit"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d bytes (%d KiB)", kitty_images.total_limit, units.toKibiBytes(kitty_images.total_limit)); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d bytes (%d KiB)", kitty_images.total_limit, units.toKibiBytes(kitty_images.total_limit)); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Image Count"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Image Count"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", kitty_images.images.count()); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", kitty_images.images.count()); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Placement Count"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Placement Count"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", kitty_images.placements.count()); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", kitty_images.placements.count()); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Image Loading"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Image Loading"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%s", if (kitty_images.loading != null) "true".ptr else "false".ptr); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%s", if (kitty_images.loading != null) "true".ptr else "false".ptr); } } } // table } // kitty graphics - if (cimgui.c.igCollapsingHeader_TreeNodeFlags( + if (cimgui.c.ImGui_CollapsingHeader( "Internal Terminal State", cimgui.c.ImGuiTreeNodeFlags_DefaultOpen, )) { const pages = &screen.pages; { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "##terminal_state", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Memory Usage"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Memory Usage"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d bytes (%d KiB)", pages.page_size, units.toKibiBytes(pages.page_size)); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d bytes (%d KiB)", pages.page_size, units.toKibiBytes(pages.page_size)); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Memory Limit"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Memory Limit"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d bytes (%d KiB)", pages.maxSize(), units.toKibiBytes(pages.maxSize())); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d bytes (%d KiB)", pages.maxSize(), units.toKibiBytes(pages.maxSize())); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Viewport Location"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Viewport Location"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%s", @tagName(pages.viewport).ptr); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%s", @tagName(pages.viewport).ptr); } } } // table // - if (cimgui.c.igCollapsingHeader_TreeNodeFlags( + if (cimgui.c.ImGui_CollapsingHeader( "Active Page", cimgui.c.ImGuiTreeNodeFlags_DefaultOpen, )) { @@ -569,28 +555,26 @@ fn renderScreenWindow(self: *Inspector) void { /// users to toggle them on and off. fn renderModesWindow(self: *Inspector) void { // Start our window. If we're collapsed we do nothing. - defer cimgui.c.igEnd(); - if (!cimgui.c.igBegin( + defer cimgui.c.ImGui_End(); + if (!cimgui.c.ImGui_Begin( window_modes, null, cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing, )) return; - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_modes", 3, cimgui.c.ImGuiTableFlags_SizingFixedFit | cimgui.c.ImGuiTableFlags_RowBg, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); { - _ = cimgui.c.igTableSetupColumn("", cimgui.c.ImGuiTableColumnFlags_NoResize, 0, 0); - _ = cimgui.c.igTableSetupColumn("Number", cimgui.c.ImGuiTableColumnFlags_PreferSortAscending, 0, 0); - _ = cimgui.c.igTableSetupColumn("Name", cimgui.c.ImGuiTableColumnFlags_WidthStretch, 0, 0); - cimgui.c.igTableHeadersRow(); + cimgui.c.ImGui_TableSetupColumn("", cimgui.c.ImGuiTableColumnFlags_NoResize); + cimgui.c.ImGui_TableSetupColumn("Number", cimgui.c.ImGuiTableColumnFlags_PreferSortAscending); + cimgui.c.ImGui_TableSetupColumn("Name", cimgui.c.ImGuiTableColumnFlags_WidthStretch); + cimgui.c.ImGui_TableHeadersRow(); } const t = self.surface.renderer_state.terminal; @@ -598,59 +582,57 @@ fn renderModesWindow(self: *Inspector) void { @setEvalBranchQuota(6000); const tag: terminal.modes.ModeTag = @bitCast(@as(terminal.modes.ModeTag.Backing, field.value)); - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); var value: bool = t.modes.get(@field(terminal.Mode, field.name)); - _ = cimgui.c.igCheckbox("", &value); + _ = cimgui.c.ImGui_Checkbox("", &value); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "%s%d", if (tag.ansi) "" else "?", @as(u32, @intCast(tag.value)), ); } { - _ = cimgui.c.igTableSetColumnIndex(2); + _ = cimgui.c.ImGui_TableSetColumnIndex(2); const name = std.fmt.comptimePrint("{s}", .{field.name}); - cimgui.c.igText("%s", name.ptr); + cimgui.c.ImGui_Text("%s", name.ptr); } } } fn renderSizeWindow(self: *Inspector) void { // Start our window. If we're collapsed we do nothing. - defer cimgui.c.igEnd(); - if (!cimgui.c.igBegin( + defer cimgui.c.ImGui_End(); + if (!cimgui.c.ImGui_Begin( window_size, null, cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing, )) return; - cimgui.c.igSeparatorText("Dimensions"); + cimgui.c.ImGui_SeparatorText("Dimensions"); { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_size", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); // Screen Size { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Screen Size"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Screen Size"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "%dpx x %dpx", self.surface.size.screen.width, self.surface.size.screen.height, @@ -660,15 +642,15 @@ fn renderSizeWindow(self: *Inspector) void { // Grid Size { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Grid Size"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Grid Size"); } { - _ = cimgui.c.igTableSetColumnIndex(1); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); const grid_size = self.surface.size.grid(); - cimgui.c.igText( + cimgui.c.ImGui_Text( "%dc x %dr", grid_size.columns, grid_size.rows, @@ -678,14 +660,14 @@ fn renderSizeWindow(self: *Inspector) void { // Cell Size { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Cell Size"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Cell Size"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "%dpx x %dpx", self.surface.size.cell.width, self.surface.size.cell.height, @@ -695,14 +677,14 @@ fn renderSizeWindow(self: *Inspector) void { // Padding { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Window Padding"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Window Padding"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "T=%d B=%d L=%d R=%d px", self.surface.size.padding.top, self.surface.size.padding.bottom, @@ -713,27 +695,25 @@ fn renderSizeWindow(self: *Inspector) void { } } - cimgui.c.igSeparatorText("Font"); + cimgui.c.ImGui_SeparatorText("Font"); { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_font", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Size (Points)"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Size (Points)"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "%.2f pt", self.surface.font_size.points, ); @@ -741,14 +721,14 @@ fn renderSizeWindow(self: *Inspector) void { } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Size (Pixels)"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Size (Pixels)"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "%.2f px", self.surface.font_size.pixels(), ); @@ -756,17 +736,15 @@ fn renderSizeWindow(self: *Inspector) void { } } - cimgui.c.igSeparatorText("Mouse"); + cimgui.c.ImGui_SeparatorText("Mouse"); { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_mouse", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); const mouse = &self.surface.mouse; const t = self.surface.renderer_state.terminal; @@ -781,14 +759,14 @@ fn renderSizeWindow(self: *Inspector) void { break :pt pt.coord(); }; - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Hover Grid"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Hover Grid"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "row=%d, col=%d", hover_point.y, hover_point.x, @@ -804,14 +782,14 @@ fn renderSizeWindow(self: *Inspector) void { }, }).convert(.terminal, self.surface.size).terminal; - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Hover Point"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Hover Point"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "(%dpx, %dpx)", @as(i64, @intFromFloat(coord.x)), @as(i64, @intFromFloat(coord.y)), @@ -824,23 +802,23 @@ fn renderSizeWindow(self: *Inspector) void { } else false; click: { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Click State"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Click State"); } { - _ = cimgui.c.igTableSetColumnIndex(1); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); if (!any_click) { - cimgui.c.igText("none"); + cimgui.c.ImGui_Text("none"); break :click; } for (mouse.click_state, 0..) |state, i| { if (state != .press) continue; const button: input.MouseButton = @enumFromInt(i); - cimgui.c.igSameLine(0, 0); - cimgui.c.igText("%s", (switch (button) { + cimgui.c.ImGui_SameLine(); + cimgui.c.ImGui_Text("%s", (switch (button) { .unknown => "?", .left => "L", .middle => "M", @@ -868,14 +846,14 @@ fn renderSizeWindow(self: *Inspector) void { break :pt pt.coord(); }; - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Click Grid"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Click Grid"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "row=%d, col=%d", left_click_point.y, left_click_point.x, @@ -884,14 +862,14 @@ fn renderSizeWindow(self: *Inspector) void { } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Click Point"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Click Point"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "(%dpx, %dpx)", @as(u32, @intFromFloat(mouse.left_click_xpos)), @as(u32, @intFromFloat(mouse.left_click_ypos)), @@ -903,8 +881,8 @@ fn renderSizeWindow(self: *Inspector) void { fn renderCellWindow(self: *Inspector) void { // Start our window. If we're collapsed we do nothing. - defer cimgui.c.igEnd(); - if (!cimgui.c.igBegin( + defer cimgui.c.ImGui_End(); + if (!cimgui.c.ImGui_Begin( window_cell, null, cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing, @@ -913,45 +891,45 @@ fn renderCellWindow(self: *Inspector) void { // Our popup for the picker const popup_picker = "Cell Picker"; - if (cimgui.c.igButton("Picker", .{ .x = 0, .y = 0 })) { + if (cimgui.c.ImGui_Button("Picker")) { // Request a cell self.cell.request(); - cimgui.c.igOpenPopup_Str( + cimgui.c.ImGui_OpenPopup( popup_picker, cimgui.c.ImGuiPopupFlags_None, ); } - if (cimgui.c.igBeginPopupModal( + if (cimgui.c.ImGui_BeginPopupModal( popup_picker, null, cimgui.c.ImGuiWindowFlags_AlwaysAutoResize, )) popup: { - defer cimgui.c.igEndPopup(); + defer cimgui.c.ImGui_EndPopup(); // Once we select a cell, close this popup. if (self.cell == .selected) { - cimgui.c.igCloseCurrentPopup(); + cimgui.c.ImGui_CloseCurrentPopup(); break :popup; } - cimgui.c.igText( + cimgui.c.ImGui_Text( "Click on a cell in the terminal to inspect it.\n" ++ "The click will be intercepted by the picker, \n" ++ "so it won't be sent to the terminal.", ); - cimgui.c.igSeparator(); + cimgui.c.ImGui_Separator(); - if (cimgui.c.igButton("Cancel", .{ .x = 0, .y = 0 })) { - cimgui.c.igCloseCurrentPopup(); + if (cimgui.c.ImGui_Button("Cancel")) { + cimgui.c.ImGui_CloseCurrentPopup(); } } // cell pick popup - cimgui.c.igSeparator(); + cimgui.c.ImGui_Separator(); if (self.cell != .selected) { - cimgui.c.igText("No cell selected."); + cimgui.c.ImGui_Text("No cell selected."); return; } @@ -965,8 +943,8 @@ fn renderCellWindow(self: *Inspector) void { fn renderKeyboardWindow(self: *Inspector) void { // Start our window. If we're collapsed we do nothing. - defer cimgui.c.igEnd(); - if (!cimgui.c.igBegin( + defer cimgui.c.ImGui_End(); + if (!cimgui.c.ImGui_Begin( window_keyboard, null, cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing, @@ -974,47 +952,44 @@ fn renderKeyboardWindow(self: *Inspector) void { list: { if (self.key_events.empty()) { - cimgui.c.igText("No recorded key events. Press a key with the " ++ + cimgui.c.ImGui_Text("No recorded key events. Press a key with the " ++ "terminal focused to record it."); break :list; } - if (cimgui.c.igButton("Clear", .{ .x = 0, .y = 0 })) { + if (cimgui.c.ImGui_Button("Clear")) { var it = self.key_events.iterator(.forward); while (it.next()) |v| v.deinit(self.surface.alloc); self.key_events.clear(); self.vt_stream.handler.current_seq = 1; } - cimgui.c.igSeparator(); + cimgui.c.ImGui_Separator(); - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_key_events", 1, //cimgui.c.ImGuiTableFlags_ScrollY | cimgui.c.ImGuiTableFlags_RowBg | cimgui.c.ImGuiTableFlags_Borders, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); var it = self.key_events.iterator(.reverse); while (it.next()) |ev| { // Need to push an ID so that our selectable is unique. - cimgui.c.igPushID_Ptr(ev); - defer cimgui.c.igPopID(); + cimgui.c.ImGui_PushIDPtr(ev); + defer cimgui.c.ImGui_PopID(); - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); var buf: [1024]u8 = undefined; const label = ev.label(&buf) catch "Key Event"; - _ = cimgui.c.igSelectable_BoolPtr( + _ = cimgui.c.ImGui_SelectableBoolPtr( label.ptr, &ev.imgui_state.selected, cimgui.c.ImGuiSelectableFlags_None, - .{ .x = 0, .y = 0 }, ); if (!ev.imgui_state.selected) continue; @@ -1034,7 +1009,7 @@ fn getKeyAction(self: *Inspector) KeyAction { }; inline for (keys) |k| { - if (cimgui.c.igIsKeyPressed_Bool(k.key, false)) { + if (cimgui.c.ImGui_IsKeyPressed(k.key)) { return k.action; } } @@ -1043,8 +1018,8 @@ fn getKeyAction(self: *Inspector) KeyAction { fn renderTermioWindow(self: *Inspector) void { // Start our window. If we're collapsed we do nothing. - defer cimgui.c.igEnd(); - if (!cimgui.c.igBegin( + defer cimgui.c.ImGui_End(); + if (!cimgui.c.ImGui_Begin( window_termio, null, cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing, @@ -1057,21 +1032,21 @@ fn renderTermioWindow(self: *Inspector) void { "Pause##pause_play" else "Resume##pause_play"; - if (cimgui.c.igButton(pause_play.ptr, .{ .x = 0, .y = 0 })) { + if (cimgui.c.ImGui_Button(pause_play.ptr)) { self.vt_stream.handler.active = !self.vt_stream.handler.active; } - cimgui.c.igSameLine(0, cimgui.c.igGetStyle().*.ItemInnerSpacing.x); - if (cimgui.c.igButton("Filter", .{ .x = 0, .y = 0 })) { - cimgui.c.igOpenPopup_Str( + cimgui.c.ImGui_SameLineEx(0, cimgui.c.ImGui_GetStyle().*.ItemInnerSpacing.x); + if (cimgui.c.ImGui_Button("Filter")) { + cimgui.c.ImGui_OpenPopup( popup_filter, cimgui.c.ImGuiPopupFlags_None, ); } if (!self.vt_events.empty()) { - cimgui.c.igSameLine(0, cimgui.c.igGetStyle().*.ItemInnerSpacing.x); - if (cimgui.c.igButton("Clear", .{ .x = 0, .y = 0 })) { + cimgui.c.ImGui_SameLineEx(0, cimgui.c.ImGui_GetStyle().*.ItemInnerSpacing.x); + if (cimgui.c.ImGui_Button("Clear")) { var it = self.vt_events.iterator(.forward); while (it.next()) |v| v.deinit(self.surface.alloc); self.vt_events.clear(); @@ -1081,44 +1056,36 @@ fn renderTermioWindow(self: *Inspector) void { } } - cimgui.c.igSeparator(); + cimgui.c.ImGui_Separator(); if (self.vt_events.empty()) { - cimgui.c.igText("Waiting for events..."); + cimgui.c.ImGui_Text("Waiting for events..."); break :list; } - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_vt_events", 3, cimgui.c.ImGuiTableFlags_RowBg | cimgui.c.ImGuiTableFlags_Borders, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); - cimgui.c.igTableSetupColumn( + cimgui.c.ImGui_TableSetupColumn( "Seq", cimgui.c.ImGuiTableColumnFlags_WidthFixed, - 0, - 0, ); - cimgui.c.igTableSetupColumn( + cimgui.c.ImGui_TableSetupColumn( "Kind", cimgui.c.ImGuiTableColumnFlags_WidthFixed, - 0, - 0, ); - cimgui.c.igTableSetupColumn( + cimgui.c.ImGui_TableSetupColumn( "Description", cimgui.c.ImGuiTableColumnFlags_WidthStretch, - 0, - 0, ); // Handle keyboard navigation when window is focused - if (cimgui.c.igIsWindowFocused(cimgui.c.ImGuiFocusedFlags_RootAndChildWindows)) { + if (cimgui.c.ImGui_IsWindowFocused(cimgui.c.ImGuiFocusedFlags_RootAndChildWindows)) { const key_pressed = self.getKeyAction(); switch (key_pressed) { @@ -1174,11 +1141,11 @@ fn renderTermioWindow(self: *Inspector) void { var it = self.vt_events.iterator(.reverse); while (it.next()) |ev| { // Need to push an ID so that our selectable is unique. - cimgui.c.igPushID_Ptr(ev); - defer cimgui.c.igPopID(); + cimgui.c.ImGui_PushIDPtr(ev); + defer cimgui.c.ImGui_PopID(); - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableNextColumn(); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableNextColumn(); // Store the previous selection state to detect changes const was_selected = ev.imgui_selected; @@ -1189,11 +1156,10 @@ fn renderTermioWindow(self: *Inspector) void { } // Handle selectable widget - if (cimgui.c.igSelectable_BoolPtr( + if (cimgui.c.ImGui_SelectableBoolPtr( "##select", &ev.imgui_selected, cimgui.c.ImGuiSelectableFlags_SpanAllColumns, - .{ .x = 0, .y = 0 }, )) { // If selection state changed, update keyboard navigation state if (ev.imgui_selected != was_selected) { @@ -1205,40 +1171,38 @@ fn renderTermioWindow(self: *Inspector) void { } } - cimgui.c.igSameLine(0, 0); - cimgui.c.igText("%d", ev.seq); - _ = cimgui.c.igTableNextColumn(); - cimgui.c.igText("%s", @tagName(ev.kind).ptr); - _ = cimgui.c.igTableNextColumn(); - cimgui.c.igText("%s", ev.str.ptr); + cimgui.c.ImGui_SameLine(); + cimgui.c.ImGui_Text("%d", ev.seq); + _ = cimgui.c.ImGui_TableNextColumn(); + cimgui.c.ImGui_Text("%s", @tagName(ev.kind).ptr); + _ = cimgui.c.ImGui_TableNextColumn(); + cimgui.c.ImGui_Text("%s", ev.str.ptr); // If the event is selected, we render info about it. For now - // we put this in the last column because thats the widest and + // we put this in the last column because that's the widest and // imgui has no way to make a column span. if (ev.imgui_selected) { { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "details", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); inspector.cursor.renderInTable( self.surface.renderer_state.terminal, &ev.cursor, ); { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Scroll Region"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Scroll Region"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "T=%d B=%d L=%d R=%d", ev.scrolling_region.top, ev.scrolling_region.bottom, @@ -1253,51 +1217,49 @@ fn renderTermioWindow(self: *Inspector) void { var buf: [256]u8 = undefined; const key = std.fmt.bufPrintZ(&buf, "{s}", .{entry.key_ptr.*}) catch ""; - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableNextColumn(); - cimgui.c.igText("%s", key.ptr); - _ = cimgui.c.igTableNextColumn(); - cimgui.c.igText("%s", entry.value_ptr.ptr); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableNextColumn(); + cimgui.c.ImGui_Text("%s", key.ptr); + _ = cimgui.c.ImGui_TableNextColumn(); + cimgui.c.ImGui_Text("%s", entry.value_ptr.ptr); } } // If this is the selected event and scrolling is needed, scroll to it if (self.need_scroll_to_selected and self.is_keyboard_selection) { - cimgui.c.igSetScrollHereY(0.5); + cimgui.c.ImGui_SetScrollHereY(0.5); self.need_scroll_to_selected = false; } } } } // table - if (cimgui.c.igBeginPopupModal( + if (cimgui.c.ImGui_BeginPopupModal( popup_filter, null, cimgui.c.ImGuiWindowFlags_AlwaysAutoResize, )) { - defer cimgui.c.igEndPopup(); + defer cimgui.c.ImGui_EndPopup(); - cimgui.c.igText("Changed filter settings will only affect future events."); + cimgui.c.ImGui_Text("Changed filter settings will only affect future events."); - cimgui.c.igSeparator(); + cimgui.c.ImGui_Separator(); { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_filter_kind", 3, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); inline for (@typeInfo(terminal.Parser.Action.Tag).@"enum".fields) |field| { const tag = @field(terminal.Parser.Action.Tag, field.name); if (tag == .apc_put or tag == .dcs_put) continue; - _ = cimgui.c.igTableNextColumn(); + _ = cimgui.c.ImGui_TableNextColumn(); var value = !self.vt_stream.handler.filter_exclude.contains(tag); - if (cimgui.c.igCheckbox(@tagName(tag).ptr, &value)) { + if (cimgui.c.ImGui_Checkbox(@tagName(tag).ptr, &value)) { if (value) { self.vt_stream.handler.filter_exclude.remove(tag); } else { @@ -1307,22 +1269,22 @@ fn renderTermioWindow(self: *Inspector) void { } } // Filter kind table - cimgui.c.igSeparator(); + cimgui.c.ImGui_Separator(); - cimgui.c.igText( + cimgui.c.ImGui_Text( "Filter by string. Empty displays all, \"abc\" finds lines\n" ++ "containing \"abc\", \"abc,xyz\" finds lines containing \"abc\"\n" ++ "or \"xyz\", \"-abc\" excludes lines containing \"abc\".", ); _ = cimgui.c.ImGuiTextFilter_Draw( - self.vt_stream.handler.filter_text, + &self.vt_stream.handler.filter_text, "##filter_text", 0, ); - cimgui.c.igSeparator(); - if (cimgui.c.igButton("Close", .{ .x = 0, .y = 0 })) { - cimgui.c.igCloseCurrentPopup(); + cimgui.c.ImGui_Separator(); + if (cimgui.c.ImGui_Button("Close")) { + cimgui.c.ImGui_CloseCurrentPopup(); } } // filter popup } diff --git a/src/inspector/cell.zig b/src/inspector/cell.zig index 2f72556bd..540e044fd 100644 --- a/src/inspector/cell.zig +++ b/src/inspector/cell.zig @@ -1,7 +1,7 @@ const std = @import("std"); const assert = @import("../quirks.zig").inlineAssert; const Allocator = std.mem.Allocator; -const cimgui = @import("cimgui"); +const cimgui = @import("dcimgui"); const terminal = @import("../terminal/main.zig"); /// A cell being inspected. This duplicates much of the data in @@ -55,24 +55,22 @@ pub const Cell = struct { y: usize, ) void { // We have a selected cell, show information about it. - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_cursor", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Grid Position"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Grid Position"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("row=%d col=%d", y, x); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("row=%d col=%d", y, x); } } @@ -82,18 +80,18 @@ pub const Cell = struct { // the single glyph in an image view so it looks _identical_ to the // terminal. codepoint: { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Codepoints"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Codepoints"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - if (cimgui.c.igBeginListBox("##codepoints", .{ .x = 0, .y = 0 })) { - defer cimgui.c.igEndListBox(); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + if (cimgui.c.ImGui_BeginListBox("##codepoints", .{ .x = 0, .y = 0 })) { + defer cimgui.c.ImGui_EndListBox(); if (self.codepoint == 0) { - _ = cimgui.c.igSelectable_Bool("(empty)", false, 0, .{}); + _ = cimgui.c.ImGui_SelectableEx("(empty)", false, 0, .{}); break :codepoint; } @@ -102,42 +100,42 @@ pub const Cell = struct { { const key = std.fmt.bufPrintZ(&buf, "U+{X}", .{self.codepoint}) catch ""; - _ = cimgui.c.igSelectable_Bool(key.ptr, false, 0, .{}); + _ = cimgui.c.ImGui_SelectableEx(key.ptr, false, 0, .{}); } // All extras for (self.cps) |cp| { const key = std.fmt.bufPrintZ(&buf, "U+{X}", .{cp}) catch ""; - _ = cimgui.c.igSelectable_Bool(key.ptr, false, 0, .{}); + _ = cimgui.c.ImGui_SelectableEx(key.ptr, false, 0, .{}); } } } } // Character width property - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Width Property"); - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText(@tagName(self.wide)); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Width Property"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text(@tagName(self.wide)); // If we have a color then we show the color - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Foreground Color"); - _ = cimgui.c.igTableSetColumnIndex(1); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Foreground Color"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); switch (self.style.fg_color) { - .none => cimgui.c.igText("default"), + .none => cimgui.c.ImGui_Text("default"), .palette => |idx| { const rgb = t.colors.palette.current[idx]; - cimgui.c.igValue_Int("Palette", idx); + cimgui.c.ImGui_Text("Palette %d", idx); var color: [3]f32 = .{ @as(f32, @floatFromInt(rgb.r)) / 255, @as(f32, @floatFromInt(rgb.g)) / 255, @as(f32, @floatFromInt(rgb.b)) / 255, }; - _ = cimgui.c.igColorEdit3( + _ = cimgui.c.ImGui_ColorEdit3( "color_fg", &color, cimgui.c.ImGuiColorEditFlags_DisplayHex | @@ -152,7 +150,7 @@ pub const Cell = struct { @as(f32, @floatFromInt(rgb.g)) / 255, @as(f32, @floatFromInt(rgb.b)) / 255, }; - _ = cimgui.c.igColorEdit3( + _ = cimgui.c.ImGui_ColorEdit3( "color_fg", &color, cimgui.c.ImGuiColorEditFlags_DisplayHex | @@ -162,21 +160,21 @@ pub const Cell = struct { }, } - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Background Color"); - _ = cimgui.c.igTableSetColumnIndex(1); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Background Color"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); switch (self.style.bg_color) { - .none => cimgui.c.igText("default"), + .none => cimgui.c.ImGui_Text("default"), .palette => |idx| { const rgb = t.colors.palette.current[idx]; - cimgui.c.igValue_Int("Palette", idx); + cimgui.c.ImGui_Text("Palette %d", idx); var color: [3]f32 = .{ @as(f32, @floatFromInt(rgb.r)) / 255, @as(f32, @floatFromInt(rgb.g)) / 255, @as(f32, @floatFromInt(rgb.b)) / 255, }; - _ = cimgui.c.igColorEdit3( + _ = cimgui.c.ImGui_ColorEdit3( "color_bg", &color, cimgui.c.ImGuiColorEditFlags_DisplayHex | @@ -191,7 +189,7 @@ pub const Cell = struct { @as(f32, @floatFromInt(rgb.g)) / 255, @as(f32, @floatFromInt(rgb.b)) / 255, }; - _ = cimgui.c.igColorEdit3( + _ = cimgui.c.ImGui_ColorEdit3( "color_bg", &color, cimgui.c.ImGuiColorEditFlags_DisplayHex | @@ -209,17 +207,17 @@ pub const Cell = struct { inline for (styles) |style| style: { if (!@field(self.style.flags, style)) break :style; - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText(style.ptr); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text(style.ptr); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("true"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("true"); } } - cimgui.c.igTextDisabled("(Any styles not shown are not currently set)"); + cimgui.c.ImGui_TextDisabled("(Any styles not shown are not currently set)"); } }; diff --git a/src/inspector/cursor.zig b/src/inspector/cursor.zig index 756898252..4f8bfb2e0 100644 --- a/src/inspector/cursor.zig +++ b/src/inspector/cursor.zig @@ -1,4 +1,4 @@ -const cimgui = @import("cimgui"); +const cimgui = @import("dcimgui"); const terminal = @import("../terminal/main.zig"); /// Render cursor information with a table already open. @@ -7,57 +7,57 @@ pub fn renderInTable( cursor: *const terminal.Screen.Cursor, ) void { { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Position (x, y)"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Position (x, y)"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("(%d, %d)", cursor.x, cursor.y); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("(%d, %d)", cursor.x, cursor.y); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Style"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Style"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%s", @tagName(cursor.cursor_style).ptr); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%s", @tagName(cursor.cursor_style).ptr); } } if (cursor.pending_wrap) { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Pending Wrap"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Pending Wrap"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%s", if (cursor.pending_wrap) "true".ptr else "false".ptr); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%s", if (cursor.pending_wrap) "true".ptr else "false".ptr); } } // If we have a color then we show the color - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Foreground Color"); - _ = cimgui.c.igTableSetColumnIndex(1); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Foreground Color"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); switch (cursor.style.fg_color) { - .none => cimgui.c.igText("default"), + .none => cimgui.c.ImGui_Text("default"), .palette => |idx| { const rgb = t.colors.palette.current[idx]; - cimgui.c.igValue_Int("Palette", idx); + cimgui.c.ImGui_Text("Palette %d", idx); var color: [3]f32 = .{ @as(f32, @floatFromInt(rgb.r)) / 255, @as(f32, @floatFromInt(rgb.g)) / 255, @as(f32, @floatFromInt(rgb.b)) / 255, }; - _ = cimgui.c.igColorEdit3( + _ = cimgui.c.ImGui_ColorEdit3( "color_fg", &color, cimgui.c.ImGuiColorEditFlags_DisplayHex | @@ -72,7 +72,7 @@ pub fn renderInTable( @as(f32, @floatFromInt(rgb.g)) / 255, @as(f32, @floatFromInt(rgb.b)) / 255, }; - _ = cimgui.c.igColorEdit3( + _ = cimgui.c.ImGui_ColorEdit3( "color_fg", &color, cimgui.c.ImGuiColorEditFlags_DisplayHex | @@ -82,21 +82,21 @@ pub fn renderInTable( }, } - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Background Color"); - _ = cimgui.c.igTableSetColumnIndex(1); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Background Color"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); switch (cursor.style.bg_color) { - .none => cimgui.c.igText("default"), + .none => cimgui.c.ImGui_Text("default"), .palette => |idx| { const rgb = t.colors.palette.current[idx]; - cimgui.c.igValue_Int("Palette", idx); + cimgui.c.ImGui_Text("Palette %d", idx); var color: [3]f32 = .{ @as(f32, @floatFromInt(rgb.r)) / 255, @as(f32, @floatFromInt(rgb.g)) / 255, @as(f32, @floatFromInt(rgb.b)) / 255, }; - _ = cimgui.c.igColorEdit3( + _ = cimgui.c.ImGui_ColorEdit3( "color_bg", &color, cimgui.c.ImGuiColorEditFlags_DisplayHex | @@ -111,7 +111,7 @@ pub fn renderInTable( @as(f32, @floatFromInt(rgb.g)) / 255, @as(f32, @floatFromInt(rgb.b)) / 255, }; - _ = cimgui.c.igColorEdit3( + _ = cimgui.c.ImGui_ColorEdit3( "color_bg", &color, cimgui.c.ImGuiColorEditFlags_DisplayHex | @@ -129,14 +129,14 @@ pub fn renderInTable( inline for (styles) |style| style: { if (!@field(cursor.style.flags, style)) break :style; - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText(style.ptr); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text(style.ptr); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("true"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("true"); } } } diff --git a/src/inspector/key.zig b/src/inspector/key.zig index dbccb47a8..12d91a107 100644 --- a/src/inspector/key.zig +++ b/src/inspector/key.zig @@ -2,7 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const input = @import("../input.zig"); const CircBuf = @import("../datastruct/main.zig").CircBuf; -const cimgui = @import("cimgui"); +const cimgui = @import("dcimgui"); /// Circular buffer of key events. pub const EventRing = CircBuf(Event, undefined); @@ -13,7 +13,8 @@ pub const Event = struct { event: input.KeyEvent, /// The binding that was triggered as a result of this event. - binding: ?input.Binding.Action = null, + /// Multiple bindings are possible if they are chained. + binding: []const input.Binding.Action = &.{}, /// The data sent to the pty as a result of this keyboard event. /// This is allocated using the inspector allocator. @@ -32,6 +33,7 @@ pub const Event = struct { } pub fn deinit(self: *const Event, alloc: Allocator) void { + alloc.free(self.binding); if (self.event.utf8.len > 0) alloc.free(self.event.utf8); if (self.pty.len > 0) alloc.free(self.pty); } @@ -70,82 +72,96 @@ pub const Event = struct { /// Render this event in the inspector GUI. pub fn render(self: *const Event) void { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "##event", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); - if (self.binding) |binding| { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Triggered Binding"); - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%s", @tagName(binding).ptr); + if (self.binding.len > 0) { + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Triggered Binding"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + + const height: f32 = height: { + const item_count: f32 = @floatFromInt(@min(self.binding.len, 5)); + const padding = cimgui.c.ImGui_GetStyle().*.FramePadding.y * 2; + break :height cimgui.c.ImGui_GetTextLineHeightWithSpacing() * item_count + padding; + }; + if (cimgui.c.ImGui_BeginListBox("##bindings", .{ .x = 0, .y = height })) { + defer cimgui.c.ImGui_EndListBox(); + for (self.binding) |action| { + _ = cimgui.c.ImGui_SelectableEx( + @tagName(action).ptr, + false, + cimgui.c.ImGuiSelectableFlags_None, + .{ .x = 0, .y = 0 }, + ); + } + } } pty: { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Encoding to Pty"); - _ = cimgui.c.igTableSetColumnIndex(1); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Encoding to Pty"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); if (self.pty.len == 0) { - cimgui.c.igTextDisabled("(no data)"); + cimgui.c.ImGui_TextDisabled("(no data)"); break :pty; } self.renderPty() catch { - cimgui.c.igTextDisabled("(error rendering pty data)"); + cimgui.c.ImGui_TextDisabled("(error rendering pty data)"); break :pty; }; } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Action"); - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%s", @tagName(self.event.action).ptr); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Action"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%s", @tagName(self.event.action).ptr); } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Key"); - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%s", @tagName(self.event.key).ptr); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Key"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%s", @tagName(self.event.key).ptr); } if (!self.event.mods.empty()) { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Mods"); - _ = cimgui.c.igTableSetColumnIndex(1); - if (self.event.mods.shift) cimgui.c.igText("shift "); - if (self.event.mods.ctrl) cimgui.c.igText("ctrl "); - if (self.event.mods.alt) cimgui.c.igText("alt "); - if (self.event.mods.super) cimgui.c.igText("super "); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Mods"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + if (self.event.mods.shift) cimgui.c.ImGui_Text("shift "); + if (self.event.mods.ctrl) cimgui.c.ImGui_Text("ctrl "); + if (self.event.mods.alt) cimgui.c.ImGui_Text("alt "); + if (self.event.mods.super) cimgui.c.ImGui_Text("super "); } if (self.event.composing) { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Composing"); - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("true"); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Composing"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("true"); } utf8: { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("UTF-8"); - _ = cimgui.c.igTableSetColumnIndex(1); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("UTF-8"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); if (self.event.utf8.len == 0) { - cimgui.c.igTextDisabled("(empty)"); + cimgui.c.ImGui_TextDisabled("(empty)"); break :utf8; } self.renderUtf8(self.event.utf8) catch { - cimgui.c.igTextDisabled("(error rendering utf-8)"); + cimgui.c.ImGui_TextDisabled("(error rendering utf-8)"); break :utf8; }; } @@ -169,13 +185,11 @@ pub const Event = struct { try writer.writeByte(0); // Render as a textbox - _ = cimgui.c.igInputText( + _ = cimgui.c.ImGui_InputText( "##utf8", &buf, buf_stream.getWritten().len - 1, cimgui.c.ImGuiInputTextFlags_ReadOnly, - null, - null, ); } @@ -205,13 +219,11 @@ pub const Event = struct { try writer.writeByte(0); // Render as a textbox - _ = cimgui.c.igInputText( + _ = cimgui.c.ImGui_InputText( "##pty", &buf, buf_stream.getWritten().len - 1, cimgui.c.ImGuiInputTextFlags_ReadOnly, - null, - null, ); } }; diff --git a/src/inspector/page.zig b/src/inspector/page.zig index 7da469e21..fd9d3bfb4 100644 --- a/src/inspector/page.zig +++ b/src/inspector/page.zig @@ -1,167 +1,161 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const cimgui = @import("cimgui"); +const cimgui = @import("dcimgui"); const terminal = @import("../terminal/main.zig"); const units = @import("units.zig"); pub fn render(page: *const terminal.Page) void { - cimgui.c.igPushID_Ptr(page); - defer cimgui.c.igPopID(); + cimgui.c.ImGui_PushIDPtr(page); + defer cimgui.c.ImGui_PopID(); - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "##page_state", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Memory Size"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Memory Size"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d bytes (%d KiB)", page.memory.len, units.toKibiBytes(page.memory.len)); - cimgui.c.igText("%d VM pages", page.memory.len / std.heap.page_size_min); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d bytes (%d KiB)", page.memory.len, units.toKibiBytes(page.memory.len)); + cimgui.c.ImGui_Text("%d VM pages", page.memory.len / std.heap.page_size_min); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Unique Styles"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Unique Styles"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", page.styles.count()); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", page.styles.count()); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Grapheme Entries"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Grapheme Entries"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", page.graphemeCount()); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", page.graphemeCount()); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Capacity"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Capacity"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + _ = cimgui.c.ImGui_BeginTable( "##capacity", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); const cap = page.capacity; { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Columns"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Columns"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", @as(u32, @intCast(cap.cols))); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", @as(u32, @intCast(cap.cols))); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Rows"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Rows"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", @as(u32, @intCast(cap.rows))); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", @as(u32, @intCast(cap.rows))); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Unique Styles"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Unique Styles"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", @as(u32, @intCast(cap.styles))); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", @as(u32, @intCast(cap.styles))); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Grapheme Bytes"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Grapheme Bytes"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", cap.grapheme_bytes); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", cap.grapheme_bytes); } } } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Size"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Size"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + _ = cimgui.c.ImGui_BeginTable( "##size", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); const size = page.size; { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Columns"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Columns"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", @as(u32, @intCast(size.cols))); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", @as(u32, @intCast(size.cols))); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Rows"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Rows"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", @as(u32, @intCast(size.rows))); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", @as(u32, @intCast(size.rows))); } } } diff --git a/src/inspector/termio.zig b/src/inspector/termio.zig index 7e2b51ee1..934bb6e2d 100644 --- a/src/inspector/termio.zig +++ b/src/inspector/termio.zig @@ -1,6 +1,6 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const cimgui = @import("cimgui"); +const cimgui = @import("dcimgui"); const terminal = @import("../terminal/main.zig"); const CircBuf = @import("../datastruct/main.zig").CircBuf; const Surface = @import("../Surface.zig"); @@ -83,7 +83,7 @@ pub const VTEvent = struct { /// Returns true if the event passes the given filter. pub fn passFilter( self: *const VTEvent, - filter: *cimgui.c.ImGuiTextFilter, + filter: *const cimgui.c.ImGuiTextFilter, ) bool { // Check our main string if (cimgui.c.ImGuiTextFilter_PassFilter( @@ -286,18 +286,19 @@ pub const VTEvent = struct { ), else => switch (Value) { - u8, u16 => try md.put( - key, - try std.fmt.allocPrintSentinel(alloc, "{}", .{value}, 0), - ), - []const u8, [:0]const u8, => try md.put(key, try alloc.dupeZ(u8, value)), - else => |T| { - @compileLog(T); - @compileError("unsupported type, see log"); + else => |T| switch (@typeInfo(T)) { + .int => try md.put( + key, + try std.fmt.allocPrintSentinel(alloc, "{}", .{value}, 0), + ), + else => { + @compileLog(T); + @compileError("unsupported type, see log"); + }, }, }, } @@ -318,19 +319,18 @@ pub const VTHandler = struct { /// Exclude certain actions by tag. filter_exclude: ActionTagSet = .initMany(&.{.print}), - filter_text: *cimgui.c.ImGuiTextFilter, + filter_text: cimgui.c.ImGuiTextFilter = .{}, const ActionTagSet = std.EnumSet(terminal.Parser.Action.Tag); pub fn init(surface: *Surface) VTHandler { return .{ .surface = surface, - .filter_text = cimgui.c.ImGuiTextFilter_ImGuiTextFilter(""), }; } pub fn deinit(self: *VTHandler) void { - cimgui.c.ImGuiTextFilter_destroy(self.filter_text); + _ = self; } pub fn vt( @@ -371,7 +371,7 @@ pub const VTHandler = struct { errdefer ev.deinit(alloc); // Check if the event passes the filter - if (!ev.passFilter(self.filter_text)) { + if (!ev.passFilter(&self.filter_text)) { ev.deinit(alloc); return true; } diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index 261e0ad7d..531a06461 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -118,19 +118,17 @@ fn logFn( comptime format: []const u8, args: anytype, ) void { - // Stuff we can do before the lock - const level_txt = comptime level.asText(); - const prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; - - // Lock so we are thread-safe - std.debug.lockStdErr(); - defer std.debug.unlockStdErr(); - // On Mac, we use unified logging. To view this: // // sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"' // - if (builtin.target.os.tag.isDarwin()) { + // macOS logging is thread safe so no need for locks/mutexes + macos: { + if (comptime !builtin.target.os.tag.isDarwin()) break :macos; + if (!state.logging.macos) break :macos; + + const prefix = if (scope == .default) "" else @tagName(scope) ++ ": "; + // Convert our levels to Mac levels const mac_level: macos.os.LogType = switch (level) { .debug => .debug, @@ -143,26 +141,35 @@ fn logFn( // but we shouldn't be logging too much. const logger = macos.os.Log.create(build_config.bundle_id, @tagName(scope)); defer logger.release(); - logger.log(std.heap.c_allocator, mac_level, format, args); + logger.log(std.heap.c_allocator, mac_level, prefix ++ format, args); } - switch (state.logging) { - .disabled => {}, + stderr: { + // don't log debug messages to stderr unless we are a debug build + if (comptime builtin.mode != .Debug and level == .debug) break :stderr; - .stderr => { - // Always try default to send to stderr - var buffer: [1024]u8 = undefined; - var stderr = std.fs.File.stderr().writer(&buffer); - const writer = &stderr.interface; - nosuspend writer.print(level_txt ++ prefix ++ format ++ "\n", args) catch return; - // TODO: Do we want to use flushless stderr in the future? - writer.flush() catch {}; - }, + // skip if we are not logging to stderr + if (!state.logging.stderr) break :stderr; + + // Lock so we are thread-safe + var buf: [64]u8 = undefined; + const stderr = std.debug.lockStderrWriter(&buf); + defer std.debug.unlockStderrWriter(); + + const level_txt = comptime level.asText(); + const prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; + nosuspend stderr.print(level_txt ++ prefix ++ format ++ "\n", args) catch break :stderr; + nosuspend stderr.flush() catch break :stderr; } } pub const std_options: std.Options = .{ // Our log level is always at least info in every build mode. + // + // Note, we don't lower this to debug even with conditional logging + // via GHOSTTY_LOG because our debug logs are very expensive to + // calculate and we want to make sure they're optimized out in + // builds. .log_level = switch (builtin.mode) { .Debug => .debug, else => .info, @@ -183,6 +190,7 @@ test { _ = @import("surface_mouse.zig"); // Libraries + _ = @import("tripwire.zig"); _ = @import("benchmark/main.zig"); _ = @import("crash/main.zig"); _ = @import("datastruct/main.zig"); diff --git a/src/os/mach.zig b/src/os/mach.zig new file mode 100644 index 000000000..4477bd128 --- /dev/null +++ b/src/os/mach.zig @@ -0,0 +1,150 @@ +const std = @import("std"); +const assert = @import("../quirks.zig").inlineAssert; +const mem = std.mem; +const Allocator = std.mem.Allocator; + +/// macOS virtual memory tags for use with mach_vm_map/mach_vm_allocate. +/// These identify memory regions in tools like vmmap and Instruments. +pub const VMTag = enum(u8) { + application_specific_1 = 240, + application_specific_2 = 241, + application_specific_3 = 242, + application_specific_4 = 243, + application_specific_5 = 244, + application_specific_6 = 245, + application_specific_7 = 246, + application_specific_8 = 247, + application_specific_9 = 248, + application_specific_10 = 249, + application_specific_11 = 250, + application_specific_12 = 251, + application_specific_13 = 252, + application_specific_14 = 253, + application_specific_15 = 254, + application_specific_16 = 255, + + // We ignore the rest because we never realistic set them. + _, + + /// Converts the tag to the format expected by mach_vm_map/mach_vm_allocate. + /// Equivalent to C macro: VM_MAKE_TAG(tag) + pub fn make(self: VMTag) i32 { + return @bitCast(@as(u32, @intFromEnum(self)) << 24); + } +}; + +/// Creates a page allocator that tags all allocated memory with the given +/// VMTag. +pub fn taggedPageAllocator(tag: VMTag) Allocator { + return .{ + // We smuggle the tag in as the context pointer. + .ptr = @ptrFromInt(@as(usize, @intFromEnum(tag))), + .vtable = &TaggedPageAllocator.vtable, + }; +} + +/// This is based heavily on the Zig 0.15.2 PageAllocator implementation, +/// with only the posix implementation. Zig 0.15.2 is MIT licensed. +const TaggedPageAllocator = struct { + pub const vtable: Allocator.VTable = .{ + .alloc = alloc, + .resize = resize, + .remap = remap, + .free = free, + }; + + fn alloc(context: *anyopaque, n: usize, alignment: mem.Alignment, ra: usize) ?[*]u8 { + _ = ra; + assert(n > 0); + const tag: VMTag = @enumFromInt(@as(u8, @truncate(@intFromPtr(context)))); + return map(n, alignment, tag); + } + + fn resize(context: *anyopaque, memory: []u8, alignment: mem.Alignment, new_len: usize, return_address: usize) bool { + _ = context; + _ = alignment; + _ = return_address; + return realloc(memory, new_len, false) != null; + } + + fn remap(context: *anyopaque, memory: []u8, alignment: mem.Alignment, new_len: usize, return_address: usize) ?[*]u8 { + _ = context; + _ = alignment; + _ = return_address; + return realloc(memory, new_len, true); + } + + fn free(context: *anyopaque, memory: []u8, alignment: mem.Alignment, return_address: usize) void { + _ = context; + _ = alignment; + _ = return_address; + return unmap(@alignCast(memory)); + } + + pub fn map(n: usize, alignment: mem.Alignment, tag: VMTag) ?[*]u8 { + const page_size = std.heap.pageSize(); + if (n >= std.math.maxInt(usize) - page_size) return null; + const alignment_bytes = alignment.toByteUnits(); + + const aligned_len = mem.alignForward(usize, n, page_size); + const max_drop_len = alignment_bytes - @min(alignment_bytes, page_size); + const overalloc_len = if (max_drop_len <= aligned_len - n) + aligned_len + else + mem.alignForward(usize, aligned_len + max_drop_len, page_size); + const hint = @atomicLoad(@TypeOf(std.heap.next_mmap_addr_hint), &std.heap.next_mmap_addr_hint, .unordered); + const slice = std.posix.mmap( + hint, + overalloc_len, + std.posix.PROT.READ | std.posix.PROT.WRITE, + .{ .TYPE = .PRIVATE, .ANONYMOUS = true }, + tag.make(), + 0, + ) catch return null; + const result_ptr = mem.alignPointer(slice.ptr, alignment_bytes) orelse return null; + // Unmap the extra bytes that were only requested in order to guarantee + // that the range of memory we were provided had a proper alignment in it + // somewhere. The extra bytes could be at the beginning, or end, or both. + const drop_len = result_ptr - slice.ptr; + if (drop_len != 0) std.posix.munmap(slice[0..drop_len]); + const remaining_len = overalloc_len - drop_len; + if (remaining_len > aligned_len) std.posix.munmap(@alignCast(result_ptr[aligned_len..remaining_len])); + const new_hint: [*]align(std.heap.page_size_min) u8 = @alignCast(result_ptr + aligned_len); + _ = @cmpxchgStrong(@TypeOf(std.heap.next_mmap_addr_hint), &std.heap.next_mmap_addr_hint, hint, new_hint, .monotonic, .monotonic); + return result_ptr; + } + + pub fn unmap(memory: []align(std.heap.page_size_min) u8) void { + const page_aligned_len = mem.alignForward(usize, memory.len, std.heap.pageSize()); + std.posix.munmap(memory.ptr[0..page_aligned_len]); + } + + pub fn realloc(uncasted_memory: []u8, new_len: usize, may_move: bool) ?[*]u8 { + const memory: []align(std.heap.page_size_min) u8 = @alignCast(uncasted_memory); + const page_size = std.heap.pageSize(); + const new_size_aligned = mem.alignForward(usize, new_len, page_size); + + const page_aligned_len = mem.alignForward(usize, memory.len, page_size); + if (new_size_aligned == page_aligned_len) + return memory.ptr; + + if (std.posix.MREMAP != void) { + // TODO: if the next_mmap_addr_hint is within the remapped range, update it + const new_memory = std.posix.mremap(memory.ptr, memory.len, new_len, .{ .MAYMOVE = may_move }, null) catch return null; + return new_memory.ptr; + } + + if (new_size_aligned < page_aligned_len) { + const ptr = memory.ptr + new_size_aligned; + // TODO: if the next_mmap_addr_hint is within the unmapped range, update it + std.posix.munmap(@alignCast(ptr[0 .. page_aligned_len - new_size_aligned])); + return memory.ptr; + } + + return null; + } +}; + +test "VMTag.make" { + try std.testing.expectEqual(@as(i32, @bitCast(@as(u32, 240) << 24)), VMTag.application_specific_1.make()); +} diff --git a/src/os/main.zig b/src/os/main.zig index c105f6143..2aadabac5 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -23,6 +23,7 @@ pub const args = @import("args.zig"); pub const cgroup = @import("cgroup.zig"); pub const hostname = @import("hostname.zig"); pub const i18n = @import("i18n.zig"); +pub const mach = @import("mach.zig"); pub const path = @import("path.zig"); pub const passwd = @import("passwd.zig"); pub const xdg = @import("xdg.zig"); @@ -73,5 +74,8 @@ test { if (comptime builtin.os.tag == .linux) { _ = kernel_info; + } else if (comptime builtin.os.tag.isDarwin()) { + _ = mach; + _ = macos; } } diff --git a/src/os/shell.zig b/src/os/shell.zig index 9fce3e385..fe8f1b2fd 100644 --- a/src/os/shell.zig +++ b/src/os/shell.zig @@ -1,7 +1,84 @@ const std = @import("std"); const testing = std.testing; +const Allocator = std.mem.Allocator; const Writer = std.Io.Writer; +/// Builder for constructing space-separated shell command strings. +/// Uses a caller-provided allocator (typically with stackFallback). +pub const ShellCommandBuilder = struct { + buffer: std.Io.Writer.Allocating, + + pub fn init(allocator: Allocator) ShellCommandBuilder { + return .{ .buffer = .init(allocator) }; + } + + pub fn deinit(self: *ShellCommandBuilder) void { + self.buffer.deinit(); + } + + /// Append an argument to the command with automatic space separation. + pub fn appendArg(self: *ShellCommandBuilder, arg: []const u8) (Allocator.Error || Writer.Error)!void { + if (arg.len == 0) return; + if (self.buffer.written().len > 0) { + try self.buffer.writer.writeByte(' '); + } + try self.buffer.writer.writeAll(arg); + } + + /// Get the final null-terminated command string, transferring ownership to caller. + /// Calling deinit() after this is safe but unnecessary. + pub fn toOwnedSlice(self: *ShellCommandBuilder) Allocator.Error![:0]const u8 { + return try self.buffer.toOwnedSliceSentinel(0); + } +}; + +test ShellCommandBuilder { + // Empty command + { + var cmd = ShellCommandBuilder.init(testing.allocator); + defer cmd.deinit(); + try testing.expectEqualStrings("", cmd.buffer.written()); + } + + // Single arg + { + var cmd = ShellCommandBuilder.init(testing.allocator); + defer cmd.deinit(); + try cmd.appendArg("bash"); + try testing.expectEqualStrings("bash", cmd.buffer.written()); + } + + // Multiple args + { + var cmd = ShellCommandBuilder.init(testing.allocator); + defer cmd.deinit(); + try cmd.appendArg("bash"); + try cmd.appendArg("--posix"); + try cmd.appendArg("-l"); + try testing.expectEqualStrings("bash --posix -l", cmd.buffer.written()); + } + + // Empty arg + { + var cmd = ShellCommandBuilder.init(testing.allocator); + defer cmd.deinit(); + try cmd.appendArg("bash"); + try cmd.appendArg(""); + try testing.expectEqualStrings("bash", cmd.buffer.written()); + } + + // toOwnedSlice + { + var cmd = ShellCommandBuilder.init(testing.allocator); + try cmd.appendArg("bash"); + try cmd.appendArg("--posix"); + const result = try cmd.toOwnedSlice(); + defer testing.allocator.free(result); + try testing.expectEqualStrings("bash --posix", result); + try testing.expectEqual(@as(u8, 0), result[result.len]); + } +} + /// Writer that escapes characters that shells treat specially to reduce the /// risk of injection attacks or other such weirdness. Specifically excludes /// linefeeds so that they can be used to delineate lists of file paths. diff --git a/src/os/string_encoding.zig b/src/os/string_encoding.zig index 162023ad2..042001ea7 100644 --- a/src/os/string_encoding.zig +++ b/src/os/string_encoding.zig @@ -265,3 +265,16 @@ test "percent 7" { @memcpy(&src, s); try std.testing.expectError(error.DecodeError, urlPercentDecode(&src)); } + +/// Is the given character valid in URI percent encoding? +fn isValidChar(c: u8) bool { + return switch (c) { + ' ', ';', '=' => false, + else => return std.ascii.isPrint(c), + }; +} + +/// Write data to the writer after URI percent encoding. +pub fn urlPercentEncode(writer: *std.Io.Writer, data: []const u8) std.Io.Writer.Error!void { + try std.Uri.Component.percentEncode(writer, data, isValidChar); +} diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 168f54c2b..6c7432d21 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -55,6 +55,9 @@ blending: configpkg.Config.AlphaBlending, /// the "shared" storage mode, instead we have to use the "managed" mode. default_storage_mode: mtl.MTLResourceOptions.StorageMode, +/// The maximum 2D texture width and height supported by the device. +max_texture_size: u32, + /// We start an AutoreleasePool before `drawFrame` and end it afterwards. autorelease_pool: ?*objc.AutoreleasePool = null, @@ -72,8 +75,17 @@ pub fn init(alloc: Allocator, opts: rendererpkg.Options) !Metal { const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); errdefer queue.release(); - const default_storage_mode: mtl.MTLResourceOptions.StorageMode = - if (device.getProperty(bool, "hasUnifiedMemory")) .shared else .managed; + // Grab metadata about the device. + const default_storage_mode: mtl.MTLResourceOptions.StorageMode = switch (comptime builtin.os.tag) { + // manage mode is not supported by iOS + .ios => .shared, + else => if (device.getProperty(bool, "hasUnifiedMemory")) .shared else .managed, + }; + const max_texture_size = queryMaxTextureSize(device); + log.debug( + "device properties default_storage_mode={} max_texture_size={}", + .{ default_storage_mode, max_texture_size }, + ); const ViewInfo = struct { view: objc.Object, @@ -114,7 +126,8 @@ pub fn init(alloc: Allocator, opts: rendererpkg.Options) !Metal { }, .ios => { - info.view.msgSend(void, objc.sel("addSublayer"), .{layer.layer.value}); + const view_layer = objc.Object.fromId(info.view.getProperty(?*anyopaque, "layer")); + view_layer.msgSend(void, objc.sel("addSublayer:"), .{layer.layer.value}); }, else => @compileError("unsupported target for Metal"), @@ -138,6 +151,7 @@ pub fn init(alloc: Allocator, opts: rendererpkg.Options) !Metal { .queue = queue, .blending = opts.config.blending, .default_storage_mode = default_storage_mode, + .max_texture_size = max_texture_size, }; } @@ -202,9 +216,19 @@ pub fn initShaders( pub fn surfaceSize(self: *const Metal) !struct { width: u32, height: u32 } { const bounds = self.layer.layer.getProperty(graphics.Rect, "bounds"); const scale = self.layer.layer.getProperty(f64, "contentsScale"); + + // We need to clamp our runtime surface size to the maximum + // possible texture size since we can't create a screen buffer (texture) + // larger than that. return .{ - .width = @intFromFloat(bounds.size.width * scale), - .height = @intFromFloat(bounds.size.height * scale), + .width = @min( + @as(u32, @intFromFloat(bounds.size.width * scale)), + self.max_texture_size, + ), + .height = @min( + @as(u32, @intFromFloat(bounds.size.height * scale)), + self.max_texture_size, + ), }; } @@ -412,3 +436,23 @@ fn chooseDevice() error{NoMetalDevice}!objc.Object { const device = chosen_device orelse return error.NoMetalDevice; return device.retain(); } + +/// Determines the maximum 2D texture size supported by the device. +/// We need to clamp our frame size to this if it's larger. +fn queryMaxTextureSize(device: objc.Object) u32 { + // https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf + + if (device.msgSend( + bool, + objc.sel("supportsFamily:"), + .{mtl.MTLGPUFamily.apple10}, + )) return 32768; + + if (device.msgSend( + bool, + objc.sel("supportsFamily:"), + .{mtl.MTLGPUFamily.apple3}, + )) return 16384; + + return 8192; +} diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index da577f957..4b01da0c5 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -137,15 +137,7 @@ fn prepareContext(getProcAddress: anytype) !void { errdefer gl.glad.unload(); log.info("loaded OpenGL {}.{}", .{ major, minor }); - // Enable debug output for the context. - try gl.enable(gl.c.GL_DEBUG_OUTPUT); - - // Register our debug message callback with the OpenGL context. - gl.glad.context.DebugMessageCallback.?(glDebugMessageCallback, null); - - // Enable SRGB framebuffer for linear blending support. - try gl.enable(gl.c.GL_FRAMEBUFFER_SRGB); - + // Need to check version before trying to enable it if (major < MIN_VERSION_MAJOR or (major == MIN_VERSION_MAJOR and minor < MIN_VERSION_MINOR)) { @@ -155,6 +147,15 @@ fn prepareContext(getProcAddress: anytype) !void { ); return error.OpenGLOutdated; } + + // Enable debug output for the context. + try gl.enable(gl.c.GL_DEBUG_OUTPUT); + + // Register our debug message callback with the OpenGL context. + gl.glad.context.DebugMessageCallback.?(glDebugMessageCallback, null); + + // Enable SRGB framebuffer for linear blending support. + try gl.enable(gl.c.GL_FRAMEBUFFER_SRGB); } /// This is called early right after surface creation. diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index 9e5802ea5..5ea5b7ab0 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -90,7 +90,6 @@ pub const Contents = struct { const bg_cells = try alloc.alloc(shaderpkg.CellBg, cell_count); errdefer alloc.free(bg_cells); - @memset(bg_cells, .{ 0, 0, 0, 0 }); // The foreground lists can hold 3 types of items: @@ -106,32 +105,28 @@ pub const Contents = struct { // We have size.rows + 2 lists because indexes 0 and size.rows - 1 are // used for special lists containing the cursor cell which need to // be first and last in the buffer, respectively. - var fg_rows = try ArrayListCollection(shaderpkg.CellText).init( + var fg_rows: ArrayListCollection(shaderpkg.CellText) = try .init( alloc, size.rows + 2, size.columns * 3, ); errdefer fg_rows.deinit(alloc); - alloc.free(self.bg_cells); - self.fg_rows.deinit(alloc); - - self.bg_cells = bg_cells; - self.fg_rows = fg_rows; - // We don't need 3*cols worth of cells for the cursor lists, so we can // replace them with smaller lists. This is technically a tiny bit of // extra work but resize is not a hot function so it's worth it to not // waste the memory. - self.fg_rows.lists[0].deinit(alloc); - self.fg_rows.lists[0] = try std.ArrayListUnmanaged( - shaderpkg.CellText, - ).initCapacity(alloc, 1); + fg_rows.lists[0].deinit(alloc); + fg_rows.lists[0] = try .initCapacity(alloc, 1); + fg_rows.lists[size.rows + 1].deinit(alloc); + fg_rows.lists[size.rows + 1] = try .initCapacity(alloc, 1); - self.fg_rows.lists[size.rows + 1].deinit(alloc); - self.fg_rows.lists[size.rows + 1] = try std.ArrayListUnmanaged( - shaderpkg.CellText, - ).initCapacity(alloc, 1); + // Perform the swap, no going back from here. + errdefer comptime unreachable; + alloc.free(self.bg_cells); + self.fg_rows.deinit(alloc); + self.bg_cells = bg_cells; + self.fg_rows = fg_rows; } /// Reset the cell contents to an empty state without resizing. diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 8c55da602..e75171721 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -116,6 +116,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// True if the window is focused focused: bool, + /// Flag to indicate that our focus state changed for custom + /// shaders to update their state. + custom_shader_focused_changed: bool = false, + /// The most recent scrollbar state. We use this as a cache to /// determine if we need to notify the apprt that there was a /// scrollbar change. @@ -227,7 +231,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { search_match, search_match_selected, }; - /// Swap chain which maintains multiple copies of the state needed to /// render a frame, so that we can start building the next frame while /// the previous frame is still being processed on the GPU. @@ -561,6 +564,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { vsync: bool, colorspace: configpkg.Config.WindowColorspace, blending: configpkg.Config.AlphaBlending, + background_blur: configpkg.Config.BackgroundBlur, pub fn init( alloc_gpa: Allocator, @@ -633,6 +637,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .vsync = config.@"window-vsync", .colorspace = config.@"window-colorspace", .blending = config.@"alpha-blending", + .background_blur = config.@"background-blur", .arena = arena, }; } @@ -716,6 +721,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type { options.config.background.r, options.config.background.g, options.config.background.b, + // Note that if we're on macOS with glass effects + // we'll disable background opacity but we handle + // that in updateFrame. @intFromFloat(@round(options.config.background_opacity * 255.0)), }, .bools = .{ @@ -741,6 +749,15 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .current_cursor_color = @splat(0), .previous_cursor_color = @splat(0), .cursor_change_time = 0, + .time_focus = 0, + .focus = 1, // assume focused initially + .palette = @splat(@splat(0)), + .background_color = @splat(0), + .foreground_color = @splat(0), + .cursor_color = @splat(0), + .cursor_text = @splat(0), + .selection_background_color = @splat(0), + .selection_foreground_color = @splat(0), }, .bg_image_buffer = undefined, @@ -1003,8 +1020,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// /// Must be called on the render thread. pub fn setFocus(self: *Self, focus: bool) !void { + assert(self.focused != focus); + self.focused = focus; + // Flag that we need to update our custom shaders + self.custom_shader_focused_changed = true; + // If we're not focused, then we want to stop the display link // because it is a waste of resources and we can move to pure // change-driven updates. @@ -1095,7 +1117,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self: *Self, state: *renderer.State, cursor_blink_visible: bool, - ) !void { + ) Allocator.Error!void { // We fully deinit and reset the terminal state every so often // so that a particularly large terminal state doesn't cause // the renderer to hold on to retained memory. @@ -1171,7 +1193,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { if (state.terminal.screens.active.kitty_images.dirty or self.image_virtual) { - try self.prepKittyGraphics(state.terminal); + self.prepKittyGraphics(state.terminal); } // Get our OSC8 links we're hovering if we have a mouse. @@ -1260,26 +1282,34 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } } - // Build our GPU cells - try self.rebuildCells( - critical.preedit, - renderer.cursorStyle(&self.terminal_state, .{ - .preedit = critical.preedit != null, - .focused = self.focused, - .blink_visible = cursor_blink_visible, - }), - &critical.links, - ); + // From this point forward no more errors. + errdefer comptime unreachable; - // Notify our shaper we're done for the frame. For some shapers, - // such as CoreText, this triggers off-thread cleanup logic. - self.font_shaper.endFrame(); + // Reset our dirty state after updating. + defer self.terminal_state.dirty = .false; - // Acquire the draw mutex because we're modifying state here. + // Acquire the draw mutex for all remaining state updates. { self.draw_mutex.lock(); defer self.draw_mutex.unlock(); + // Build our GPU cells + self.rebuildCells( + critical.preedit, + renderer.cursorStyle(&self.terminal_state, .{ + .preedit = critical.preedit != null, + .focused = self.focused, + .blink_visible = cursor_blink_visible, + }), + &critical.links, + ) catch |err| { + // This means we weren't able to allocate our buffer + // to update the cells. In this case, we continue with + // our old buffer (frozen contents) and log it. + comptime assert(@TypeOf(err) == error{OutOfMemory}); + log.warn("error rebuilding GPU cells err={}", .{err}); + }; + // The scrollbar is only emitted during draws so we also // check the scrollbar cache here and update if needed. // This is pretty fast. @@ -1295,7 +1325,25 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self.terminal_state.colors.background.b, @intFromFloat(@round(self.config.background_opacity * 255.0)), }; + + // If we're on macOS and have glass styles, we remove + // the background opacity because the glass effect handles + // it. + if (comptime builtin.os.tag == .macos) switch (self.config.background_blur) { + .@"macos-glass-regular", + .@"macos-glass-clear", + => self.uniforms.bg_color[3] = 0, + + else => {}, + }; + + // Update custom shader uniforms that depend on terminal state. + self.updateCustomShaderUniformsFromState(); } + + // Notify our shaper we're done for the frame. For some shapers, + // such as CoreText, this triggers off-thread cleanup logic. + self.font_shaper.endFrame(); } /// Draw the frame to the screen. @@ -1417,8 +1465,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Upload the background image to the GPU as necessary. try self.uploadBackgroundImage(); - // Update custom shader uniforms if necessary. - try self.updateCustomShaderUniforms(); + // Update per-frame custom shader uniforms. + try self.updateCustomShaderUniformsForFrame(); // Setup our frame data try frame.uniforms.sync(&.{self.uniforms}); @@ -1664,7 +1712,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { fn prepKittyGraphics( self: *Self, t: *terminal.Terminal, - ) !void { + ) void { self.draw_mutex.lock(); defer self.draw_mutex.unlock(); @@ -1728,16 +1776,32 @@ pub fn Renderer(comptime GraphicsAPI: type) type { continue; }; - try self.prepKittyPlacement(t, top_y, bot_y, &image, p); + self.prepKittyPlacement( + t, + top_y, + bot_y, + &image, + p, + ) catch |err| { + // For errors we log and continue. We try to place + // other placements even if one fails. + log.warn("error preparing kitty placement err={}", .{err}); + }; } // If we have virtual placements then we need to scan for placeholders. if (self.image_virtual) { var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot); - while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement( - t, - &virtual_p, - ); + while (v_it.next()) |virtual_p| { + self.prepKittyVirtualPlacement( + t, + &virtual_p, + ) catch |err| { + // For errors we log and continue. We try to place + // other placements even if one fails. + log.warn("error preparing kitty placement err={}", .{err}); + }; + } } // Sort the placements by their Z value. @@ -1785,7 +1849,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self: *Self, t: *terminal.Terminal, p: *const terminal.kitty.graphics.unicode.Placement, - ) !void { + ) PrepKittyImageError!void { const storage = &t.screens.active.kitty_images; const image = storage.imageById(p.image_id) orelse { log.warn( @@ -1846,7 +1910,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { bot_y: u32, image: *const terminal.kitty.graphics.Image, p: *const terminal.kitty.graphics.ImageStorage.Placement, - ) !void { + ) PrepKittyImageError!void { // Get the rect for the placement. If this placement doesn't have // a rect then its virtual or something so skip it. const rect = p.rect(image.*, t) orelse return; @@ -1902,12 +1966,17 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } } + const PrepKittyImageError = error{ + OutOfMemory, + ImageConversionError, + }; + /// Prepare the provided image for upload to the GPU by copying its /// data with our allocator and setting it to the pending state. fn prepKittyImage( self: *Self, image: *const terminal.kitty.graphics.Image, - ) !void { + ) PrepKittyImageError!void { // If this image exists and its transmit time is the same we assume // it is the identical image so we don't need to send it to the GPU. const gop = try self.images.getOrPut(self.alloc, image.id); @@ -1918,39 +1987,60 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } // Copy the data into the pending state. - const data = try self.alloc.dupe(u8, image.data); - errdefer self.alloc.free(data); + const data = if (self.alloc.dupe( + u8, + image.data, + )) |v| v else |_| { + if (!gop.found_existing) { + // If this is a new entry we can just remove it since it + // was never sent to the GPU. + _ = self.images.remove(image.id); + } else { + // If this was an existing entry, it is invalid and + // we must unload it. + gop.value_ptr.image.markForUnload(); + } + + return error.OutOfMemory; + }; + // Note: we don't need to errdefer free the data because it is + // put into the map immediately below and our errdefer to + // handle our map state will fix this up. // Store it in the map - const pending: Image.Pending = .{ - .width = image.width, - .height = image.height, - .pixel_format = switch (image.format) { - .gray => .gray, - .gray_alpha => .gray_alpha, - .rgb => .rgb, - .rgba => .rgba, - .png => unreachable, // should be decoded by now + const new_image: Image = .{ + .pending = .{ + .width = image.width, + .height = image.height, + .pixel_format = switch (image.format) { + .gray => .gray, + .gray_alpha => .gray_alpha, + .rgb => .rgb, + .rgba => .rgba, + .png => unreachable, // should be decoded by now + }, + .data = data.ptr, }, - .data = data.ptr, }; - - const new_image: Image = .{ .pending = pending }; - if (!gop.found_existing) { gop.value_ptr.* = .{ .image = new_image, .transmit_time = undefined, }; } else { - try gop.value_ptr.image.markForReplace( + gop.value_ptr.image.markForReplace( self.alloc, new_image, ); } - try gop.value_ptr.image.prepForUpload(self.alloc); + // If any error happens, we unload the image and it is invalid. + errdefer gop.value_ptr.image.markForUnload(); + gop.value_ptr.image.prepForUpload(self.alloc) catch |err| { + log.warn("error preparing kitty image for upload err={}", .{err}); + return error.ImageConversionError; + }; gop.value_ptr.transmit_time = image.transmit_time; } @@ -2042,7 +2132,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // If we have an existing background image, replace it. // Otherwise, set this as our background image directly. if (self.bg_image) |*img| { - try img.markForReplace(self.alloc, image); + img.markForReplace(self.alloc, image); } else { self.bg_image = image; } @@ -2083,7 +2173,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } // We also need to reset the shaper cache so shaper info - // from the previous font isn't re-used for the new font. + // from the previous font isn't reused for the new font. const font_shaper_cache = font.ShaperCache.init(); self.font_shaper_cache.deinit(self.alloc); self.font_shaper_cache = font_shaper_cache; @@ -2232,13 +2322,98 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self.bg_image_buffer_modified +%= 1; } - /// Update uniforms for the custom shaders, if necessary. + /// Update custom shader uniforms that depend on terminal state. /// - /// This should be called exactly once per frame, inside `drawFrame`. - fn updateCustomShaderUniforms(self: *Self) !void { + /// This should be called in `updateFrame` when terminal state changes. + fn updateCustomShaderUniformsFromState(self: *Self) void { // We only need to do this if we have custom shaders. if (!self.has_custom_shaders) return; + // Only update when terminal state is dirty. + if (self.terminal_state.dirty == .false) return; + + const colors: *const terminal.RenderState.Colors = &self.terminal_state.colors; + + // 256-color palette + for (colors.palette, 0..) |color, i| { + self.custom_shader_uniforms.palette[i] = .{ + @as(f32, @floatFromInt(color.r)) / 255.0, + @as(f32, @floatFromInt(color.g)) / 255.0, + @as(f32, @floatFromInt(color.b)) / 255.0, + 1.0, + }; + } + + // Background color + self.custom_shader_uniforms.background_color = .{ + @as(f32, @floatFromInt(colors.background.r)) / 255.0, + @as(f32, @floatFromInt(colors.background.g)) / 255.0, + @as(f32, @floatFromInt(colors.background.b)) / 255.0, + 1.0, + }; + + // Foreground color + self.custom_shader_uniforms.foreground_color = .{ + @as(f32, @floatFromInt(colors.foreground.r)) / 255.0, + @as(f32, @floatFromInt(colors.foreground.g)) / 255.0, + @as(f32, @floatFromInt(colors.foreground.b)) / 255.0, + 1.0, + }; + + // Cursor color + if (colors.cursor) |cursor_color| { + self.custom_shader_uniforms.cursor_color = .{ + @as(f32, @floatFromInt(cursor_color.r)) / 255.0, + @as(f32, @floatFromInt(cursor_color.g)) / 255.0, + @as(f32, @floatFromInt(cursor_color.b)) / 255.0, + 1.0, + }; + } + + // NOTE: the following could be optimized to follow a change in + // config for a slight optimization however this is only 12 bytes + // each being updated and likely isn't a cause for concern + + // Cursor text color + if (self.config.cursor_text) |cursor_text| { + self.custom_shader_uniforms.cursor_text = .{ + @as(f32, @floatFromInt(cursor_text.color.r)) / 255.0, + @as(f32, @floatFromInt(cursor_text.color.g)) / 255.0, + @as(f32, @floatFromInt(cursor_text.color.b)) / 255.0, + 1.0, + }; + } + + // Selection background color + if (self.config.selection_background) |selection_bg| { + self.custom_shader_uniforms.selection_background_color = .{ + @as(f32, @floatFromInt(selection_bg.color.r)) / 255.0, + @as(f32, @floatFromInt(selection_bg.color.g)) / 255.0, + @as(f32, @floatFromInt(selection_bg.color.b)) / 255.0, + 1.0, + }; + } + + // Selection foreground color + if (self.config.selection_foreground) |selection_fg| { + self.custom_shader_uniforms.selection_foreground_color = .{ + @as(f32, @floatFromInt(selection_fg.color.r)) / 255.0, + @as(f32, @floatFromInt(selection_fg.color.g)) / 255.0, + @as(f32, @floatFromInt(selection_fg.color.b)) / 255.0, + 1.0, + }; + } + } + + /// Update per-frame custom shader uniforms. + /// + /// This should be called exactly once per frame, inside `drawFrame`. + fn updateCustomShaderUniformsForFrame(self: *Self) !void { + // We only need to do this if we have custom shaders. + if (!self.has_custom_shaders) return; + + const uniforms = &self.custom_shader_uniforms; + const now = try std.time.Instant.now(); defer self.last_frame_time = now; const first_frame_time = self.first_frame_time orelse t: { @@ -2248,23 +2423,23 @@ pub fn Renderer(comptime GraphicsAPI: type) type { const last_frame_time = self.last_frame_time orelse now; const since_ns: f32 = @floatFromInt(now.since(first_frame_time)); - self.custom_shader_uniforms.time = since_ns / std.time.ns_per_s; + uniforms.time = since_ns / std.time.ns_per_s; const delta_ns: f32 = @floatFromInt(now.since(last_frame_time)); - self.custom_shader_uniforms.time_delta = delta_ns / std.time.ns_per_s; + uniforms.time_delta = delta_ns / std.time.ns_per_s; - self.custom_shader_uniforms.frame += 1; + uniforms.frame += 1; const screen = self.size.screen; const padding = self.size.padding; const cell = self.size.cell; - self.custom_shader_uniforms.resolution = .{ + uniforms.resolution = .{ @floatFromInt(screen.width), @floatFromInt(screen.height), 1, }; - self.custom_shader_uniforms.channel_resolution[0] = .{ + uniforms.channel_resolution[0] = .{ @floatFromInt(screen.width), @floatFromInt(screen.height), 1, @@ -2329,8 +2504,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { @as(f32, @floatFromInt(cursor.color[3])) / 255.0, }; - const uniforms = &self.custom_shader_uniforms; - const cursor_changed: bool = !std.meta.eql(new_cursor, uniforms.current_cursor) or !std.meta.eql(cursor_color, uniforms.current_cursor_color); @@ -2343,22 +2516,41 @@ pub fn Renderer(comptime GraphicsAPI: type) type { uniforms.cursor_change_time = uniforms.time; } } + + // Update focus uniforms + uniforms.focus = @intFromBool(self.focused); + + // If we need to update the time our focus state changed + // then update it to our current frame time. This may not be + // exactly correct since it is frame time, not exact focus + // time, but focus time on its own isn't exactly correct anyways + // since it comes async from a message. + if (self.custom_shader_focused_changed and self.focused) { + uniforms.time_focus = uniforms.time; + self.custom_shader_focused_changed = false; + } } + const PreeditRange = struct { + y: terminal.size.CellCountInt, + x: [2]terminal.size.CellCountInt, + cp_offset: usize, + }; + /// Convert the terminal state to GPU cells stored in CPU memory. These /// are then synced to the GPU in the next frame. This only updates CPU /// memory and doesn't touch the GPU. + /// + /// This requires the draw mutex. + /// + /// Dirty state on terminal state won't be reset by this. fn rebuildCells( self: *Self, preedit: ?renderer.State.Preedit, cursor_style_: ?renderer.CursorStyle, links: *const terminal.RenderState.CellSet, - ) !void { + ) Allocator.Error!void { const state: *terminal.RenderState = &self.terminal_state; - defer state.dirty = .false; - - self.draw_mutex.lock(); - defer self.draw_mutex.unlock(); // const start = try std.time.Instant.now(); // const start_micro = std.time.microTimestamp(); @@ -2370,11 +2562,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Determine our x/y range for preedit. We don't want to render anything // here because we will render the preedit separately. - const preedit_range: ?struct { - y: terminal.size.CellCountInt, - x: [2]terminal.size.CellCountInt, - cp_offset: usize, - } = if (preedit) |preedit_v| preedit: { + const preedit_range: ?PreeditRange = if (preedit) |preedit_v| preedit: { // We base the preedit on the position of the cursor in the // viewport. If the cursor isn't visible in the viewport we // don't show it. @@ -2429,6 +2617,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } } + // From this point on we never fail. We produce some kind of + // working terminal state, even if incorrect. + errdefer comptime unreachable; + // Get our row data from our state const row_data = state.row_data.slice(); const row_raws = row_data.items(.raw); @@ -2452,7 +2644,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { row_dirty[0..row_len], row_selection[0..row_len], row_highlights[0..row_len], - ) |y_usize, row, *cells, *dirty, selection, highlights| { + ) |y_usize, row, *cells, *dirty, selection, *highlights| { const y: terminal.size.CellCountInt = @intCast(y_usize); if (!rebuild) { @@ -2466,440 +2658,22 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Unmark the dirty state in our render state. dirty.* = false; - // If our viewport is wider than our cell contents buffer, - // we still only process cells up to the width of the buffer. - const cells_slice = cells.slice(); - const cells_len = @min(cells_slice.len, self.cells.size.columns); - const cells_raw = cells_slice.items(.raw); - const cells_style = cells_slice.items(.style); - - // On primary screen, we still apply vertical padding - // extension under certain conditions we feel are safe. - // - // This helps make some scenarios look better while - // avoiding scenarios we know do NOT look good. - switch (self.config.padding_color) { - // These already have the correct values set above. - .background, .@"extend-always" => {}, - - // Apply heuristics for padding extension. - .extend => if (y == 0) { - self.uniforms.padding_extend.up = !rowNeverExtendBg( - row, - cells_raw, - cells_style, - &state.colors.palette, - state.colors.background, - ); - } else if (y == self.cells.size.rows - 1) { - self.uniforms.padding_extend.down = !rowNeverExtendBg( - row, - cells_raw, - cells_style, - &state.colors.palette, - state.colors.background, - ); - }, - } - - // Iterator of runs for shaping. - var run_iter_opts: font.shape.RunOptions = .{ - .grid = self.font_grid, - .cells = cells_slice, - .selection = if (selection) |s| s else null, - - // We want to do font shaping as long as the cursor is - // visible on this viewport. - .cursor_x = cursor_x: { - const vp = state.cursor.viewport orelse break :cursor_x null; - if (vp.y != y) break :cursor_x null; - break :cursor_x vp.x; - }, + self.rebuildRow( + y, + row, + cells, + preedit_range, + selection, + highlights, + links, + ) catch |err| { + // This should never happen except under exceptional + // scenarios. In this case, we don't want to corrupt + // our render state so just clear this row and keep + // trying to finish it out. + log.warn("error building row y={} err={}", .{ y, err }); + self.cells.clear(y); }; - run_iter_opts.applyBreakConfig(self.config.font_shaping_break); - var run_iter = self.font_shaper.runIterator(run_iter_opts); - var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc); - var shaper_cells: ?[]const font.shape.Cell = null; - var shaper_cells_i: usize = 0; - - for ( - 0.., - cells_raw[0..cells_len], - cells_style[0..cells_len], - ) |x, *cell, *managed_style| { - // If this cell falls within our preedit range then we - // skip this because preedits are setup separately. - if (preedit_range) |range| preedit: { - // We're not on the preedit line, no actions necessary. - if (range.y != y) break :preedit; - // We're before the preedit range, no actions necessary. - if (x < range.x[0]) break :preedit; - // We're in the preedit range, skip this cell. - if (x <= range.x[1]) continue; - // After exiting the preedit range we need to catch - // the run position up because of the missed cells. - // In all other cases, no action is necessary. - if (x != range.x[1] + 1) break :preedit; - - // Step the run iterator until we find a run that ends - // after the current cell, which will be the soonest run - // that might contain glyphs for our cell. - while (shaper_run) |run| { - if (run.offset + run.cells > x) break; - shaper_run = try run_iter.next(self.alloc); - shaper_cells = null; - shaper_cells_i = 0; - } - - const run = shaper_run orelse break :preedit; - - // If we haven't shaped this run, do so now. - shaper_cells = shaper_cells orelse - // Try to read the cells from the shaping cache if we can. - self.font_shaper_cache.get(run) orelse - cache: { - // Otherwise we have to shape them. - const new_cells = try self.font_shaper.shape(run); - - // Try to cache them. If caching fails for any reason we - // continue because it is just a performance optimization, - // not a correctness issue. - self.font_shaper_cache.put( - self.alloc, - run, - new_cells, - ) catch |err| { - log.warn( - "error caching font shaping results err={}", - .{err}, - ); - }; - - // The cells we get from direct shaping are always owned - // by the shaper and valid until the next shaping call so - // we can safely use them. - break :cache new_cells; - }; - - // Advance our index until we reach or pass - // our current x position in the shaper cells. - const shaper_cells_unwrapped = shaper_cells.?; - while (run.offset + shaper_cells_unwrapped[shaper_cells_i].x < x) { - shaper_cells_i += 1; - } - } - - const wide = cell.wide; - const style: terminal.Style = if (cell.hasStyling()) - managed_style.* - else - .{}; - - // True if this cell is selected - const selected: enum { - false, - selection, - search, - search_selected, - } = selected: { - // Order below matters for precedence. - - // Selection should take the highest precedence. - const x_compare = if (wide == .spacer_tail) - x -| 1 - else - x; - if (selection) |sel| { - if (x_compare >= sel[0] and - x_compare <= sel[1]) break :selected .selection; - } - - // If we're highlighted, then we're selected. In the - // future we want to use a different style for this - // but this to get started. - for (highlights.items) |hl| { - if (x_compare >= hl.range[0] and - x_compare <= hl.range[1]) - { - const tag: HighlightTag = @enumFromInt(hl.tag); - break :selected switch (tag) { - .search_match => .search, - .search_match_selected => .search_selected, - }; - } - } - - break :selected .false; - }; - - // The `_style` suffixed values are the colors based on - // the cell style (SGR), before applying any additional - // configuration, inversions, selections, etc. - const bg_style = style.bg( - cell, - &state.colors.palette, - ); - const fg_style = style.fg(.{ - .default = state.colors.foreground, - .palette = &state.colors.palette, - .bold = self.config.bold_color, - }); - - // The final background color for the cell. - const bg = switch (selected) { - // If we have an explicit selection background color - // specified in the config, use that. - // - // If no configuration, then our selection background - // is our foreground color. - .selection => if (self.config.selection_background) |v| switch (v) { - .color => |color| color.toTerminalRGB(), - .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, - .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, - } else state.colors.foreground, - - .search => switch (self.config.search_background) { - .color => |color| color.toTerminalRGB(), - .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, - .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, - }, - - .search_selected => switch (self.config.search_selected_background) { - .color => |color| color.toTerminalRGB(), - .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, - .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, - }, - - // Not selected - .false => if (style.flags.inverse != isCovering(cell.codepoint())) - // Two cases cause us to invert (use the fg color as the bg) - // - The "inverse" style flag. - // - A "covering" glyph; we use fg for bg in that - // case to help make sure that padding extension - // works correctly. - // - // If one of these is true (but not the other) - // then we use the fg style color for the bg. - fg_style - else - // Otherwise they cancel out. - bg_style, - }; - - const fg = fg: { - // Our happy-path non-selection background color - // is our style or our configured defaults. - const final_bg = bg_style orelse state.colors.background; - - // Whether we need to use the bg color as our fg color: - // - Cell is selected, inverted, and set to cell-foreground - // - Cell is selected, not inverted, and set to cell-background - // - Cell is inverted and not selected - break :fg switch (selected) { - .selection => if (self.config.selection_foreground) |v| switch (v) { - .color => |color| color.toTerminalRGB(), - .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, - .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, - } else state.colors.background, - - .search => switch (self.config.search_foreground) { - .color => |color| color.toTerminalRGB(), - .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, - .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, - }, - - .search_selected => switch (self.config.search_selected_foreground) { - .color => |color| color.toTerminalRGB(), - .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, - .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, - }, - - .false => if (style.flags.inverse) - final_bg - else - fg_style, - }; - }; - - // Foreground alpha for this cell. - const alpha: u8 = if (style.flags.faint) self.config.faint_opacity else 255; - - // Set the cell's background color. - { - const rgb = bg orelse state.colors.background; - - // Determine our background alpha. If we have transparency configured - // then this is dynamic depending on some situations. This is all - // in an attempt to make transparency look the best for various - // situations. See inline comments. - const bg_alpha: u8 = bg_alpha: { - const default: u8 = 255; - - // Cells that are selected should be fully opaque. - if (selected != .false) break :bg_alpha default; - - // Cells that are reversed should be fully opaque. - if (style.flags.inverse) break :bg_alpha default; - - // If the user requested to have opacity on all cells, apply it. - if (self.config.background_opacity_cells and bg_style != null) { - var opacity: f64 = @floatFromInt(default); - opacity *= self.config.background_opacity; - break :bg_alpha @intFromFloat(opacity); - } - - // Cells that have an explicit bg color should be fully opaque. - if (bg_style != null) break :bg_alpha default; - - // Otherwise, we won't draw the bg for this cell, - // we'll let the already-drawn background color - // show through. - break :bg_alpha 0; - }; - - self.cells.bgCell(y, x).* = .{ - rgb.r, rgb.g, rgb.b, bg_alpha, - }; - } - - // If the invisible flag is set on this cell then we - // don't need to render any foreground elements, so - // we just skip all glyphs with this x coordinate. - // - // NOTE: This behavior matches xterm. Some other terminal - // emulators, e.g. Alacritty, still render text decorations - // and only make the text itself invisible. The decision - // has been made here to match xterm's behavior for this. - if (style.flags.invisible) { - continue; - } - - // Give links a single underline, unless they already have - // an underline, in which case use a double underline to - // distinguish them. - const underline: terminal.Attribute.Underline = underline: { - if (links.contains(.{ - .x = @intCast(x), - .y = @intCast(y), - })) { - break :underline if (style.flags.underline == .single) - .double - else - .single; - } - break :underline style.flags.underline; - }; - - // We draw underlines first so that they layer underneath text. - // This improves readability when a colored underline is used - // which intersects parts of the text (descenders). - if (underline != .none) self.addUnderline( - @intCast(x), - @intCast(y), - underline, - style.underlineColor(&state.colors.palette) orelse fg, - alpha, - ) catch |err| { - log.warn( - "error adding underline to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - - if (style.flags.overline) self.addOverline(@intCast(x), @intCast(y), fg, alpha) catch |err| { - log.warn( - "error adding overline to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - - // If we're at or past the end of our shaper run then - // we need to get the next run from the run iterator. - if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) { - shaper_run = try run_iter.next(self.alloc); - shaper_cells = null; - shaper_cells_i = 0; - } - - if (shaper_run) |run| glyphs: { - // If we haven't shaped this run yet, do so. - shaper_cells = shaper_cells orelse - // Try to read the cells from the shaping cache if we can. - self.font_shaper_cache.get(run) orelse - cache: { - // Otherwise we have to shape them. - const new_cells = try self.font_shaper.shape(run); - - // Try to cache them. If caching fails for any reason we - // continue because it is just a performance optimization, - // not a correctness issue. - self.font_shaper_cache.put( - self.alloc, - run, - new_cells, - ) catch |err| { - log.warn( - "error caching font shaping results err={}", - .{err}, - ); - }; - - // The cells we get from direct shaping are always owned - // by the shaper and valid until the next shaping call so - // we can safely use them. - break :cache new_cells; - }; - - const shaped_cells = shaper_cells orelse break :glyphs; - - // If there are no shaper cells for this run, ignore it. - // This can occur for runs of empty cells, and is fine. - if (shaped_cells.len == 0) break :glyphs; - - // If we encounter a shaper cell to the left of the current - // cell then we have some problems. This logic relies on x - // position monotonically increasing. - assert(run.offset + shaped_cells[shaper_cells_i].x >= x); - - // NOTE: An assumption is made here that a single cell will never - // be present in more than one shaper run. If that assumption is - // violated, this logic breaks. - - while (shaper_cells_i < shaped_cells.len and - run.offset + shaped_cells[shaper_cells_i].x == x) : ({ - shaper_cells_i += 1; - }) { - self.addGlyph( - @intCast(x), - @intCast(y), - state.cols, - cells_raw, - shaped_cells[shaper_cells_i], - shaper_run.?, - fg, - alpha, - ) catch |err| { - log.warn( - "error adding glyph to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - } - } - - // Finally, draw a strikethrough if necessary. - if (style.flags.strikethrough) self.addStrikethrough( - @intCast(x), - @intCast(y), - fg, - alpha, - ) catch |err| { - log.warn( - "error adding strikethrough to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - } } // Setup our cursor rendering information. @@ -3068,6 +2842,454 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // }); } + fn rebuildRow( + self: *Self, + y: terminal.size.CellCountInt, + row: terminal.page.Row, + cells: *std.MultiArrayList(terminal.RenderState.Cell), + preedit_range: ?PreeditRange, + selection: ?[2]terminal.size.CellCountInt, + highlights: *const std.ArrayList(terminal.RenderState.Highlight), + links: *const terminal.RenderState.CellSet, + ) !void { + const state = &self.terminal_state; + + // If our viewport is wider than our cell contents buffer, + // we still only process cells up to the width of the buffer. + const cells_slice = cells.slice(); + const cells_len = @min(cells_slice.len, self.cells.size.columns); + const cells_raw = cells_slice.items(.raw); + const cells_style = cells_slice.items(.style); + + // On primary screen, we still apply vertical padding + // extension under certain conditions we feel are safe. + // + // This helps make some scenarios look better while + // avoiding scenarios we know do NOT look good. + switch (self.config.padding_color) { + // These already have the correct values set above. + .background, .@"extend-always" => {}, + + // Apply heuristics for padding extension. + .extend => if (y == 0) { + self.uniforms.padding_extend.up = !rowNeverExtendBg( + row, + cells_raw, + cells_style, + &state.colors.palette, + state.colors.background, + ); + } else if (y == self.cells.size.rows - 1) { + self.uniforms.padding_extend.down = !rowNeverExtendBg( + row, + cells_raw, + cells_style, + &state.colors.palette, + state.colors.background, + ); + }, + } + + // Iterator of runs for shaping. + var run_iter_opts: font.shape.RunOptions = .{ + .grid = self.font_grid, + .cells = cells_slice, + .selection = if (selection) |s| s else null, + + // We want to do font shaping as long as the cursor is + // visible on this viewport. + .cursor_x = cursor_x: { + const vp = state.cursor.viewport orelse break :cursor_x null; + if (vp.y != y) break :cursor_x null; + break :cursor_x vp.x; + }, + }; + run_iter_opts.applyBreakConfig(self.config.font_shaping_break); + var run_iter = self.font_shaper.runIterator(run_iter_opts); + var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc); + var shaper_cells: ?[]const font.shape.Cell = null; + var shaper_cells_i: usize = 0; + + for ( + 0.., + cells_raw[0..cells_len], + cells_style[0..cells_len], + ) |x, *cell, *managed_style| { + // If this cell falls within our preedit range then we + // skip this because preedits are setup separately. + if (preedit_range) |range| preedit: { + // We're not on the preedit line, no actions necessary. + if (range.y != y) break :preedit; + // We're before the preedit range, no actions necessary. + if (x < range.x[0]) break :preedit; + // We're in the preedit range, skip this cell. + if (x <= range.x[1]) continue; + // After exiting the preedit range we need to catch + // the run position up because of the missed cells. + // In all other cases, no action is necessary. + if (x != range.x[1] + 1) break :preedit; + + // Step the run iterator until we find a run that ends + // after the current cell, which will be the soonest run + // that might contain glyphs for our cell. + while (shaper_run) |run| { + if (run.offset + run.cells > x) break; + shaper_run = try run_iter.next(self.alloc); + shaper_cells = null; + shaper_cells_i = 0; + } + + const run = shaper_run orelse break :preedit; + + // If we haven't shaped this run, do so now. + shaper_cells = shaper_cells orelse + // Try to read the cells from the shaping cache if we can. + self.font_shaper_cache.get(run) orelse + cache: { + // Otherwise we have to shape them. + const new_cells = try self.font_shaper.shape(run); + + // Try to cache them. If caching fails for any reason we + // continue because it is just a performance optimization, + // not a correctness issue. + self.font_shaper_cache.put( + self.alloc, + run, + new_cells, + ) catch |err| { + log.warn( + "error caching font shaping results err={}", + .{err}, + ); + }; + + // The cells we get from direct shaping are always owned + // by the shaper and valid until the next shaping call so + // we can safely use them. + break :cache new_cells; + }; + + // Advance our index until we reach or pass + // our current x position in the shaper cells. + const shaper_cells_unwrapped = shaper_cells.?; + while (run.offset + shaper_cells_unwrapped[shaper_cells_i].x < x) { + shaper_cells_i += 1; + } + } + + const wide = cell.wide; + const style: terminal.Style = if (cell.hasStyling()) + managed_style.* + else + .{}; + + // True if this cell is selected + const selected: enum { + false, + selection, + search, + search_selected, + } = selected: { + // Order below matters for precedence. + + // Selection should take the highest precedence. + const x_compare = if (wide == .spacer_tail) + x -| 1 + else + x; + if (selection) |sel| { + if (x_compare >= sel[0] and + x_compare <= sel[1]) break :selected .selection; + } + + // If we're highlighted, then we're selected. In the + // future we want to use a different style for this + // but this to get started. + for (highlights.items) |hl| { + if (x_compare >= hl.range[0] and + x_compare <= hl.range[1]) + { + const tag: HighlightTag = @enumFromInt(hl.tag); + break :selected switch (tag) { + .search_match => .search, + .search_match_selected => .search_selected, + }; + } + } + + break :selected .false; + }; + + // The `_style` suffixed values are the colors based on + // the cell style (SGR), before applying any additional + // configuration, inversions, selections, etc. + const bg_style = style.bg( + cell, + &state.colors.palette, + ); + const fg_style = style.fg(.{ + .default = state.colors.foreground, + .palette = &state.colors.palette, + .bold = self.config.bold_color, + }); + + // The final background color for the cell. + const bg = switch (selected) { + // If we have an explicit selection background color + // specified in the config, use that. + // + // If no configuration, then our selection background + // is our foreground color. + .selection => if (self.config.selection_background) |v| switch (v) { + .color => |color| color.toTerminalRGB(), + .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, + .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, + } else state.colors.foreground, + + .search => switch (self.config.search_background) { + .color => |color| color.toTerminalRGB(), + .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, + .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, + }, + + .search_selected => switch (self.config.search_selected_background) { + .color => |color| color.toTerminalRGB(), + .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, + .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, + }, + + // Not selected + .false => if (style.flags.inverse != isCovering(cell.codepoint())) + // Two cases cause us to invert (use the fg color as the bg) + // - The "inverse" style flag. + // - A "covering" glyph; we use fg for bg in that + // case to help make sure that padding extension + // works correctly. + // + // If one of these is true (but not the other) + // then we use the fg style color for the bg. + fg_style + else + // Otherwise they cancel out. + bg_style, + }; + + const fg = fg: { + // Our happy-path non-selection background color + // is our style or our configured defaults. + const final_bg = bg_style orelse state.colors.background; + + // Whether we need to use the bg color as our fg color: + // - Cell is selected, inverted, and set to cell-foreground + // - Cell is selected, not inverted, and set to cell-background + // - Cell is inverted and not selected + break :fg switch (selected) { + .selection => if (self.config.selection_foreground) |v| switch (v) { + .color => |color| color.toTerminalRGB(), + .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, + .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, + } else state.colors.background, + + .search => switch (self.config.search_foreground) { + .color => |color| color.toTerminalRGB(), + .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, + .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, + }, + + .search_selected => switch (self.config.search_selected_foreground) { + .color => |color| color.toTerminalRGB(), + .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, + .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, + }, + + .false => if (style.flags.inverse) + final_bg + else + fg_style, + }; + }; + + // Foreground alpha for this cell. + const alpha: u8 = if (style.flags.faint) self.config.faint_opacity else 255; + + // Set the cell's background color. + { + const rgb = bg orelse state.colors.background; + + // Determine our background alpha. If we have transparency configured + // then this is dynamic depending on some situations. This is all + // in an attempt to make transparency look the best for various + // situations. See inline comments. + const bg_alpha: u8 = bg_alpha: { + const default: u8 = 255; + + // Cells that are selected should be fully opaque. + if (selected != .false) break :bg_alpha default; + + // Cells that are reversed should be fully opaque. + if (style.flags.inverse) break :bg_alpha default; + + // If the user requested to have opacity on all cells, apply it. + if (self.config.background_opacity_cells and bg_style != null) { + var opacity: f64 = @floatFromInt(default); + opacity *= self.config.background_opacity; + break :bg_alpha @intFromFloat(opacity); + } + + // Cells that have an explicit bg color should be fully opaque. + if (bg_style != null) break :bg_alpha default; + + // Otherwise, we won't draw the bg for this cell, + // we'll let the already-drawn background color + // show through. + break :bg_alpha 0; + }; + + self.cells.bgCell(y, x).* = .{ + rgb.r, rgb.g, rgb.b, bg_alpha, + }; + } + + // If the invisible flag is set on this cell then we + // don't need to render any foreground elements, so + // we just skip all glyphs with this x coordinate. + // + // NOTE: This behavior matches xterm. Some other terminal + // emulators, e.g. Alacritty, still render text decorations + // and only make the text itself invisible. The decision + // has been made here to match xterm's behavior for this. + if (style.flags.invisible) { + continue; + } + + // Give links a single underline, unless they already have + // an underline, in which case use a double underline to + // distinguish them. + const underline: terminal.Attribute.Underline = underline: { + if (links.contains(.{ + .x = @intCast(x), + .y = @intCast(y), + })) { + break :underline if (style.flags.underline == .single) + .double + else + .single; + } + break :underline style.flags.underline; + }; + + // We draw underlines first so that they layer underneath text. + // This improves readability when a colored underline is used + // which intersects parts of the text (descenders). + if (underline != .none) self.addUnderline( + @intCast(x), + @intCast(y), + underline, + style.underlineColor(&state.colors.palette) orelse fg, + alpha, + ) catch |err| { + log.warn( + "error adding underline to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + + if (style.flags.overline) self.addOverline(@intCast(x), @intCast(y), fg, alpha) catch |err| { + log.warn( + "error adding overline to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + + // If we're at or past the end of our shaper run then + // we need to get the next run from the run iterator. + if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) { + shaper_run = try run_iter.next(self.alloc); + shaper_cells = null; + shaper_cells_i = 0; + } + + if (shaper_run) |run| glyphs: { + // If we haven't shaped this run yet, do so. + shaper_cells = shaper_cells orelse + // Try to read the cells from the shaping cache if we can. + self.font_shaper_cache.get(run) orelse + cache: { + // Otherwise we have to shape them. + const new_cells = try self.font_shaper.shape(run); + + // Try to cache them. If caching fails for any reason we + // continue because it is just a performance optimization, + // not a correctness issue. + self.font_shaper_cache.put( + self.alloc, + run, + new_cells, + ) catch |err| { + log.warn( + "error caching font shaping results err={}", + .{err}, + ); + }; + + // The cells we get from direct shaping are always owned + // by the shaper and valid until the next shaping call so + // we can safely use them. + break :cache new_cells; + }; + + const shaped_cells = shaper_cells orelse break :glyphs; + + // If there are no shaper cells for this run, ignore it. + // This can occur for runs of empty cells, and is fine. + if (shaped_cells.len == 0) break :glyphs; + + // If we encounter a shaper cell to the left of the current + // cell then we have some problems. This logic relies on x + // position monotonically increasing. + assert(run.offset + shaped_cells[shaper_cells_i].x >= x); + + // NOTE: An assumption is made here that a single cell will never + // be present in more than one shaper run. If that assumption is + // violated, this logic breaks. + + while (shaper_cells_i < shaped_cells.len and + run.offset + shaped_cells[shaper_cells_i].x == x) : ({ + shaper_cells_i += 1; + }) { + self.addGlyph( + @intCast(x), + @intCast(y), + state.cols, + cells_raw, + shaped_cells[shaper_cells_i], + shaper_run.?, + fg, + alpha, + ) catch |err| { + log.warn( + "error adding glyph to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + } + } + + // Finally, draw a strikethrough if necessary. + if (style.flags.strikethrough) self.addStrikethrough( + @intCast(x), + @intCast(y), + fg, + alpha, + ) catch |err| { + log.warn( + "error adding strikethrough to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + } + } + /// Add an underline decoration to the specified cell fn addUnderline( self: *Self, @@ -3330,10 +3552,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { screen_bg: terminal.color.RGB, screen_fg: terminal.color.RGB, ) !void { - // Preedit is rendered inverted - const bg = screen_fg; - const fg = screen_bg; - // Render the glyph for our preedit text const render_ = self.font_grid.renderCodepoint( self.alloc, @@ -3352,11 +3570,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Add our opaque background cell self.cells.bgCell(coord.y, coord.x).* = .{ - bg.r, bg.g, bg.b, 255, + screen_bg.r, screen_bg.g, screen_bg.b, 255, }; if (cp.wide and coord.x < self.cells.size.columns - 1) { self.cells.bgCell(coord.y, coord.x + 1).* = .{ - bg.r, bg.g, bg.b, 255, + screen_bg.r, screen_bg.g, screen_bg.b, 255, }; } @@ -3364,7 +3582,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { try self.cells.add(self.alloc, .text, .{ .atlas = .grayscale, .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, - .color = .{ fg.r, fg.g, fg.b, 255 }, + .color = .{ screen_fg.r, screen_fg.g, screen_fg.b, 255 }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, .glyph_size = .{ render.glyph.width, render.glyph.height }, .bearings = .{ @@ -3372,6 +3590,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type { @intCast(render.glyph.offset_y), }, }); + + // Add underline + try self.addUnderline(@intCast(coord.x), @intCast(coord.y), .single, screen_fg, 255); + if (cp.wide and coord.x < self.cells.size.columns - 1) { + try self.addUnderline(@intCast(coord.x + 1), @intCast(coord.y), .single, screen_fg, 255); + } } /// Sync the atlas data to the given texture. This copies the bytes diff --git a/src/renderer/image.zig b/src/renderer/image.zig index 7089f5a8b..bf0f7b736 100644 --- a/src/renderer/image.zig +++ b/src/renderer/image.zig @@ -146,7 +146,7 @@ pub const Image = union(enum) { /// Mark the current image to be replaced with a pending one. This will /// attempt to update the existing texture if we have one, otherwise it /// will act like a new upload. - pub fn markForReplace(self: *Image, alloc: Allocator, img: Image) !void { + pub fn markForReplace(self: *Image, alloc: Allocator, img: Image) void { assert(img.isPending()); // If we have pending data right now, free it. @@ -216,9 +216,8 @@ pub const Image = union(enum) { /// Prepare the pending image data for upload to the GPU. /// This doesn't need GPU access so is safe to call any time. - pub fn prepForUpload(self: *Image, alloc: Allocator) !void { + pub fn prepForUpload(self: *Image, alloc: Allocator) wuffs.Error!void { assert(self.isPending()); - try self.convert(alloc); } diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig index e1daa6848..a2d8a1356 100644 --- a/src/renderer/metal/api.zig +++ b/src/renderer/metal/api.zig @@ -391,6 +391,27 @@ pub const MTLRenderStage = enum(c_ulong) { mesh = 16, }; +/// https://developer.apple.com/documentation/metal/mtlgpufamily?language=objc +pub const MTLGPUFamily = enum(c_long) { + apple1 = 1001, + apple2 = 1002, + apple3 = 1003, + apple4 = 1004, + apple5 = 1005, + apple6 = 1006, + apple7 = 1007, + apple8 = 1008, + apple9 = 1009, + apple10 = 1010, + + common1 = 3001, + common2 = 3002, + common3 = 3003, + + metal3 = 5001, + metal4 = 5002, +}; + pub const MTLClearColor = extern struct { red: f64, green: f64, diff --git a/src/renderer/shaders/shadertoy_prefix.glsl b/src/renderer/shaders/shadertoy_prefix.glsl index 6d9cf0f68..661bd233d 100644 --- a/src/renderer/shaders/shadertoy_prefix.glsl +++ b/src/renderer/shaders/shadertoy_prefix.glsl @@ -16,6 +16,15 @@ layout(binding = 1, std140) uniform Globals { uniform vec4 iCurrentCursorColor; uniform vec4 iPreviousCursorColor; uniform float iTimeCursorChange; + uniform float iTimeFocus; + uniform int iFocus; + uniform vec3 iPalette[256]; + uniform vec3 iBackgroundColor; + uniform vec3 iForegroundColor; + uniform vec3 iCursorColor; + uniform vec3 iCursorText; + uniform vec3 iSelectionForegroundColor; + uniform vec3 iSelectionBackgroundColor; }; layout(binding = 0) uniform sampler2D iChannel0; diff --git a/src/renderer/shaders/test_shadertoy_focus.glsl b/src/renderer/shaders/test_shadertoy_focus.glsl new file mode 100644 index 000000000..9fc2304df --- /dev/null +++ b/src/renderer/shaders/test_shadertoy_focus.glsl @@ -0,0 +1,41 @@ +// Test shader for iTimeFocus and iFocus +// Shows border when focused, green fade that restarts on each focus gain +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + + // Sample the terminal content + vec4 terminal = texture2D(iChannel0, uv); + vec3 color = terminal.rgb; + + if (iFocus > 0) { + // FOCUSED: Add border and fading green overlay + + // Calculate time since focus was gained + float timeSinceFocus = iTime - iTimeFocus; + + // Green fade: starts at 1.0 (full green), fades to 0.0 over 3 seconds + float fadeOut = max(0.0, 1.0 - (timeSinceFocus / 3.0)); + + // Add green overlay that fades out + color = mix(color, vec3(0.0, 1.0, 0.0), fadeOut * 0.4); + + // Add border (5 pixels) + float borderSize = 5.0; + vec2 pixelCoord = fragCoord; + bool isBorder = pixelCoord.x < borderSize || + pixelCoord.x > iResolution.x - borderSize || + pixelCoord.y < borderSize || + pixelCoord.y > iResolution.y - borderSize; + + if (isBorder) { + // Bright cyan border that pulses subtly + float pulse = sin(timeSinceFocus * 2.0) * 0.1 + 0.9; + color = vec3(0.0, 1.0, 1.0) * pulse; + } + } else { + // UNFOCUSED: Solid red overlay (no border) + color = mix(color, vec3(1.0, 0.0, 0.0), 0.3); + } + + fragColor = vec4(color, 1.0); +} diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 0d096c0fc..7d0ad4b0a 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -25,6 +25,15 @@ pub const Uniforms = extern struct { current_cursor_color: [4]f32 align(16), previous_cursor_color: [4]f32 align(16), cursor_change_time: f32 align(4), + time_focus: f32 align(4), + focus: i32 align(4), + palette: [256][4]f32 align(16), + background_color: [4]f32 align(16), + foreground_color: [4]f32 align(16), + cursor_color: [4]f32 align(16), + cursor_text: [4]f32 align(16), + selection_background_color: [4]f32 align(16), + selection_foreground_color: [4]f32 align(16), }; /// The target to load shaders for. @@ -412,3 +421,4 @@ test "shadertoy to glsl" { const test_crt = @embedFile("shaders/test_shadertoy_crt.glsl"); const test_invalid = @embedFile("shaders/test_shadertoy_invalid.glsl"); +const test_focus = @embedFile("shaders/test_shadertoy_focus.glsl"); diff --git a/src/renderer/size.zig b/src/renderer/size.zig index b26c1581e..d8b529c26 100644 --- a/src/renderer/size.zig +++ b/src/renderer/size.zig @@ -44,6 +44,15 @@ pub const Size = struct { self.grid(), self.cell, ); + + // The top/bottom padding is interesting. Subjectively, lots of padding + // at the top looks bad. So instead of always being equal (like left/right), + // we force the top padding to be at most equal to the maximum left padding, + // which is the balanced explicit horizontal padding plus half a cell width. + const max_padding_left = (explicit.left + explicit.right + self.cell.width) / 2; + const vshift = self.padding.top -| max_padding_left; + self.padding.top -= vshift; + self.padding.bottom += vshift; } }; @@ -258,16 +267,12 @@ pub const Padding = struct { const space_right = @as(f32, @floatFromInt(screen.width)) - grid_width; const space_bot = @as(f32, @floatFromInt(screen.height)) - grid_height; - // The left/right padding is just an equal split. + // The padding is split equally along both axes. const padding_right = @floor(space_right / 2); const padding_left = padding_right; - // The top/bottom padding is interesting. Subjectively, lots of padding - // at the top looks bad. So instead of always being equal (like left/right), - // we force the top padding to be at most equal to the left, and the bottom - // padding is the difference thereafter. - const padding_top = @min(padding_left, @floor(space_bot / 2)); - const padding_bot = space_bot - padding_top; + const padding_bot = @floor(space_bot / 2); + const padding_top = padding_bot; const zero = @as(f32, 0); return .{ diff --git a/src/shell-integration/README.md b/src/shell-integration/README.md index 3f8543c68..3484b0cdc 100644 --- a/src/shell-integration/README.md +++ b/src/shell-integration/README.md @@ -76,12 +76,37 @@ allowing us to automatically integrate with the shell. For details on the Fish startup process, see the [Fish documentation](https://fishshell.com/docs/current/language.html). +### Nushell + +For [Nushell](https://www.nushell.sh/), Ghostty prepends to the +`XDG_DATA_DIRS` directory, making the `ghostty` module available through +Nushell's vendor autoload mechanism. Ghostty then automatically imports +the module using the `-e "use ghostty *"` flag when starting Nushell. + +Nushell provides many shell features itself, such as `title` and `cursor`, +so our integration focuses on Ghostty-specific features like `sudo`, +`ssh-env`, and `ssh-terminfo`. + +The shell integration is automatically enabled when running Nushell in Ghostty, +but you can also load it manually is shell integration is disabled: + +```nushell +source $GHOSTTY_RESOURCES_DIR/shell-integration/nushell/vendor/autoload/ghostty.nu +use ghostty * +``` + ### Zsh -For `zsh`, Ghostty sets `ZDOTDIR` so that it loads our configuration -from the `zsh` directory. The existing `ZDOTDIR` is retained so that -after loading the Ghostty shell integration the normal Zsh loading -sequence occurs. +Automatic [Zsh](https://www.zsh.org/) integration works by temporarily setting +`ZDOTDIR` to our `zsh` directory. An existing `ZDOTDIR` environment variable +value will be retained and restored after our shell integration scripts are +run. + +However, if `ZDOTDIR` is set in a system-wide file like `/etc/zshenv`, it will +override Ghostty's `ZDOTDIR` value, preventing the shell integration from being +loaded. In this case, the shell integration needs to be loaded manually. + +To load the Zsh shell integration manually: ```zsh if [[ -n $GHOSTTY_RESOURCES_DIR ]]; then diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index e910a9885..799d0cff6 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -103,7 +103,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then if [[ "$sudo_has_sudoedit_flags" == "yes" ]]; then builtin command sudo "$@"; else - builtin command sudo TERMINFO="$TERMINFO" "$@"; + builtin command sudo --preserve-env=TERMINFO "$@"; fi } fi diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 33473c8b0..e4b449ae5 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -97,7 +97,7 @@ if (not (has-value $arg =)) { break } } - if (not $sudoedit) { set args = [ TERMINFO=$E:TERMINFO $@args ] } + if (not $sudoedit) { set args = [ --preserve-env=TERMINFO $@args ] } (external sudo) $@args } diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index 47af9be98..580e27f45 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -90,7 +90,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" if test "$sudo_has_sudoedit_flags" = "yes" command sudo $argv else - command sudo TERMINFO="$TERMINFO" $argv + command sudo --preserve-env=TERMINFO $argv end end end diff --git a/src/shell-integration/nushell/vendor/autoload/ghostty.nu b/src/shell-integration/nushell/vendor/autoload/ghostty.nu new file mode 100644 index 000000000..93e5fd909 --- /dev/null +++ b/src/shell-integration/nushell/vendor/autoload/ghostty.nu @@ -0,0 +1,133 @@ +# Ghostty shell integration +export module ghostty { + def has_feature [feature: string] { + $feature in ($env.GHOSTTY_SHELL_FEATURES | default "" | split row ',') + } + + # Enables automatic terminfo installation on remote hosts. + # Attempts to install Ghostty's terminfo entry using infocmp and tic when + # connecting to hosts that lack it. + # Requires infocmp to be available locally and tic to be available on remote hosts. + # Caches installations to avoid repeat installations. + def set_ssh_terminfo [ + ssh_opts: list + ssh_args: list + ]: [nothing -> record>] { + let ssh_cfg = ^ssh -G ...($ssh_args) + | lines + | parse "{key} {value}" + | where key in ["user" "hostname"] + | select key value + | transpose -rd + | default {user: $env.USER hostname: "localhost"} + + let ssh_id = $"($ssh_cfg.user)@($ssh_cfg.hostname)" + let ghostty_bin = $env.GHOSTTY_BIN_DIR | path join "ghostty" + + let is_cached = ( + ^$ghostty_bin ...(["+ssh-cache" $"--host=($ssh_id)"]) + | complete + | $in.exit_code == 0 + ) + + if not $is_cached { + let terminfo_data = try { ^infocmp -0 -x xterm-ghostty } catch { + print "Warning: Could not generate terminfo data." + return {ssh_term: "xterm-256color" ssh_opts: $ssh_opts} + } + + print $"Setting up xterm-ghostty terminfo on ($ssh_cfg.hostname)..." + + let ctrl_path = ( + mktemp -td $"ghostty-ssh-($ssh_cfg.user).XXXXXX" + | path join "socket" + ) + + let master_parts = $ssh_opts ++ ["-o" "ControlMaster=yes" "-o" $"ControlPath=($ctrl_path)" "-o" "ControlPersist=60s"] ++ $ssh_args + + ($terminfo_data) | ^ssh ...( + $master_parts ++ + [ + ' + infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 + command -v tic >/dev/null 2>&1 || exit 1 + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 + exit 1' + ] + ) + | complete + | if $in.exit_code != 0 { + print "Warning: Failed to install terminfo." + return {ssh_term: "xterm-256color" ssh_opts: $ssh_opts} + } + + ^$ghostty_bin ...(["+ssh-cache" $"--add=($ssh_id)"]) o+e>| ignore + + return {ssh_term: "xterm-ghostty" ssh_opts: ($ssh_opts ++ ["-o" $"ControlPath=($ctrl_path)"])} + } + + return {ssh_term: "xterm-ghostty" ssh_opts: $ssh_opts} + } + + # Wrap `ssh` with Ghostty TERMINFO support + export def --wrapped ssh [...ssh_args: string]: any -> any { + if ($ssh_args | is-empty) { + return (^ssh) + } + # `ssh-env` enables SSH environment variable compatibility. + # Converts TERM from xterm-ghostty to xterm-256color + # and propagates COLORTERM, TERM_PROGRAM, and TERM_PROGRAM_VERSION + # Check your sshd_config on remote host to see if these variables are accepted + let base_ssh_opts = if (has_feature "ssh-env") { + ["-o" "SetEnv COLORTERM=truecolor" "-o" "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION"] + } else { + [] + } + let base_ssh_term = if (has_feature "ssh-env") { + "xterm-256color" + } else { + ($env.TERM? | default "") + } + + let session = if (has_feature "ssh-terminfo") { + set_ssh_terminfo $base_ssh_opts $ssh_args + } else { + {ssh_term: $base_ssh_term ssh_opts: $base_ssh_opts} + } + + with-env {TERM: $session.ssh_term} { + ^ssh ...($session.ssh_opts ++ $ssh_args) + } + } + + # Wrap `sudo` to preserve Ghostty's TERMINFO environment variable + export def --wrapped sudo [ + ...args # Arguments to pass to `sudo` + ] { + mut sudo_args = $args + + if (has_feature "sudo") { + # Extract just the sudo options (before the command) + let sudo_options = ( + $args | take until {|arg| + not (($arg | str starts-with "-") or ($arg | str contains "=")) + } + ) + + # Prepend TERMINFO preservation flag if not using sudoedit + if (not ("-e" in $sudo_options or "--edit" in $sudo_options)) { + $sudo_args = ($args | prepend "--preserve-env=TERMINFO") + } + } + + ^sudo ...$sudo_args + } +} + +# Clean up XDG_DATA_DIRS by removing GHOSTTY_SHELL_INTEGRATION_XDG_DIR +if 'GHOSTTY_SHELL_INTEGRATION_XDG_DIR' in $env { + if 'XDG_DATA_DIRS' in $env { + $env.XDG_DATA_DIRS = ($env.XDG_DATA_DIRS | str replace $"($env.GHOSTTY_SHELL_INTEGRATION_XDG_DIR):" "") + } + hide-env GHOSTTY_SHELL_INTEGRATION_XDG_DIR +} diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 7ff43efd9..3fb3ec19b 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -93,9 +93,6 @@ _entrypoint() { _ghostty_deferred_init() { builtin emulate -L zsh -o no_warn_create_global -o no_aliases - # The directory where ghostty-integration is located: /../shell-integration/zsh. - builtin local self_dir="${functions_source[_ghostty_deferred_init]:A:h}" - # Enable semantic markup with OSC 133. _ghostty_precmd() { builtin local -i cmd_status=$? @@ -204,11 +201,11 @@ _ghostty_deferred_init() { _ghostty_report_pwd if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then - # Enable terminal title changes. + # Enable terminal title changes, formatted for user-friendly display. functions[_ghostty_precmd]+=" builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(%):-%(4~|…/%3~|%~)}\"\$'\\a'" functions[_ghostty_preexec]+=" - builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(V)1}\"\$'\\a'" + builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${1//[[:cntrl:]]}\"\$'\\a'" fi if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then @@ -255,7 +252,7 @@ _ghostty_deferred_init() { if [[ "$sudo_has_sudoedit_flags" == "yes" ]]; then builtin command sudo "$@"; else - builtin command sudo TERMINFO="$TERMINFO" "$@"; + builtin command sudo --preserve-env=TERMINFO "$@"; fi } fi diff --git a/src/synthetic/Bytes.zig b/src/synthetic/Bytes.zig index 40a94e0e3..7d4c34a33 100644 --- a/src/synthetic/Bytes.zig +++ b/src/synthetic/Bytes.zig @@ -1,4 +1,4 @@ -/// Generates bytes. +//! Generates bytes. const Bytes = @This(); const std = @import("std"); @@ -7,9 +7,7 @@ const Generator = @import("Generator.zig"); /// Random number generator. rand: std.Random, -/// The minimum and maximum length of the generated bytes. The maximum -/// length will be capped to the length of the buffer passed in if the -/// buffer length is smaller. +/// The minimum and maximum length of the generated bytes. min_len: usize = 1, max_len: usize = std.math.maxInt(usize), @@ -18,23 +16,79 @@ max_len: usize = std.math.maxInt(usize), /// side effect of the generator, not an intended use case. alphabet: ?[]const u8 = null, -/// Predefined alphabets. -pub const Alphabet = struct { - pub const ascii = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;':\\\",./<>?`~"; -}; +/// Generate an alphabet given a function that returns true/false for a +/// given byte. +pub fn generateAlphabet(comptime func: fn (u8) bool) []const u8 { + @setEvalBranchQuota(3000); + var count = 0; + for (0..256) |c| { + if (func(c)) count += 1; + } + var alphabet: [count]u8 = undefined; + var i = 0; + for (0..256) |c| { + if (func(c)) { + alphabet[i] = c; + i += 1; + } + } + const result = alphabet; + return &result; +} pub fn generator(self: *Bytes) Generator { return .init(self, next); } -pub fn next(self: *Bytes, writer: *std.Io.Writer, max_len: usize) Generator.Error!void { - std.debug.assert(max_len >= 1); - const len = @min( - self.rand.intRangeAtMostBiased(usize, self.min_len, self.max_len), - max_len, - ); +/// Return a copy of the Bytes, but with a new alphabet. +pub fn newAlphabet(self: *const Bytes, new_alphabet: ?[]const u8) Bytes { + return .{ + .rand = self.rand, + .alphabet = new_alphabet, + .min_len = self.min_len, + .max_len = self.max_len, + }; +} + +/// Return a copy of the Bytes, but with a new min_len. The new min +/// len cannot be more than the previous max_len. +pub fn atLeast(self: *const Bytes, new_min_len: usize) Bytes { + return .{ + .rand = self.rand, + .alphabet = self.alphabet, + .min_len = @min(self.max_len, new_min_len), + .max_len = self.max_len, + }; +} + +/// Return a copy of the Bytes, but with a new max_len. The new max_len cannot +/// be more the previous max_len. +pub fn atMost(self: *const Bytes, new_max_len: usize) Bytes { + return .{ + .rand = self.rand, + .alphabet = self.alphabet, + .min_len = @min(self.min_len, @min(self.max_len, new_max_len)), + .max_len = @min(self.max_len, new_max_len), + }; +} + +pub fn next(self: *const Bytes, writer: *std.Io.Writer, max_len: usize) std.Io.Writer.Error!void { + _ = try self.atMost(max_len).write(writer); +} + +pub fn format(self: *const Bytes, writer: *std.Io.Writer) std.Io.Writer.Error!void { + _ = try self.write(writer); +} + +/// Write some random data and return the number of bytes written. +pub fn write(self: *const Bytes, writer: *std.Io.Writer) std.Io.Writer.Error!usize { + std.debug.assert(self.min_len >= 1); + std.debug.assert(self.max_len >= self.min_len); + + const len = self.rand.intRangeAtMostBiased(usize, self.min_len, self.max_len); var buf: [8]u8 = undefined; + var remaining = len; while (remaining > 0) { const data = buf[0..@min(remaining, buf.len)]; @@ -45,6 +99,8 @@ pub fn next(self: *Bytes, writer: *std.Io.Writer, max_len: usize) Generator.Erro try writer.writeAll(data); remaining -= data.len; } + + return len; } test "bytes" { @@ -52,9 +108,11 @@ test "bytes" { var prng = std.Random.DefaultPrng.init(0); var buf: [256]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); - var v: Bytes = .{ .rand = prng.random() }; - v.min_len = buf.len; - v.max_len = buf.len; + var v: Bytes = .{ + .rand = prng.random(), + .min_len = buf.len, + .max_len = buf.len, + }; const gen = v.generator(); try gen.next(&writer, buf.len); try testing.expectEqual(buf.len, writer.buffered().len); diff --git a/src/synthetic/Osc.zig b/src/synthetic/Osc.zig index 52940fee9..00de43f7f 100644 --- a/src/synthetic/Osc.zig +++ b/src/synthetic/Osc.zig @@ -5,12 +5,23 @@ const std = @import("std"); const assert = std.debug.assert; const Generator = @import("Generator.zig"); const Bytes = @import("Bytes.zig"); +const urlPercentEncode = @import("../os/string_encoding.zig").urlPercentEncode; /// Valid OSC request kinds that can be generated. pub const ValidKind = enum { change_window_title, prompt_start, prompt_end, + end_of_input, + end_of_command, + rxvt_notify, + mouse_shape, + clipboard_operation, + report_pwd, + hyperlink_start, + hyperlink_end, + conemu_progress, + iterm2_notification, }; /// Invalid OSC request kinds that can be generated. @@ -35,19 +46,29 @@ p_valid: f64 = 1.0, p_valid_kind: std.enums.EnumArray(ValidKind, f64) = .initFill(1.0), p_invalid_kind: std.enums.EnumArray(InvalidKind, f64) = .initFill(1.0), -/// The alphabet for random bytes (omitting 0x1B and 0x07). -const bytes_alphabet: []const u8 = alphabet: { - var alphabet: [256]u8 = undefined; - for (0..alphabet.len) |i| { - if (i == 0x1B or i == 0x07) { - alphabet[i] = @intCast(i + 1); - } else { - alphabet[i] = @intCast(i); - } - } - const result = alphabet; - break :alphabet &result; -}; +fn checkKvAlphabet(c: u8) bool { + return switch (c) { + std.ascii.control_code.esc, std.ascii.control_code.bel, ';', '=' => false, + else => std.ascii.isPrint(c), + }; +} + +/// The alphabet for random bytes in OSC key/value pairs (omitting 0x1B, +/// 0x07, ';', '='). +pub const kv_alphabet = Bytes.generateAlphabet(checkKvAlphabet); + +fn checkOscAlphabet(c: u8) bool { + return switch (c) { + std.ascii.control_code.esc, std.ascii.control_code.bel => false, + else => true, + }; +} + +/// The alphabet for random bytes in OSCs (omitting 0x1B and 0x07). +pub const osc_alphabet = Bytes.generateAlphabet(checkOscAlphabet); +pub const ascii_alphabet = Bytes.generateAlphabet(std.ascii.isPrint); +pub const alphabetic_alphabet = Bytes.generateAlphabet(std.ascii.isAlphabetic); +pub const alphanumeric_alphabet = Bytes.generateAlphabet(std.ascii.isAlphanumeric); pub fn generator(self: *Osc) Generator { return .init(self, next); @@ -99,35 +120,152 @@ fn nextUnwrapped(self: *Osc, writer: *std.Io.Writer, max_len: usize) Generator.E fn nextUnwrappedValidExact(self: *const Osc, writer: *std.Io.Writer, k: ValidKind, max_len: usize) Generator.Error!void { switch (k) { - .change_window_title => { - try writer.writeAll("0;"); // Set window title - var bytes_gen = self.bytes(); - try bytes_gen.next(writer, max_len - 2); + .change_window_title => change_window_title: { + if (max_len < 3) break :change_window_title; + try writer.print("0;{f}", .{self.bytes().atMost(max_len - 3)}); // Set window title }, - .prompt_start => { + .prompt_start => prompt_start: { + if (max_len < 4) break :prompt_start; + var remaining = max_len; + try writer.writeAll("133;A"); // Start prompt + remaining -= 4; // aid - if (self.rand.boolean()) { - var bytes_gen = self.bytes(); - bytes_gen.max_len = 16; + if (self.rand.boolean()) aid: { + if (remaining < 6) break :aid; try writer.writeAll(";aid="); - try bytes_gen.next(writer, max_len); + remaining -= 5; + remaining -= try self.bytes().newAlphabet(kv_alphabet).atMost(@min(16, remaining)).write(writer); } // redraw - if (self.rand.boolean()) { + if (self.rand.boolean()) redraw: { + if (remaining < 9) break :redraw; try writer.writeAll(";redraw="); if (self.rand.boolean()) { try writer.writeAll("1"); } else { try writer.writeAll("0"); } + remaining -= 9; } }, - .prompt_end => try writer.writeAll("133;B"), // End prompt + .prompt_end => prompt_end: { + if (max_len < 4) break :prompt_end; + try writer.writeAll("133;B"); // End prompt + }, + + .end_of_input => end_of_input: { + if (max_len < 5) break :end_of_input; + var remaining = max_len; + try writer.writeAll("133;C"); // End prompt + remaining -= 5; + if (self.rand.boolean()) cmdline: { + const prefix = ";cmdline_url="; + if (remaining < prefix.len + 1) break :cmdline; + try writer.writeAll(prefix); + remaining -= prefix.len; + var buf: [128]u8 = undefined; + var w: std.Io.Writer = .fixed(&buf); + try self.bytes().newAlphabet(ascii_alphabet).atMost(@min(remaining, buf.len)).format(&w); + try urlPercentEncode(writer, w.buffered()); + remaining -= w.buffered().len; + } + }, + + .end_of_command => end_of_command: { + if (max_len < 4) break :end_of_command; + try writer.writeAll("133;D"); // End prompt + if (self.rand.boolean()) exit_code: { + if (max_len < 7) break :exit_code; + try writer.print(";{d}", .{self.rand.int(u8)}); + } + }, + + .mouse_shape => mouse_shape: { + if (max_len < 4) break :mouse_shape; + try writer.print("22;{f}", .{self.bytes().newAlphabet(alphabetic_alphabet).atMost(@min(32, max_len - 3))}); // Start prompt + }, + + .rxvt_notify => rxvt_notify: { + const prefix = "777;notify;"; + if (max_len < prefix.len) break :rxvt_notify; + var remaining = max_len; + try writer.writeAll(prefix); + remaining -= prefix.len; + remaining -= try self.bytes().newAlphabet(kv_alphabet).atMost(@min(remaining - 2, 32)).write(writer); + try writer.writeByte(';'); + remaining -= 1; + remaining -= try self.bytes().newAlphabet(osc_alphabet).atMost(remaining).write(writer); + }, + + .clipboard_operation => { + try writer.writeAll("52;"); + var remaining = max_len - 3; + if (self.rand.boolean()) { + remaining -= try self.bytes().newAlphabet(alphabetic_alphabet).atMost(1).write(writer); + } + try writer.writeByte(';'); + remaining -= 1; + if (self.rand.boolean()) { + remaining -= try self.bytes().newAlphabet(osc_alphabet).atMost(remaining).write(writer); + } + }, + + .report_pwd => report_pwd: { + const prefix = "7;file://localhost"; + if (max_len < prefix.len) break :report_pwd; + var remaining = max_len; + try writer.writeAll(prefix); + remaining -= prefix.len; + for (0..self.rand.intRangeAtMost(usize, 2, 5)) |_| { + try writer.writeByte('/'); + remaining -= 1; + remaining -= try self.bytes().newAlphabet(alphanumeric_alphabet).atMost(@min(16, remaining)).write(writer); + } + }, + + .hyperlink_start => { + try writer.writeAll("8;"); + if (self.rand.boolean()) { + try writer.print("id={f}", .{self.bytes().newAlphabet(alphanumeric_alphabet).atMost(16)}); + } + try writer.writeAll(";https://localhost"); + for (0..self.rand.intRangeAtMost(usize, 2, 5)) |_| { + try writer.print("/{f}", .{self.bytes().newAlphabet(alphanumeric_alphabet).atMost(16)}); + } + }, + + .hyperlink_end => hyperlink_end: { + if (max_len < 3) break :hyperlink_end; + try writer.writeAll("8;;"); + }, + + .conemu_progress => { + try writer.writeAll("9;"); + switch (self.rand.intRangeAtMost(u3, 0, 4)) { + 0, 3 => |c| { + try writer.print(";{d}", .{c}); + }, + 1, 2, 4 => |c| { + if (self.rand.boolean()) { + try writer.print(";{d}", .{c}); + } else { + try writer.print(";{d};{d}", .{ c, self.rand.intRangeAtMost(u8, 0, 100) }); + } + }, + else => unreachable, + } + }, + + .iterm2_notification => iterm2_notification: { + if (max_len < 3) break :iterm2_notification; + // add a prefix to ensure that this is not interpreted as a ConEmu OSC + try writer.print("9;_{f}", .{self.bytes().newAlphabet(ascii_alphabet).atMost(max_len - 3)}); + }, } } @@ -139,14 +277,11 @@ fn nextUnwrappedInvalidExact( ) Generator.Error!void { switch (k) { .random => { - var bytes_gen = self.bytes(); - try bytes_gen.next(writer, max_len); + try self.bytes().atMost(max_len).format(writer); }, .good_prefix => { - try writer.writeAll("133;"); - var bytes_gen = self.bytes(); - try bytes_gen.next(writer, max_len - 4); + try writer.print("133;{f}", .{self.bytes().atMost(max_len - 4)}); }, } } @@ -154,7 +289,7 @@ fn nextUnwrappedInvalidExact( fn bytes(self: *const Osc) Bytes { return .{ .rand = self.rand, - .alphabet = bytes_alphabet, + .alphabet = osc_alphabet, }; } diff --git a/src/synthetic/cli/Ascii.zig b/src/synthetic/cli/Ascii.zig index b2d57fa88..d416189ce 100644 --- a/src/synthetic/cli/Ascii.zig +++ b/src/synthetic/cli/Ascii.zig @@ -3,12 +3,21 @@ const Ascii = @This(); const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; -const synthetic = @import("../main.zig"); +const Bytes = @import("../Bytes.zig"); const log = std.log.scoped(.@"terminal-stream-bench"); pub const Options = struct {}; +fn checkAsciiAlphabet(c: u8) bool { + return switch (c) { + ' ' => false, + else => std.ascii.isPrint(c), + }; +} + +pub const ascii = Bytes.generateAlphabet(checkAsciiAlphabet); + /// Create a new terminal stream handler for the given arguments. pub fn create( alloc: Allocator, @@ -23,16 +32,16 @@ pub fn destroy(self: *Ascii, alloc: Allocator) void { alloc.destroy(self); } -pub fn run(self: *Ascii, writer: *std.Io.Writer, rand: std.Random) !void { - _ = self; - - var gen: synthetic.Bytes = .{ +pub fn run(_: *Ascii, writer: *std.Io.Writer, rand: std.Random) !void { + var gen: Bytes = .{ .rand = rand, - .alphabet = synthetic.Bytes.Alphabet.ascii, + .alphabet = ascii, + .min_len = 1024, + .max_len = 1024, }; while (true) { - gen.next(writer, 1024) catch |err| { + _ = gen.write(writer) catch |err| { const Error = error{ WriteFailed, BrokenPipe } || @TypeOf(err); switch (@as(Error, err)) { error.BrokenPipe => return, // stdout closed diff --git a/src/synthetic/cli/Osc.zig b/src/synthetic/cli/Osc.zig index 8250b81de..686563fc3 100644 --- a/src/synthetic/cli/Osc.zig +++ b/src/synthetic/cli/Osc.zig @@ -10,6 +10,14 @@ const log = std.log.scoped(.@"terminal-stream-bench"); pub const Options = struct { /// Probability of generating a valid value. @"p-valid": f64 = 0.5, + + style: enum { + /// Write all OSC data, including ESC ] and ST for end-to-end tests + streaming, + /// Only write data, prefixed with a length, used for testing just the + /// OSC parser. + parser, + } = .streaming, }; opts: Options, @@ -40,9 +48,21 @@ pub fn run(self: *Osc, writer: *std.Io.Writer, rand: std.Random) !void { var fixed: std.Io.Writer = .fixed(&buf); try gen.next(&fixed, buf.len); const data = fixed.buffered(); - writer.writeAll(data) catch |err| switch (err) { - error.WriteFailed => return, - }; + switch (self.opts.style) { + .streaming => { + writer.writeAll(data) catch |err| switch (err) { + error.WriteFailed => return, + }; + }, + .parser => { + writer.writeInt(usize, data.len - 3, .little) catch |err| switch (err) { + error.WriteFailed => return, + }; + writer.writeAll(data[2 .. data.len - 1]) catch |err| switch (err) { + error.WriteFailed => return, + }; + }, + } } } diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 29f414e03..f7d3c735f 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -4,10 +4,12 @@ const PageList = @This(); const std = @import("std"); +const builtin = @import("builtin"); const build_options = @import("terminal_options"); const Allocator = std.mem.Allocator; const assert = @import("../quirks.zig").inlineAssert; const fastmem = @import("../fastmem.zig"); +const tripwire = @import("../tripwire.zig"); const DoublyLinkedList = @import("../datastruct/main.zig").IntrusiveDoublyLinkedList; const color = @import("color.zig"); const kitty = @import("kitty.zig"); @@ -48,7 +50,12 @@ const Node = struct { /// The memory pool we get page nodes from. const NodePool = std.heap.MemoryPool(List.Node); +/// The standard page capacity that we use as a starting point for +/// all pages. This is chosen as a sane default that fits most terminal +/// usage to support using our pool. const std_capacity = pagepkg.std_capacity; + +/// The byte size required for a standard page. const std_size = Page.layout(std_capacity).total_size; /// The memory pool we use for page memory buffers. We use a separate pool @@ -78,7 +85,7 @@ pub const MemoryPool = struct { gen_alloc: Allocator, page_alloc: Allocator, preheat: usize, - ) !MemoryPool { + ) Allocator.Error!MemoryPool { var node_pool = try NodePool.initPreheated(gen_alloc, preheat); errdefer node_pool.deinit(); var page_pool = try PagePool.initPreheated(page_alloc, preheat); @@ -108,7 +115,6 @@ pub const MemoryPool = struct { /// The memory pool we get page nodes, pages from. pool: MemoryPool, -pool_owned: bool, /// The list of pages in the screen. pages: List, @@ -223,19 +229,30 @@ pub const Viewport = union(enum) { /// But this gives us a nice fast heuristic for determining min/max size. /// Therefore, if the page size is violated you should always also verify /// that we have enough space for the active area. -fn minMaxSize(cols: size.CellCountInt, rows: size.CellCountInt) !usize { +fn minMaxSize(cols: size.CellCountInt, rows: size.CellCountInt) usize { + // Invariant required to ensure our divCeil below cannot overflow. + comptime { + const max_rows = std.math.maxInt(size.CellCountInt); + _ = std.math.divCeil(usize, max_rows, 1) catch unreachable; + } + // Get our capacity to fit our rows. If the cols are too big, it may // force less rows than we want meaning we need more than one page to // represent a viewport. - const cap = try std_capacity.adjust(.{ .cols = cols }); + const cap = initialCapacity(cols); // Calculate the number of standard sized pages we need to represent // an active area. - const pages_exact = if (cap.rows >= rows) 1 else try std.math.divCeil( + const pages_exact = if (cap.rows >= rows) 1 else std.math.divCeil( usize, rows, cap.rows, - ); + ) catch { + // Not possible: + // - initialCapacity guarantees at least 1 row + // - numerator/denominator can't overflow because of comptime check above + unreachable; + }; // We always need at least one page extra so that we // can fit partial pages to spread our active area across two pages. @@ -255,6 +272,72 @@ fn minMaxSize(cols: size.CellCountInt, rows: size.CellCountInt) !usize { return PagePool.item_size * pages; } +/// Calculates the initial capacity for a new page for a given column +/// count. This will attempt to fit within std_size at all times so we +/// can use our memory pool, but if cols is too big, this will return a +/// larger capacity. +/// +/// The returned capacity is always guaranteed to layout properly (not +/// overflow). We are able to support capacities up to the maximum int +/// value of cols, so this will never overflow. +fn initialCapacity(cols: size.CellCountInt) Capacity { + // This is an important invariant that ensures that this function + // can never return an error. We verify here that our standard capacity + // when increased to maximum possible columns can always support at + // least one row in memory. + // + // IF THIS EVER FAILS: We probably need to modify our logic below + // to reduce other elements of the capacity (styles, graphemes, etc.). + // But, instead, I recommend taking a step back and re-evaluating + // life choices. + comptime { + var cap = std_capacity; + cap.cols = std.math.maxInt(size.CellCountInt); + const layout = Page.layout(cap); + assert(layout.total_size <= size.max_page_size); + } + + if (std_capacity.adjust( + .{ .cols = cols }, + )) |cap| { + // If we can adjust our standard capacity, we fit within the + // standard size and we're good! + return cap; + } else |err| { + // Ensure our error set doesn't change. + comptime assert(@TypeOf(err) == error{OutOfMemory}); + } + + // This code path means that our standard capacity can't even + // accommodate our column count! The only solution is to increase + // our capacity and go non-standard. + var cap: Capacity = std_capacity; + cap.cols = cols; + return cap; +} + +/// This is the page allocator we'll use for all our underlying +/// VM page allocations. +inline fn pageAllocator() Allocator { + // In tests we use our testing allocator so we can detect leaks. + if (builtin.is_test) return std.testing.allocator; + + // On non-macOS we use our standard Zig page allocator. + if (!builtin.target.os.tag.isDarwin()) return std.heap.page_allocator; + + // On macOS we want to tag our memory so we can assign it to our + // core terminal usage. + const mach = @import("../os/mach.zig"); + return mach.taggedPageAllocator(.application_specific_1); +} + +const init_tw = tripwire.module(enum { + init_memory_pool, + init_pages, + viewport_pin, + viewport_pin_track, +}, init); + /// Initialize the page. The top of the first page in the list is always the /// top of the active area of the screen (important knowledge for quickly /// setting up cursors in Screen). @@ -276,12 +359,21 @@ pub fn init( cols: size.CellCountInt, rows: size.CellCountInt, max_size: ?usize, -) !PageList { +) Allocator.Error!PageList { + const tw = init_tw; + // The screen starts with a single page that is the entire viewport, // and we'll split it thereafter if it gets too large and add more as // necessary. - var pool = try MemoryPool.init(alloc, std.heap.page_allocator, page_preheat); + try tw.check(.init_memory_pool); + var pool = try MemoryPool.init( + alloc, + pageAllocator(), + page_preheat, + ); errdefer pool.deinit(); + + try tw.check(.init_pages); var page_serial: u64 = 0; const page_list, const page_size = try initPages( &pool, @@ -291,20 +383,23 @@ pub fn init( ); // Get our minimum max size, see doc comments for more details. - const min_max_size = try minMaxSize(cols, rows); + const min_max_size = minMaxSize(cols, rows); // We always track our viewport pin to ensure this is never an allocation + try tw.check(.viewport_pin); const viewport_pin = try pool.pins.create(); viewport_pin.* = .{ .node = page_list.first.? }; var tracked_pins: PinSet = .{}; errdefer tracked_pins.deinit(pool.alloc); + + try tw.check(.viewport_pin_track); try tracked_pins.putNoClobber(pool.alloc, viewport_pin, {}); + errdefer comptime unreachable; const result: PageList = .{ .cols = cols, .rows = rows, .pool = pool, - .pool_owned = true, .pages = page_list, .page_serial = page_serial, .page_serial_min = 0, @@ -321,22 +416,65 @@ pub fn init( return result; } +const initPages_tw = tripwire.module(enum { + page_node, + page_buf_std, + page_buf_non_std, +}, initPages); + fn initPages( pool: *MemoryPool, serial: *u64, cols: size.CellCountInt, rows: size.CellCountInt, -) !struct { List, usize } { +) Allocator.Error!struct { List, usize } { + const tw = initPages_tw; + var page_list: List = .{}; var page_size: usize = 0; // Add pages as needed to create our initial viewport. - const cap = try std_capacity.adjust(.{ .cols = cols }); + const cap = initialCapacity(cols); + const layout = Page.layout(cap); + const pooled = layout.total_size <= std_size; + const page_alloc = pool.pages.arena.child_allocator; + + // Guaranteed by comptime checks in initialCapacity but + // redundant here for safety. + assert(layout.total_size <= size.max_page_size); + + // If we have an error, we need to clean up our non-standard pages + // since they're not in the pool. + errdefer { + var it = page_list.first; + while (it) |node| : (it = node.next) { + if (node.data.memory.len > std_size) { + page_alloc.free(node.data.memory); + } + } + } + var rem = rows; while (rem > 0) { + try tw.check(.page_node); const node = try pool.nodes.create(); - const page_buf = try pool.pages.create(); - // no errdefer because the pool deinit will clean these up + errdefer pool.nodes.destroy(node); + + const page_buf = if (pooled) buf: { + try tw.check(.page_buf_std); + break :buf try pool.pages.create(); + } else buf: { + try tw.check(.page_buf_non_std); + break :buf try page_alloc.alignedAlloc( + u8, + .fromByteUnits(std.heap.page_size_min), + layout.total_size, + ); + }; + errdefer if (pooled) + pool.pages.destroy(page_buf) + else + page_alloc.free(page_buf); // In runtime safety modes we have to memset because the Zig allocator // interface will always memset to 0xAA for undefined. In non-safe modes @@ -346,10 +484,7 @@ fn initPages( // Initialize the first set of pages to contain our viewport so that // the top of the first page is always the active area. node.* = .{ - .data = .initBuf( - .init(page_buf), - Page.layout(cap), - ), + .data = .initBuf(.init(page_buf), layout), .serial = serial.*, }; node.data.size.rows = @min(rem, node.data.capacity.rows); @@ -358,6 +493,7 @@ fn initPages( // Add the page to the list page_list.append(node); page_size += page_buf.len; + errdefer comptime unreachable; // Increment our serial serial.* += 1; @@ -392,9 +528,11 @@ pub inline fn pauseIntegrityChecks(self: *PageList, pause: bool) void { } const IntegrityError = error{ - TotalRowsMismatch, - ViewportPinOffsetMismatch, PageSerialInvalid, + TotalRowsMismatch, + TrackedPinInvalid, + ViewportPinOffsetMismatch, + ViewportPinInsufficientRows, }; /// Verify the integrity of the PageList. This is expensive and should @@ -435,9 +573,13 @@ fn verifyIntegrity(self: *const PageList) IntegrityError!void { return IntegrityError.TotalRowsMismatch; } - // Verify that our viewport pin row offset is correct. - if (self.viewport == .pin) pin: { - const cached_offset = self.viewport_pin_row_offset orelse break :pin; + // Verify that all our tracked pins point to valid pages. + for (self.tracked_pins.keys()) |p| { + if (!self.pinIsValid(p.*)) return error.TrackedPinInvalid; + } + + if (self.viewport == .pin) { + // Verify that our viewport pin row offset is correct. const actual_offset: usize = offset: { var offset: usize = 0; var node = self.pages.last; @@ -456,12 +598,24 @@ fn verifyIntegrity(self: *const PageList) IntegrityError!void { return error.ViewportPinOffsetMismatch; }; - if (cached_offset != actual_offset) { + if (self.viewport_pin_row_offset) |cached_offset| { + if (cached_offset != actual_offset) { + log.warn( + "PageList integrity violation: viewport pin offset mismatch cached={} actual={}", + .{ cached_offset, actual_offset }, + ); + return error.ViewportPinOffsetMismatch; + } + } + + // Ensure our viewport has enough rows. + const rows = self.total_rows - actual_offset; + if (rows < self.rows) { log.warn( - "PageList integrity violation: viewport pin offset mismatch cached={} actual={}", - .{ cached_offset, actual_offset }, + "PageList integrity violation: viewport pin rows too small rows={} needed={}", + .{ rows, self.rows }, ); - return error.ViewportPinOffsetMismatch; + return error.ViewportPinInsufficientRows; } } } @@ -487,11 +641,7 @@ pub fn deinit(self: *PageList) void { // Deallocate all the pages. We don't need to deallocate the list or // nodes because they all reside in the pool. - if (self.pool_owned) { - self.pool.deinit(); - } else { - self.pool.reset(.{ .retain_capacity = {} }); - } + self.pool.deinit(); } /// Reset the PageList back to an empty state. This is similar to @@ -507,10 +657,8 @@ pub fn reset(self: *PageList) void { // We need enough pages/nodes to keep our active area. This should // never fail since we by definition have allocated a page already // that fits our size but I'm not confident to make that assertion. - const cap = std_capacity.adjust( - .{ .cols = self.cols }, - ) catch @panic("reset: std_capacity.adjust failed"); - assert(cap.rows > 0); // adjust should never return 0 rows + const cap = initialCapacity(self.cols); + assert(cap.rows > 0); // The number of pages we need is the number of rows in the active // area divided by the row capacity of a page. @@ -609,14 +757,6 @@ pub const Clone = struct { top: point.Point, bot: ?point.Point = null, - /// The allocator source for the clone operation. If this is alloc - /// then the cloned pagelist will own and dealloc the memory on deinit. - /// If this is pool then the caller owns the memory. - memory: union(enum) { - alloc: Allocator, - pool: *MemoryPool, - }, - // If this is non-null then cloning will attempt to remap the tracked // pins into the new cloned area and will keep track of the old to // new mapping in this map. If this is null, the cloned pagelist will @@ -638,40 +778,33 @@ pub const Clone = struct { /// rows will be added to the bottom of the region to make up the difference. pub fn clone( self: *const PageList, + alloc: Allocator, opts: Clone, ) !PageList { - var it = self.pageIterator(.right_down, opts.top, opts.bot); + var it = self.pageIterator( + .right_down, + opts.top, + opts.bot, + ); - // Setup our own memory pool if we have to. - var owned_pool: ?MemoryPool = switch (opts.memory) { - .pool => null, - .alloc => |alloc| alloc: { - // First, count our pages so our preheat is exactly what we need. - var it_copy = it; - const page_count: usize = page_count: { - var count: usize = 0; - while (it_copy.next()) |_| count += 1; - break :page_count count; - }; - - // Setup our pools - break :alloc try .init( - alloc, - std.heap.page_allocator, - page_count, - ); - }, - }; - errdefer if (owned_pool) |*pool| pool.deinit(); - - // Create our memory pool we use - const pool: *MemoryPool = switch (opts.memory) { - .pool => |v| v, - .alloc => &owned_pool.?, + // First, count our pages so our preheat is exactly what we need. + var it_copy = it; + const page_count: usize = page_count: { + var count: usize = 0; + while (it_copy.next()) |_| count += 1; + break :page_count count; }; - // Our viewport pin is always undefined since our viewport in a clones - // goes back to the top + // Setup our pool + var pool: MemoryPool = try .init( + alloc, + pageAllocator(), + page_count, + ); + errdefer pool.deinit(); + + // Create our viewport. In a clone, the viewport always goes + // to the top. const viewport_pin = try pool.pins.create(); var tracked_pins: PinSet = .{}; errdefer tracked_pins.deinit(pool.alloc); @@ -697,7 +830,7 @@ pub fn clone( // Clone the page. We have to use createPageExt here because // we don't know if the source page has a standard size. const node = try createPageExt( - pool, + &pool, chunk.node.data.capacity, &page_serial, &page_size, @@ -737,12 +870,12 @@ pub fn clone( } } + // Initialize our viewport pin to point to the first cloned page + // so it points to valid memory. + viewport_pin.* = .{ .node = page_list.first.? }; + var result: PageList = .{ - .pool = pool.*, - .pool_owned = switch (opts.memory) { - .pool => false, - .alloc => true, - }, + .pool = pool, .pages = page_list, .page_serial = page_serial, .page_serial_min = 0, @@ -803,7 +936,7 @@ pub const Resize = struct { /// Resize /// TODO: docs -pub fn resize(self: *PageList, opts: Resize) !void { +pub fn resize(self: *PageList, opts: Resize) Allocator.Error!void { defer self.assertIntegrity(); if (comptime std.debug.runtime_safety) { @@ -825,7 +958,7 @@ pub fn resize(self: *PageList, opts: Resize) !void { // when increasing beyond our initial minimum max size or explicit max // size to fit the active area. const old_min_max_size = self.min_max_size; - self.min_max_size = try minMaxSize( + self.min_max_size = minMaxSize( opts.cols orelse self.cols, opts.rows orelse self.rows, ); @@ -856,6 +989,16 @@ pub fn resize(self: *PageList, opts: Resize) !void { try self.resizeCols(cols, opts.cursor); }, } + + // Various resize operations can change our total row count such + // that our viewport pin is now in the active area and has insufficient + // space. We need to check for this case and fix it up. + switch (self.viewport) { + .pin => if (self.pinIsActive(self.viewport_pin.*)) { + self.viewport = .active; + }, + .active, .top => {}, + } } /// Resize the pagelist with reflow by adding or removing columns. @@ -863,7 +1006,7 @@ fn resizeCols( self: *PageList, cols: size.CellCountInt, cursor: ?Resize.Cursor, -) !void { +) Allocator.Error!void { assert(cols != self.cols); // Update our cols. We have to do this early because grow() that we @@ -911,32 +1054,71 @@ fn resizeCols( } else null; defer if (preserved_cursor) |c| self.untrackPin(c.tracked_pin); - const first = self.pages.first.?; - var it = self.rowIterator(.right_down, .{ .screen = .{} }, null); + // Create the first node that contains our reflow. + const first_rewritten_node = node: { + const page = &self.pages.first.?.data; + const cap = page.capacity.adjust( + .{ .cols = cols }, + ) catch |err| err: { + comptime assert(@TypeOf(err) == error{OutOfMemory}); - const dst_node = try self.createPage(try first.data.capacity.adjust(.{ .cols = cols })); - dst_node.data.size.rows = 1; + // We verify all maxed out page layouts work. + var cap = page.capacity; + cap.cols = cols; + + // We're growing columns so we can only get less rows so use + // the lesser of our capacity and size so we minimize wasted + // rows. + cap.rows = @min(page.size.rows, cap.rows); + break :err cap; + }; + + const node = try self.createPage(cap); + node.data.size.rows = 1; + break :node node; + }; + + // We need to grab our rowIterator now before we rewrite our + // linked list below. + var it = self.rowIterator( + .right_down, + .{ .screen = .{} }, + null, + ); + errdefer { + // If an error occurs, we're in a pretty disastrous broken state, + // but we should still try to clean up our leaked memory. Free + // any of the remaining orphaned pages from before. If we reflowed + // successfully this will be null. + var node_: ?*Node = if (it.chunk) |chunk| chunk.node else null; + while (node_) |node| { + node_ = node.next; + self.destroyNode(node); + } + } // Set our new page as the only page. This orphans the existing pages // in the list, but that's fine since we're gonna delete them anyway. - self.pages.first = dst_node; - self.pages.last = dst_node; + self.pages.first = first_rewritten_node; + self.pages.last = first_rewritten_node; // Reflow all our rows. { - var dst_cursor = ReflowCursor.init(dst_node); + var reflow_cursor: ReflowCursor = .init(first_rewritten_node); while (it.next()) |row| { - try dst_cursor.reflowRow(self, row); + try reflow_cursor.reflowRow(self, row); - // Once we're done reflowing a page, destroy it. + // Once we're done reflowing a page, destroy it immediately. + // This frees memory and makes it more likely in memory + // constrained environments that the next reflow will work. if (row.y == row.node.data.size.rows - 1) { self.destroyNode(row.node); } } // At the end of the reflow, setup our total row cache - // log.warn("total old={} new={}", .{ self.total_rows, dst_cursor.total_rows }); - self.total_rows = dst_cursor.total_rows; + // log.warn("total old={} new={}", .{ self.total_rows, reflow_cursor.total_rows }); + self.total_rows = reflow_cursor.total_rows; } // If our total rows is less than our active rows, we need to grow. @@ -1029,20 +1211,15 @@ const ReflowCursor = struct { self: *ReflowCursor, list: *PageList, row: Pin, - ) !void { + ) Allocator.Error!void { const src_page: *Page = &row.node.data; const src_row = row.rowAndCell().row; const src_y = row.y; - - // Inherit increased styles or grapheme bytes from - // the src page we're reflowing from for new pages. - const cap = try src_page.capacity.adjust(.{ .cols = self.page.size.cols }); - const cells = src_row.cells.ptr(src_page.memory)[0..src_page.size.cols]; + // Calculate the columns in this row. First up we trim non-semantic + // rightmost blanks. var cols_len = src_page.size.cols; - - // If the row is wrapped, all empty cells are meaningful. if (!src_row.wrap) { while (cols_len > 0) { if (!cells[cols_len - 1].isEmpty()) break; @@ -1064,9 +1241,10 @@ const ReflowCursor = struct { // If this pin is in the blanks on the right and past the end // of the dst col width then we move it to the end of the dst // col width instead. - if (p.x >= cols_len) { - p.x = @min(p.x, cap.cols - 1 - self.x); - } + if (p.x >= cols_len) p.x = @min( + p.x, + self.page.size.cols - 1 - self.x, + ); // We increase our col len to at least include this pin. // This ensures that blank rows with pins are processed, @@ -1081,16 +1259,29 @@ const ReflowCursor = struct { // If this blank row was a wrap continuation somehow // then we won't need to write it since it should be // a part of the previously written row. - if (!src_row.wrap_continuation) { - self.new_rows += 1; - } + if (!src_row.wrap_continuation) self.new_rows += 1; return; } + // Inherit increased styles or grapheme bytes from the src page + // we're reflowing from for new pages. + const cap = src_page.capacity.adjust( + .{ .cols = self.page.size.cols }, + ) catch |err| err: { + comptime assert(@TypeOf(err) == error{OutOfMemory}); + + var cap = src_page.capacity; + cap.cols = self.page.size.cols; + // We're already a non-standard page. We don't want to + // inherit a massive set of rows, so cap it at our std size. + cap.rows = @min(src_page.size.rows, std_capacity.rows); + break :err cap; + }; + // Our row isn't blank, write any new rows we deferred. while (self.new_rows > 0) { - self.new_rows -= 1; try self.cursorScrollOrNewPage(list, cap); + self.new_rows -= 1; } self.copyRowMetadata(src_row); @@ -1118,8 +1309,93 @@ const ReflowCursor = struct { } } - const cell = &cells[x]; - x += 1; + if (self.writeCell( + list, + &cells[x], + src_page, + )) |result| switch (result) { + // Wrote the cell, move to the next. + .success => x += 1, + + // Wrote the cell but request to skip the next so skip it. + // This is used for things like spacers. + .skip_next => { + // Remap any tracked pins at the skipped position (x+1) + // since we won't process that cell in the loop. + const pin_keys = list.tracked_pins.keys(); + for (pin_keys) |p| { + if (&p.node.data != src_page or + p.y != src_y or + p.x != x + 1) continue; + + p.node = self.node; + p.x = self.x; + p.y = self.y; + } + + x += 2; + }, + + // Didn't write the cell, repeat writing this same cell. + .repeat => {}, + } else |err| switch (err) { + // System out of memory, we can't fix this. + error.OutOfMemory => return error.OutOfMemory, + + // We reached the capacity of a single page and can't + // add any more of some type of managed memory. When this + // happens we split out the current row we're working on + // into a new page and continue from there. + error.OutOfSpace => if (self.y == 0) { + // If we're already on the first-row, we can't split + // any further, so we just ignore bad cells and take + // corrupted (but valid) cell contents. + log.warn("reflowRow OutOfSpace on first row, discarding cell managed memory", .{}); + x += 1; + self.cursorForward(); + } else { + // Move our last row to a new page. + try self.moveLastRowToNewPage(list, cap); + + // Do NOT increment x so that we retry writing + // the same existing cell. + }, + } + } + + // If the source row isn't wrapped then we should scroll afterwards. + if (!src_row.wrap) { + self.new_rows += 1; + } + } + + /// Write a cell. On error, this will not unwrite the cell but + /// the cell may be incomplete (but valid). For example, if the source + /// cell is styled and we failed to allocate space for styles, the + /// written cell may not be styled but it is valid. + /// + /// The key failure to recognize for callers is when we can't increase + /// capacity in our destination page. In this case, the caller may want + /// to split the page at this row, rewrite the row into a new page + /// and continue from there. + /// + /// But this function guarantees the terminal/page will be in a + /// coherent state even on error. + fn writeCell( + self: *ReflowCursor, + list: *PageList, + cell: *const pagepkg.Cell, + src_page: *const Page, + ) IncreaseCapacityError!enum { + success, + repeat, + skip_next, + } { + // Initialize self.page_cell with basic, unmanaged memory contents. + { + // This must not fail because we want to make sure we atomically + // setup our page cell to be valid. + errdefer comptime unreachable; // Copy cell contents. switch (cell.content_tag) { @@ -1139,16 +1415,15 @@ const ReflowCursor = struct { .wide = .spacer_head, }; - // Decrement the source position so that when we - // loop we'll process this source cell again, - // since we can't copy it into a spacer head. - x -= 1; - // Move to the next row (this sets pending wrap // which will cause us to wrap on the next // iteration). self.cursorForward(); - continue; + + // Decrement the source position so that when we + // loop we'll process this source cell again, + // since we can't copy it into a spacer head. + return .repeat; } else { self.page_cell.* = cell.*; } @@ -1159,9 +1434,9 @@ const ReflowCursor = struct { self.page_cell.content.codepoint = 0; self.page_cell.wide = .narrow; self.cursorForward(); + // Skip spacer tail so it doesn't cause a wrap. - x += 1; - continue; + return .skip_next; }, .spacer_tail => if (self.page.size.cols > 1) { @@ -1171,14 +1446,14 @@ const ReflowCursor = struct { // characters are just destroyed and replaced // with empty narrow cells, so we should just // discard any spacer tails. - continue; + return .success; }, .spacer_head => { // Spacer heads should be ignored. If we need a // spacer head in our reflowed page, it is added // when processing the wide cell it belongs to. - continue; + return .success; }, }, @@ -1189,7 +1464,7 @@ const ReflowCursor = struct { // data associated with them so we can fast path them. self.page_cell.* = cell.*; self.cursorForward(); - continue; + return .success; }, } @@ -1200,185 +1475,279 @@ const ReflowCursor = struct { self.page_cell.hyperlink = false; self.page_cell.style_id = stylepkg.default_id; - // std.log.warn("\nsrc_y={} src_x={} dst_y={} dst_x={} dst_cols={} cp={X} wide={} page_cell_wide={}", .{ - // src_y, - // x, - // self.y, - // self.x, - // self.page.size.cols, - // cell.content.codepoint, - // cell.wide, - // self.page_cell.wide, - // }); - - // Copy grapheme data. - if (cell.content_tag == .codepoint_grapheme) { - // Copy the graphemes - const cps = src_page.lookupGrapheme(cell).?; - - // If our page can't support an additional cell - // with graphemes then we increase capacity. - if (self.page.graphemeCount() >= self.page.graphemeCapacity()) { - 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. - try self.page.setGraphemes(self.page_row, self.page_cell, cps); - } - - // Copy hyperlink data. - if (cell.hyperlink) { - 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 then we increase capacity. - if (self.page.hyperlinkCount() >= self.page.hyperlinkCapacity()) { - 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 |err| id: { - // If the add failed then either the set needs to grow - // or it needs to be rehashed. Either one of those can - // be accomplished by adjusting capacity, either with - // no actual change or with an increased hyperlink cap. - try self.adjustCapacity(list, switch (err) { - error.OutOfMemory => .{ - .hyperlink_bytes = cap.hyperlink_bytes * 2, - }, - error.NeedsRehash => .{}, - }); - - // We assume this one will succeed. We dupe the link - // again, and don't have to worry about the other one - // because adjusting the capacity naturally clears up - // any managed memory not associated with a cell yet. - break :id try self.page.hyperlink_set.addWithIdContext( - self.page.memory, - try src_link.dupe(src_page, self.page), - src_id, - .{ .page = self.page }, - ); - } orelse src_id; - - // We expect this to succeed due to the - // hyperlinkCapacity check we did before. - try self.page.setHyperlink( - self.page_row, - self.page_cell, - dst_id, - ); - } - - // Copy style data. - if (cell.hasStyling()) { - const style = src_page.styles.get( - src_page.memory, - cell.style_id, - ).*; - - const id = self.page.styles.addWithId( - self.page.memory, - style, - cell.style_id, - ) catch |err| id: { - // If the add failed then either the set needs to grow - // or it needs to be rehashed. Either one of those can - // be accomplished by adjusting capacity, either with - // no actual change or with an increased style cap. - try self.adjustCapacity(list, switch (err) { - error.OutOfMemory => .{ - .styles = cap.styles * 2, - }, - error.NeedsRehash => .{}, - }); - - // We assume this one will succeed. - break :id try self.page.styles.addWithId( - self.page.memory, - style, - cell.style_id, - ); - } orelse cell.style_id; - - self.page_row.styled = true; - - self.page_cell.style_id = id; - } - if (comptime build_options.kitty_graphics) { // Copy Kitty virtual placeholder status if (cell.codepoint() == kitty.graphics.unicode.placeholder) { self.page_row.kitty_virtual_placeholder = true; } } - - self.cursorForward(); } - // If the source row isn't wrapped then we should scroll afterwards. - if (!src_row.wrap) { - self.new_rows += 1; + // std.log.warn("\nsrc_y={} src_x={} dst_y={} dst_x={} dst_cols={} cp={X} wide={} page_cell_wide={}", .{ + // src_y, + // x, + // self.y, + // self.x, + // self.page.size.cols, + // cell.content.codepoint, + // cell.wide, + // self.page_cell.wide, + // }); + + // From this point on we're moving on to failable, managed memory. + // If we reach an error, we do the minimal cleanup necessary to + // not leave dangling memory but otherwise we gracefully degrade + // into some functional but not strictly correct cell. + + // Copy grapheme data. + if (cell.content_tag == .codepoint_grapheme) { + // Copy the graphemes + const cps = src_page.lookupGrapheme(cell).?; + + // If our page can't support an additional cell + // with graphemes then we increase capacity. + if (self.page.graphemeCount() >= self.page.graphemeCapacity()) { + try self.increaseCapacity( + list, + .grapheme_bytes, + ); + } + + // Attempt to allocate the space that would be required + // for these graphemes, and if it's not available, then + // increase capacity. Keep trying until we succeed. + while (true) { + if (self.page.grapheme_alloc.alloc( + u21, + self.page.memory, + cps.len, + )) |slice| { + self.page.grapheme_alloc.free( + self.page.memory, + slice, + ); + break; + } else |_| { + // Grow our capacity until we can fit the extra bytes. + try self.increaseCapacity(list, .grapheme_bytes); + } + } + + self.page.setGraphemes( + self.page_row, + self.page_cell, + cps, + ) catch |err| { + // This shouldn't fail since we made sure we have space + // above. There is no reasonable behavior we can take here + // so we have a warn level log. This is ALMOST non-recoverable, + // though we choose to recover by corrupting the cell + // to a non-grapheme codepoint. + log.err("setGraphemes failed after capacity increase err={}", .{err}); + if (comptime std.debug.runtime_safety) { + // Force a crash with safe builds. + unreachable; + } + + // Unsafe builds we throw away grapheme data! + self.page_cell.content_tag = .codepoint; + self.page_cell.content = .{ .codepoint = 0xFFFD }; + }; } + + // Copy hyperlink data. + if (cell.hyperlink) hyperlink: { + 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 then we increase capacity. + if (self.page.hyperlinkCount() >= self.page.hyperlinkCapacity()) { + try self.increaseCapacity(list, .hyperlink_bytes); + } + + // 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, + }; + // Keep trying until we have enough capacity. + while (true) { + 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, + ); + break; + } else |_| { + // Grow our capacity until we can fit the extra bytes. + try self.increaseCapacity( + list, + .string_bytes, + ); + } + } + + const dst_link = src_link.dupe( + src_page, + self.page, + ) catch |err| { + // This shouldn't fail since we did a capacity + // check above. + log.err("link dupe failed with capacity check err={}", .{err}); + if (comptime std.debug.runtime_safety) { + // Force a crash with safe builds. + unreachable; + } + + break :hyperlink; + }; + + const dst_id = self.page.hyperlink_set.addWithIdContext( + self.page.memory, + dst_link, + src_id, + .{ .page = self.page }, + ) catch |err| id: { + // Always free our original link in case the increaseCap + // call fails so we aren't leaking memory. + dst_link.free(self.page); + + // 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 increasing capacity, either with + // no actual change or with an increased hyperlink cap. + try self.increaseCapacity(list, switch (err) { + error.OutOfMemory => .hyperlink_bytes, + error.NeedsRehash => null, + }); + + // We need to recreate the link into the new page. + const dst_link2 = src_link.dupe( + src_page, + self.page, + ) catch |err2| { + // This shouldn't fail since we did a capacity + // check above. + log.err("link dupe failed with capacity check err={}", .{err2}); + if (comptime std.debug.runtime_safety) { + // Force a crash with safe builds. + unreachable; + } + + break :hyperlink; + }; + + // We assume this one will succeed. We dupe the link + // again, and don't have to worry about the other one + // because increasing the capacity naturally clears up + // any managed memory not associated with a cell yet. + break :id self.page.hyperlink_set.addWithIdContext( + self.page.memory, + dst_link2, + src_id, + .{ .page = self.page }, + ) catch |err2| { + // This shouldn't happen since we increased capacity + // above so we handle it like the other similar + // cases and log it, crash in safe builds, and + // remove the hyperlink in unsafe builds. + log.err( + "addWithIdContext failed after capacity increase err={}", + .{err2}, + ); + if (comptime std.debug.runtime_safety) { + // Force a crash with safe builds. + unreachable; + } + + dst_link2.free(self.page); + break :hyperlink; + }; + } orelse src_id; + + // We expect this to succeed due to the hyperlinkCapacity + // check we did before. If it doesn't succeed let's + // log it, crash (in safe builds), and clear our state. + self.page.setHyperlink( + self.page_row, + self.page_cell, + dst_id, + ) catch |err| { + log.err( + "setHyperlink failed after capacity increase err={}", + .{err}, + ); + if (comptime std.debug.runtime_safety) { + // Force a crash with safe builds. + unreachable; + } + + // Unsafe builds we throw away hyperlink data! + self.page.hyperlink_set.release(self.page.memory, dst_id); + self.page_cell.hyperlink = false; + break :hyperlink; + }; + } + + // Copy style data. + if (cell.hasStyling()) style: { + const style = src_page.styles.get( + src_page.memory, + cell.style_id, + ).*; + + const id = self.page.styles.addWithId( + self.page.memory, + style, + cell.style_id, + ) 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 increasing capacity, either with + // no actual change or with an increased style cap. + try self.increaseCapacity(list, switch (err) { + error.OutOfMemory => .styles, + error.NeedsRehash => null, + }); + + // We assume this one will succeed. + break :id self.page.styles.addWithId( + self.page.memory, + style, + cell.style_id, + ) catch |err2| { + // Should not fail since we just modified capacity + // above. Log it, crash in safe builds, clear style + // in unsafe builds. + log.err( + "addWithId failed after capacity increase err={}", + .{err2}, + ); + if (comptime std.debug.runtime_safety) { + // Force a crash with safe builds. + unreachable; + } + + self.page_cell.style_id = stylepkg.default_id; + break :style; + }; + } orelse cell.style_id; + + self.page_row.styled = true; + self.page_cell.style_id = id; + } + + self.cursorForward(); + return .success; } /// Create a new page in the provided list with the provided @@ -1397,7 +1766,7 @@ const ReflowCursor = struct { self: *ReflowCursor, list: *PageList, cap: Capacity, - ) !void { + ) Allocator.Error!void { assert(self.y == self.page.size.rows - 1); assert(!self.pending_wrap); @@ -1406,16 +1775,50 @@ const ReflowCursor = struct { const old_row = self.page_row; const old_x = self.x; + // Our total row count never changes, because we're removing one + // row from the last page and moving it into a new page. + const old_total_rows = self.total_rows; + defer self.total_rows = old_total_rows; + try self.cursorNewPage(list, cap); + assert(self.node != old_node); + assert(self.y == 0); + + // We have no cleanup for our old state from here on out. No failures! + errdefer comptime unreachable; // Restore the x position of the cursor. self.cursorAbsolute(old_x, 0); - // We expect to have enough capacity to clone the row. - try self.page.cloneRowFrom(old_page, self.page_row, old_row); + // Copy our old data. This should NOT fail because we have the + // capacity of the old page which already fits the data we requested. + self.page.cloneRowFrom( + old_page, + self.page_row, + old_row, + ) catch |err| { + log.err( + "error cloning single row for moveLastRowToNewPage err={}", + .{err}, + ); + @panic("unexpected copy row failure"); + }; + + // Move any tracked pins from that last row into this new node. + { + const pin_keys = list.tracked_pins.keys(); + for (pin_keys) |p| { + if (&p.node.data != old_page or + p.y != old_page.size.rows - 1) continue; + + p.node = self.node; + p.y = self.y; + // p.x remains the same since we're copying the row as-is + } + } // Clear the row from the old page and truncate it. - old_page.clearCells(old_row, 0, self.page.size.cols); + old_page.clearCells(old_row, 0, old_page.size.cols); old_page.size.rows -= 1; // If that was the last row in that page @@ -1426,27 +1829,31 @@ const ReflowCursor = struct { } } - /// Adjust the capacity of the current page. - fn adjustCapacity( + /// Increase the capacity of the current page. + fn increaseCapacity( self: *ReflowCursor, list: *PageList, - adjustment: AdjustCapacity, - ) !void { + adjustment: ?IncreaseCapacity, + ) IncreaseCapacityError!void { const old_x = self.x; const old_y = self.y; const old_total_rows = self.total_rows; - self.* = .init(node: { + const node = node: { // Pause integrity checks because the total row count won't // be correct during a reflow. list.pauseIntegrityChecks(true); defer list.pauseIntegrityChecks(false); - break :node try list.adjustCapacity( + break :node try list.increaseCapacity( self.node, adjustment, ); - }); + }; + // We must not fail after this, we've modified our self.node + // and we need to fix it up. + errdefer comptime unreachable; + self.* = .init(node); self.cursorAbsolute(old_x, old_y); self.total_rows = old_total_rows; } @@ -1497,17 +1904,17 @@ const ReflowCursor = struct { self: *ReflowCursor, list: *PageList, cap: Capacity, - ) !void { + ) Allocator.Error!void { // Remember our new row count so we can restore it // after reinitializing our cursor on the new page. const new_rows = self.new_rows; const node = try list.createPage(cap); + errdefer comptime unreachable; node.data.size.rows = 1; list.pages.insertAfter(self.node, node); self.* = .init(node); - self.new_rows = new_rows; } @@ -1517,7 +1924,7 @@ const ReflowCursor = struct { self: *ReflowCursor, list: *PageList, cap: Capacity, - ) !void { + ) Allocator.Error!void { // The functions below may overwrite self so we need to cache // our total rows. We add one because no matter what when this // returns we'll have one more row added. @@ -1575,11 +1982,11 @@ const ReflowCursor = struct { } }; -fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { +fn resizeWithoutReflow(self: *PageList, opts: Resize) Allocator.Error!void { // We only set the new min_max_size if we're not reflowing. If we are // reflowing, then resize handles this for us. const old_min_max_size = self.min_max_size; - self.min_max_size = if (!opts.reflow) try minMaxSize( + self.min_max_size = if (!opts.reflow) minMaxSize( opts.cols orelse self.cols, opts.rows orelse self.rows, ) else old_min_max_size; @@ -1717,7 +2124,7 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { // area, since that will lead to all sorts of problems. switch (self.viewport) { .pin => if (self.pinIsActive(self.viewport_pin.*)) { - self.viewport = .{ .active = {} }; + self.viewport = .active; }, .active, .top => {}, } @@ -1735,26 +2142,44 @@ fn resizeWithoutReflowGrowCols( self: *PageList, cols: size.CellCountInt, chunk: PageIterator.Chunk, -) !void { +) Allocator.Error!void { assert(cols > self.cols); const page = &chunk.node.data; - const cap = try page.capacity.adjust(.{ .cols = cols }); // Update our col count const old_cols = self.cols; - self.cols = cap.cols; + self.cols = cols; errdefer self.cols = old_cols; // Unlikely fast path: we have capacity in the page. This // is only true if we resized to less cols earlier. - if (page.capacity.cols >= cap.cols) { - page.size.cols = cap.cols; + if (page.capacity.cols >= cols) { + page.size.cols = cols; return; } // Likely slow path: we don't have capacity, so we need // to allocate a page, and copy the old data into it. + // Try to fit our new column size into our existing page capacity. + // If that doesn't work then use a non-standard page with the + // given columns. + const cap = page.capacity.adjust( + .{ .cols = cols }, + ) catch |err| err: { + comptime assert(@TypeOf(err) == error{OutOfMemory}); + + // We verify all maxed out page layouts don't overflow, + var cap = page.capacity; + cap.cols = cols; + + // We're growing columns so we can only get less rows so use + // the lesser of our capacity and size so we minimize wasted + // rows. + cap.rows = @min(page.size.rows, cap.rows); + break :err cap; + }; + // On error, we need to undo all the pages we've added. const prev = chunk.node.prev; errdefer { @@ -1816,6 +2241,39 @@ fn resizeWithoutReflowGrowCols( assert(copied == len); assert(prev_page.size.rows <= prev_page.capacity.rows); + + // Remap any tracked pins that pointed to rows we just copied to prev. + const pin_keys = self.tracked_pins.keys(); + for (pin_keys) |p| { + if (p.node != chunk.node or p.y >= len) continue; + p.node = prev_node; + p.y += prev_page.size.rows - len; + } + } + + // If we have an error, we clear the rows we just added to our prev page. + const prev_copied = copied; + errdefer if (prev_copied > 0) { + const prev_page = &prev.?.data; + const prev_size = prev_page.size.rows - prev_copied; + const prev_rows = prev_page.rows.ptr(prev_page.memory)[prev_size..prev_page.size.rows]; + for (prev_rows) |*row| prev_page.clearCells( + row, + 0, + prev_page.size.cols, + ); + prev_page.size.rows = prev_size; + }; + + // We delete any of the nodes we added. + errdefer { + var it = chunk.node.prev; + while (it) |node| { + if (node == prev) break; + it = node.prev; + self.pages.remove(node); + self.destroyNode(node); + } } // We need to loop because our col growth may force us @@ -1830,19 +2288,33 @@ fn resizeWithoutReflowGrowCols( // Perform the copy const y_start = copied; - const y_end = copied + len; - const src_rows = page.rows.ptr(page.memory)[y_start..y_end]; + const src_rows = page.rows.ptr(page.memory)[y_start .. copied + len]; const dst_rows = new_node.data.rows.ptr(new_node.data.memory)[0..len]; for (dst_rows, src_rows) |*dst_row, *src_row| { new_node.data.size.rows += 1; - errdefer new_node.data.size.rows -= 1; - try new_node.data.cloneRowFrom( + if (new_node.data.cloneRowFrom( page, dst_row, src_row, - ); + )) |_| { + copied += 1; + } else |err| { + // I don't THINK this should be possible, because while our + // row count may diminish due to the adjustment, our + // prior capacity should have been sufficient to hold all the + // managed memory. + log.warn( + "unexpected cloneRowFrom failure during resizeWithoutReflowGrowCols: {}", + .{err}, + ); + + // We can actually safely handle this though by exiting + // this loop early and cutting our copy short. + new_node.data.size.rows -= 1; + break; + } } - copied = y_end; + const y_end = copied; // Insert our new page self.pages.insertBefore(chunk.node, new_node); @@ -1859,6 +2331,10 @@ fn resizeWithoutReflowGrowCols( } assert(copied == page.size.rows); + // Our prior errdeferes are invalid after this point so ensure + // we don't have any more errors. + errdefer comptime unreachable; + // Remove the old page. // Deallocate the old page. self.pages.remove(chunk.node); @@ -1978,6 +2454,12 @@ pub const Scroll = union(enum) { pub fn scroll(self: *PageList, behavior: Scroll) void { defer self.assertIntegrity(); + // Special case no-scrollback mode to never allow scrolling. + if (self.explicit_max_size == 0) { + self.viewport = .active; + return; + } + switch (behavior) { .active => self.viewport = .active, .top => self.viewport = .top, @@ -2220,7 +2702,7 @@ fn scrollPrompt(self: *PageList, delta: isize) void { /// Clear the screen by scrolling written contents up into the scrollback. /// This will not update the viewport. -pub fn scrollClear(self: *PageList) !void { +pub fn scrollClear(self: *PageList) Allocator.Error!void { defer self.assertIntegrity(); // Go through the active area backwards to find the first non-empty @@ -2250,6 +2732,166 @@ pub fn scrollClear(self: *PageList) !void { for (0..non_empty) |_| _ = try self.grow(); } +/// Compact a page to use the minimum required memory for the contents +/// it stores. Returns the new node pointer if compaction occurred, or null +/// if the page was already compact or compaction would not provide meaningful +/// savings. +/// +/// The current design of PageList at the time of writing this doesn't +/// allow for smaller than `std_size` nodes so if the current node's backing +/// page is standard size or smaller, no compaction will occur. In the +/// future we should fix this up. +/// +/// If this returns OOM, the PageList is left unchanged and no dangling +/// memory references exist. It is safe to ignore the error and continue using +/// the uncompacted page. +pub fn compact(self: *PageList, node: *List.Node) Allocator.Error!?*List.Node { + defer self.assertIntegrity(); + const page: *Page = &node.data; + + // We should never have empty rows in our pagelist anyways... + assert(page.size.rows > 0); + + // We never compact standard size or smaller pages because changing + // the capacity to something smaller won't save memory. + if (page.memory.len <= std_size) return null; + + // Compute the minimum capacity required for this page's content + const req_cap = page.exactRowCapacity(0, page.size.rows); + const new_size = Page.layout(req_cap).total_size; + const old_size = page.memory.len; + if (new_size >= old_size) return null; + + // Create the new smaller page + const new_node = try self.createPage(req_cap); + errdefer self.destroyNode(new_node); + const new_page: *Page = &new_node.data; + new_page.size = page.size; + new_page.dirty = page.dirty; + new_page.cloneFrom( + page, + 0, + page.size.rows, + ) catch |err| { + // cloneFrom should not fail when compacting since req_cap is + // computed to exactly fit the source content and our expectation + // of exactRowCapacity ensures it can fit all the requested + // data. + log.err("compact clone failed err={}", .{err}); + + // In this case, let's gracefully degrade by pretending we + // didn't need to compact. + self.destroyNode(new_node); + return null; + }; + + // Fix up all tracked pins to point to the new page + const pin_keys = self.tracked_pins.keys(); + for (pin_keys) |p| { + if (p.node != node) continue; + p.node = new_node; + } + + // Insert the new page and destroy the old one + self.pages.insertBefore(node, new_node); + self.pages.remove(node); + self.destroyNode(node); + + new_page.assertIntegrity(); + return new_node; +} + +pub const SplitError = error{ + // Allocator OOM + OutOfMemory, + // Page can't be split further because it is already a single row. + OutOfSpace, +}; + +/// Split the given node in the PageList at the given pin. +/// +/// The row at the pin and after will be moved into a new page with +/// the same capacity as the original page. Alternatively, you can "split +/// above" by splitting the row following the desired split row. +/// +/// Since the split happens below the pin, the pin remains valid. +pub fn split( + self: *PageList, + p: Pin, +) SplitError!void { + if (build_options.slow_runtime_safety) assert(self.pinIsValid(p)); + + // Ran into a bug that I can only explain via aliasing. If a tracked + // pin is passed in, its possible Zig will alias the memory and then + // when we modify it later it updates our p here. Copying the node + // fixes this. + const original_node = p.node; + const page: *Page = &original_node.data; + + // A page that is already 1 row can't be split. In the future we can + // theoretically maybe split by soft-wrapping multiple pages but that + // seems crazy and the rest of our PageList can't handle heterogeneously + // sized pages today. + if (page.size.rows <= 1) return error.OutOfSpace; + + // Splitting at row 0 is a no-op since there's nothing before the split point. + if (p.y == 0) return; + + // At this point we're doing actual modification so make sure + // on the return that we're good. + defer self.assertIntegrity(); + + // Create a new node with the same capacity of managed memory. + const target = try self.createPage(page.capacity); + errdefer self.destroyNode(target); + + // Determine how many rows we're copying + const y_start = p.y; + const y_end = page.size.rows; + target.data.size.rows = y_end - y_start; + assert(target.data.size.rows <= target.data.capacity.rows); + + // Copy our old data. This should NOT fail because we have the + // capacity of the old page which already fits the data we requested. + target.data.cloneFrom(page, y_start, y_end) catch |err| { + log.err( + "error cloning rows for split err={}", + .{err}, + ); + + // Rather than crash, we return an OutOfSpace to show that + // we couldn't split and let our callers gracefully handle it. + // Realistically though... this should not happen. + return error.OutOfSpace; + }; + + // From this point forward there is no going back. We have no + // error handling. It is possible but we haven't written it. + errdefer comptime unreachable; + + // Move any tracked pins from the copied rows + for (self.tracked_pins.keys()) |tracked| { + if (&tracked.node.data != page or + tracked.y < p.y) continue; + + tracked.node = target; + tracked.y -= p.y; + // p.x remains the same since we're copying the row as-is + } + + // Clear our rows + for (page.rows.ptr(page.memory)[y_start..y_end]) |*row| { + page.clearCells( + row, + 0, + page.size.cols, + ); + } + page.size.rows -= y_end - y_start; + + self.pages.insertAfter(original_node, target); +} + /// This represents the state necessary to render a scrollbar for this /// PageList. It has the total size, the offset, and the size of the viewport. pub const Scrollbar = struct { @@ -2300,6 +2942,17 @@ pub const Scrollbar = struct { /// is (arbitrary pins are expensive). The caller should take care to only /// call this as needed and not too frequently. pub fn scrollbar(self: *PageList) Scrollbar { + // If we have no scrollback, special case no scrollbar. + // We need to do this because the way PageList works is that + // it always has SOME extra space (due to the way we allocate by page). + // So even with no scrollback we have some growth. It is architecturally + // much simpler to just hide that for no-scrollback cases. + if (self.explicit_max_size == 0) return .{ + .total = self.rows, + .offset = 0, + .len = self.rows, + }; + return .{ .total = self.total_rows, .offset = self.viewportRowOffset(), @@ -2401,18 +3054,6 @@ pub fn maxSize(self: *const PageList) usize { return @max(self.explicit_max_size, self.min_max_size); } -/// Returns true if we need to grow into our active area. -inline fn growRequiredForActive(self: *const PageList) bool { - var rows: usize = 0; - var page = self.pages.last; - while (page) |p| : (page = p.prev) { - rows += p.data.size.rows; - if (rows >= self.rows) return false; - } - - return true; -} - /// Grow the active area by exactly one row. /// /// This may allocate, but also may not if our current page has more @@ -2420,7 +3061,7 @@ inline fn growRequiredForActive(self: *const PageList) bool { /// adhere to max_size. /// /// This returns the newly allocated page node if there is one. -pub fn grow(self: *PageList) !?*List.Node { +pub fn grow(self: *PageList) Allocator.Error!?*List.Node { defer self.assertIntegrity(); const last = self.pages.last.?; @@ -2437,6 +3078,10 @@ pub fn grow(self: *PageList) !?*List.Node { // Slower path: we have no space, we need to allocate a new page. + // Get the layout first so our failable work is done early. + // We'll need this for both paths. + const cap = initialCapacity(self.cols); + // If allocation would exceed our max size, we prune the first page. // We don't need to reallocate because we can simply reuse that first // page. @@ -2449,22 +3094,21 @@ pub fn grow(self: *PageList) !?*List.Node { self.pages.first != self.pages.last and self.page_size + PagePool.item_size > self.maxSize()) prune: { - // If we need to add more memory to ensure our active area is - // satisfied then we do not prune. - if (self.growRequiredForActive()) break :prune; - - const layout = Page.layout(try std_capacity.adjust(.{ .cols = self.cols })); - - // Get our first page and reset it to prepare for reuse. const first = self.pages.popFirst().?; assert(first != last); - const buf = first.data.memory; - @memset(buf, 0); - // Decrease our total row count from the pruned page and then - // add one for our new row. + // Decrease our total row count from the pruned page self.total_rows -= first.data.size.rows; - self.total_rows += 1; + + // If our total row count is now less than our required + // rows then we can't prune. The "+ 1" is because we'll add one + // more row below. + if (self.total_rows + 1 < self.rows) { + self.pages.prepend(first); + assert(self.pages.first == first); + self.total_rows += first.data.size.rows; + break :prune; + } // If we have a pin viewport cache then we need to update it. if (self.viewport == .pin) viewport: { @@ -2482,21 +3126,8 @@ pub fn grow(self: *PageList) !?*List.Node { } } - // Initialize our new page and reinsert it as the last - first.data = .initBuf(.init(buf), layout); - first.data.size.rows = 1; - self.pages.insertAfter(last, first); - - // We also need to reset the serial number. Since this is the only - // place we ever reuse a serial number, we also can safely set - // page_serial_min to be one more than the old serial because we - // only ever prune the oldest pages. - self.page_serial_min = first.serial + 1; - first.serial = self.page_serial; - self.page_serial += 1; - // Update any tracked pins that point to this page to point to the - // new first page to the top-left. + // new first page to the top-left, and mark them as garbage. const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { if (p.node != first) continue; @@ -2507,6 +3138,31 @@ pub fn grow(self: *PageList) !?*List.Node { } self.viewport_pin.garbage = false; + // Non-standard pages can't be reused, just destroy them. + if (first.data.memory.len > std_size) { + self.destroyNode(first); + break :prune; + } + + // Reset our memory + const buf = first.data.memory; + @memset(buf, 0); + assert(buf.len <= std_size); + + // Initialize our new page and reinsert it as the last + first.data = .initBuf(.init(buf), Page.layout(cap)); + first.data.size.rows = 1; + self.pages.insertAfter(last, first); + self.total_rows += 1; + + // We also need to reset the serial number. Since this is the only + // place we ever reuse a serial number, we also can safely set + // page_serial_min to be one more than the old serial because we + // only ever prune the oldest pages. + self.page_serial_min = first.serial + 1; + first.serial = self.page_serial; + self.page_serial += 1; + // In this case we do NOT need to update page_size because // we're reusing an existing page so nothing has changed. @@ -2515,7 +3171,7 @@ pub fn grow(self: *PageList) !?*List.Node { } // We need to allocate a new memory buffer. - const next_node = try self.createPage(try std_capacity.adjust(.{ .cols = self.cols })); + const next_node = try self.createPage(cap); // we don't errdefer this because we've added it to the linked // list and its fine to have dangling unused pages. self.pages.append(next_node); @@ -2531,75 +3187,82 @@ pub fn grow(self: *PageList) !?*List.Node { return next_node; } -/// Adjust the capacity of the given page in the list. -pub const AdjustCapacity = struct { - /// Adjust the number of styles in the page. This may be - /// rounded up if necessary to fit alignment requirements, - /// but it will never be rounded down. - styles: ?usize = null, - - /// Adjust the number of available grapheme bytes in the page. - grapheme_bytes: ?usize = null, - - /// Adjust the number of available hyperlink bytes in the page. - hyperlink_bytes: ?usize = null, - - /// Adjust the number of available string bytes in the page. - string_bytes: ?usize = null, +/// Possible dimensions to increase capacity for. +pub const IncreaseCapacity = enum { + styles, + grapheme_bytes, + hyperlink_bytes, + string_bytes, }; -pub const AdjustCapacityError = Allocator.Error || Page.CloneFromError; +pub const IncreaseCapacityError = error{ + // An actual system OOM trying to allocate memory. + OutOfMemory, -/// Adjust the capacity of the given page in the list. This should -/// be used in cases where OutOfMemory is returned by some operation -/// i.e to increase style counts, grapheme counts, etc. + // The existing page is already at max capacity for the given + // adjustment. The caller must create a new page, remove data from + // the old page, etc. (up to the caller). + OutOfSpace, +}; + +/// Increase the capacity of the given page node in the given direction. +/// This will always allocate a new node and remove the old node, so the +/// existing node pointer will be invalid after this call. The newly created +/// node on success is returned. /// -/// Adjustment works by increasing the capacity of the desired -/// dimension to a certain amount and increases the memory allocation -/// requirement for the backing memory of the page. We currently -/// never split pages or anything like that. Because increased allocation -/// has to happen outside our memory pool, its generally much slower -/// so pages should be sized to be large enough to handle all but -/// exceptional cases. +/// The increase amount is at the control of the PageList implementation, +/// but is guaranteed to always increase by at least one unit in the +/// given dimension. Practically, we'll always increase by much more +/// (we currently double every time) but callers shouldn't depend on that. +/// The only guarantee is some amount of growth. /// -/// This can currently only INCREASE capacity size. It cannot -/// decrease capacity size. This limitation is only because we haven't -/// yet needed that use case. If we ever do, this can be added. Currently -/// any requests to decrease will be ignored. -pub fn adjustCapacity( +/// Adjustment can be null if you want to recreate, reclone the page +/// with the same capacity. This is a special case used for rehashing since +/// the logic is otherwise the same. In this case, OutOfMemory is the +/// only possible error. +pub fn increaseCapacity( self: *PageList, node: *List.Node, - adjustment: AdjustCapacity, -) AdjustCapacityError!*List.Node { + adjustment: ?IncreaseCapacity, +) IncreaseCapacityError!*List.Node { defer self.assertIntegrity(); const page: *Page = &node.data; - // We always start with the base capacity of the existing page. This - // ensures we never shrink from what we need. + // Apply our adjustment var cap = page.capacity; + if (adjustment) |v| switch (v) { + inline else => |tag| { + const field_name = @tagName(tag); + const Int = @FieldType(Capacity, field_name); + const old = @field(cap, field_name); - // All ceilPowerOfTwo is unreachable because we're always same or less - // bit width so maxInt is always possible. - if (adjustment.styles) |v| { - comptime assert(@bitSizeOf(@TypeOf(v)) <= @bitSizeOf(usize)); - const aligned = std.math.ceilPowerOfTwo(usize, v) catch unreachable; - cap.styles = @max(cap.styles, aligned); - } - if (adjustment.grapheme_bytes) |v| { - comptime assert(@bitSizeOf(@TypeOf(v)) <= @bitSizeOf(usize)); - const aligned = std.math.ceilPowerOfTwo(usize, v) catch unreachable; - cap.grapheme_bytes = @max(cap.grapheme_bytes, aligned); - } - if (adjustment.hyperlink_bytes) |v| { - comptime assert(@bitSizeOf(@TypeOf(v)) <= @bitSizeOf(usize)); - const aligned = std.math.ceilPowerOfTwo(usize, v) catch unreachable; - cap.hyperlink_bytes = @max(cap.hyperlink_bytes, aligned); - } - if (adjustment.string_bytes) |v| { - comptime assert(@bitSizeOf(@TypeOf(v)) <= @bitSizeOf(usize)); - const aligned = std.math.ceilPowerOfTwo(usize, v) catch unreachable; - cap.string_bytes = @max(cap.string_bytes, aligned); - } + // We use checked math to prevent overflow. If there is an + // overflow it means we're out of space in this dimension, + // since pages can take up to their maxInt capacity in any + // category. + const new = std.math.mul( + Int, + old, + 2, + ) catch |err| overflow: { + comptime assert(@TypeOf(err) == error{Overflow}); + // Our final doubling would overflow since maxInt is + // 2^N - 1 for an unsignged int of N bits. So, if we overflow + // and we haven't used all the bits, use all the bits. + if (old < std.math.maxInt(Int)) break :overflow std.math.maxInt(Int); + return error.OutOfSpace; + }; + @field(cap, field_name) = new; + + // If our capacity exceeds the maximum page size, treat it + // as an OutOfSpace because things like page splitting will + // help. + const layout = Page.layout(cap); + if (layout.total_size > size.max_page_size) { + return error.OutOfSpace; + } + }, + }; log.info("adjusting page capacity={}", .{cap}); @@ -2611,7 +3274,25 @@ pub fn adjustCapacity( assert(new_page.capacity.cols >= page.capacity.cols); new_page.size.rows = page.size.rows; new_page.size.cols = page.size.cols; - try new_page.cloneFrom(page, 0, page.size.rows); + new_page.cloneFrom( + page, + 0, + page.size.rows, + ) catch |err| { + // cloneFrom only errors if there isn't capacity for the data + // from the source page but we're only increasing capacity so + // this should never be possible. If it happens, we should crash + // because we're in no man's land and can't safely recover. + log.err("increaseCapacity clone failed err={}", .{err}); + @panic("unexpected clone failure"); + }; + + // Preserve page-level dirty flag (cloneFrom only copies row data) + new_page.dirty = page.dirty; + + // Must not fail after this because the operations we do after this + // can't be recovered. + errdefer comptime unreachable; // Fix up all our tracked pins to point to the new page. const pin_keys = self.tracked_pins.keys(); @@ -2657,6 +3338,11 @@ inline fn createPageExt( const pooled = layout.total_size <= std_size; const page_alloc = pool.pages.arena.child_allocator; + // It would be better to encode this into the Zig error handling + // system but that is a big undertaking and we only have a few + // centralized call sites so it is handled on its own currently. + assert(layout.total_size <= size.max_page_size); + // Our page buffer comes from our standard memory pool if it // is within our standard size since this is what the pool // dispenses. Otherwise, we use the heap allocator to allocate. @@ -3328,7 +4014,10 @@ pub fn getCell(self: *const PageList, pt: point.Point) ?Cell { /// 1 | etc.| | 4 /// +-----+ : /// +--------+ -pub fn diagram(self: *const PageList, writer: *std.Io.Writer) !void { +pub fn diagram( + self: *const PageList, + writer: *std.Io.Writer, +) std.Io.Writer.Error!void { const active_pin = self.getTopLeft(.active); var active = false; @@ -3821,6 +4510,15 @@ pub const PageIterator = struct { pub fn fullPage(self: Chunk) bool { return self.start == 0 and self.end == self.node.data.size.rows; } + + /// Returns true if this chunk overlaps with the given other chunk + /// in any way. + pub fn overlaps(self: Chunk, other: Chunk) bool { + if (self.node != other.node) return false; + if (self.end <= other.start) return false; + if (self.start >= other.end) return false; + return true; + } }; }; @@ -3956,7 +4654,7 @@ pub fn totalPages(self: *const PageList) usize { /// Grow the number of rows available in the page list by n. /// This is only used for testing so it isn't optimized in any way. -fn growRows(self: *PageList, n: usize) !void { +fn growRows(self: *PageList, n: usize) Allocator.Error!void { for (0..n) |_| _ = try self.grow(); } @@ -4466,6 +5164,62 @@ test "PageList" { }, s.scrollbar()); } +test "PageList init error" { + // Test every failure point in `init` and ensure that we don't + // leak memory (testing.allocator verifies) since we're exiting early. + for (std.meta.tags(init_tw.FailPoint)) |tag| { + const tw = init_tw; + defer tw.end(.reset) catch unreachable; + tw.errorAlways(tag, error.OutOfMemory); + try std.testing.expectError( + error.OutOfMemory, + init( + std.testing.allocator, + 80, + 24, + null, + ), + ); + } + + // init calls initPages transitively, so let's check that if + // any failures happen in initPages, we also don't leak memory. + for (std.meta.tags(initPages_tw.FailPoint)) |tag| { + const tw = initPages_tw; + defer tw.end(.reset) catch unreachable; + tw.errorAlways(tag, error.OutOfMemory); + + const cols: size.CellCountInt = if (tag == .page_buf_std) 80 else std_capacity.maxCols().? + 1; + try std.testing.expectError( + error.OutOfMemory, + init( + std.testing.allocator, + cols, + 24, + null, + ), + ); + } + + // Try non-standard pages since they don't go in our pool. + for ([_]initPages_tw.FailPoint{ + .page_buf_non_std, + }) |tag| { + const tw = initPages_tw; + defer tw.end(.reset) catch unreachable; + tw.errorAfter(tag, error.OutOfMemory, 1); + try std.testing.expectError( + error.OutOfMemory, + init( + std.testing.allocator, + std_capacity.maxCols().? + 1, + std_capacity.rows + 1, + null, + ), + ); + } +} + test "PageList init rows across two pages" { const testing = std.testing; const alloc = testing.allocator; @@ -4499,6 +5253,38 @@ test "PageList init rows across two pages" { }, s.scrollbar()); } +test "PageList init more than max cols" { + const testing = std.testing; + const alloc = testing.allocator; + + // Initialize with more columns than we can fit in our standard + // capacity. This is going to force us to go to a non-standard page + // immediately. + var s = try init( + alloc, + std_capacity.maxCols().? + 1, + 80, + null, + ); + defer s.deinit(); + try testing.expect(s.viewport == .active); + try testing.expectEqual(@as(usize, s.rows), s.totalRows()); + + // We expect a single, non-standard page + try testing.expect(s.pages.first != null); + try testing.expect(s.pages.first.?.data.memory.len > std_size); + + // Initial total rows should be our row count + try testing.expectEqual(s.rows, s.total_rows); + + // Scrollbar should be where we expect it + try testing.expectEqual(Scrollbar{ + .total = s.rows, + .offset = 0, + .len = s.rows, + }, s.scrollbar()); +} + test "PageList pointFromPin active no history" { const testing = std.testing; const alloc = testing.allocator; @@ -4731,28 +5517,22 @@ test "PageList grow prune required with a single page" { const testing = std.testing; const alloc = testing.allocator; - var s = try init(alloc, 80, 24, 0); + // Need scrollback > 0 to have a scrollbar to test + var s = try init(alloc, 80, 24, null); defer s.deinit(); // This block is all test setup. There is nothing required about this // behavior during a refactor. This is setting up a scenario that is // possible to trigger a bug (#2280). { - // Adjust our capacity until our page is larger than the standard size. + // Increase our capacity until our page is larger than the standard size. // This is important because it triggers a scenario where our calculated // minSize() which is supposed to accommodate 2 pages is no longer true. - var cap = std_capacity; while (true) { - cap.grapheme_bytes *= 2; - const layout = Page.layout(cap); + const layout = Page.layout(s.pages.first.?.data.capacity); if (layout.total_size > std_size) break; + _ = try s.increaseCapacity(s.pages.first.?, .grapheme_bytes); } - - // Adjust to that capacity. After we should still have one page. - _ = try s.adjustCapacity( - s.pages.first.?, - .{ .grapheme_bytes = cap.grapheme_bytes }, - ); try testing.expect(s.pages.first != null); try testing.expect(s.pages.first == s.pages.last); } @@ -4779,6 +5559,47 @@ test "PageList grow prune required with a single page" { }, s.scrollbar()); } +test "PageList scrollbar with max_size 0 after grow" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, 0); + defer s.deinit(); + + // Grow some rows (simulates normal terminal output) + try s.growRows(10); + + const sb = s.scrollbar(); + + // With no scrollback (max_size = 0), total should equal rows + try testing.expectEqual(s.rows, sb.total); + + // With no scrollback, offset should be 0 (nowhere to scroll back to) + try testing.expectEqual(@as(usize, 0), sb.offset); +} + +test "PageList scroll with max_size 0 no history" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, 0); + defer s.deinit(); + + try s.growRows(10); + + // Remember initial viewport position + const pt_before = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + + // Try to scroll backwards into "history" - should be no-op + s.scroll(.{ .delta_row = -5 }); + try testing.expect(s.viewport == .active); + + // Scroll to top - should also be no-op with no scrollback + s.scroll(.{ .top = {} }); + const pt_after = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(pt_before, pt_after); +} + test "PageList scroll top" { const testing = std.testing; const alloc = testing.allocator; @@ -5754,8 +6575,8 @@ test "PageList grow prune scrollback" { const testing = std.testing; const alloc = testing.allocator; - // Zero here forces minimum max size to effectively two pages. - var s = try init(alloc, 80, 24, 0); + // Use std_size to limit scrollback so pruning is triggered. + var s = try init(alloc, 80, 24, std_size); defer s.deinit(); // Grow to capacity @@ -5823,8 +6644,8 @@ test "PageList grow prune scrollback with viewport pin not in pruned page" { const testing = std.testing; const alloc = testing.allocator; - // Zero here forces minimum max size to effectively two pages. - var s = try init(alloc, 80, 24, 0); + // Use std_size to limit scrollback so pruning is triggered. + var s = try init(alloc, 80, 24, std_size); defer s.deinit(); // Grow to capacity of first page @@ -6130,12 +6951,15 @@ test "PageList eraseRowBounded exhausts pages invalidates viewport offset cache" }, s.scrollbar()); } -test "PageList adjustCapacity to increase styles" { +test "PageList increaseCapacity to increase styles" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 2, 2, 0); defer s.deinit(); + + const original_styles_cap = s.pages.first.?.data.capacity.styles; + { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; @@ -6153,14 +6977,19 @@ test "PageList adjustCapacity to increase styles" { } // Increase our styles - _ = try s.adjustCapacity( - s.pages.first.?, - .{ .styles = std_capacity.styles * 2 }, - ); + _ = try s.increaseCapacity(s.pages.first.?, .styles); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; + + // Verify capacity doubled + try testing.expectEqual( + original_styles_cap * 2, + page.capacity.styles, + ); + + // Verify data preserved for (0..s.rows) |y| { for (0..s.cols) |x| { const rac = page.getRowAndCell(x, y); @@ -6173,17 +7002,19 @@ test "PageList adjustCapacity to increase styles" { } } -test "PageList adjustCapacity to increase graphemes" { +test "PageList increaseCapacity to increase graphemes" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 2, 2, 0); defer s.deinit(); + + const original_cap = s.pages.first.?.data.capacity.grapheme_bytes; + { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; - // Write all our data so we can assert its the same after for (0..s.rows) |y| { for (0..s.cols) |x| { const rac = page.getRowAndCell(x, y); @@ -6195,15 +7026,14 @@ test "PageList adjustCapacity to increase graphemes" { } } - // Increase our graphemes - _ = try s.adjustCapacity( - s.pages.first.?, - .{ .grapheme_bytes = std_capacity.grapheme_bytes * 2 }, - ); + _ = try s.increaseCapacity(s.pages.first.?, .grapheme_bytes); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; + + try testing.expectEqual(original_cap * 2, page.capacity.grapheme_bytes); + for (0..s.rows) |y| { for (0..s.cols) |x| { const rac = page.getRowAndCell(x, y); @@ -6216,17 +7046,19 @@ test "PageList adjustCapacity to increase graphemes" { } } -test "PageList adjustCapacity to increase hyperlinks" { +test "PageList increaseCapacity to increase hyperlinks" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 2, 2, 0); defer s.deinit(); + + const original_cap = s.pages.first.?.data.capacity.hyperlink_bytes; + { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; - // Write all our data so we can assert its the same after for (0..s.rows) |y| { for (0..s.cols) |x| { const rac = page.getRowAndCell(x, y); @@ -6238,15 +7070,14 @@ test "PageList adjustCapacity to increase hyperlinks" { } } - // Increase our graphemes - _ = try s.adjustCapacity( - s.pages.first.?, - .{ .hyperlink_bytes = @max(std_capacity.hyperlink_bytes * 2, 2048) }, - ); + _ = try s.increaseCapacity(s.pages.first.?, .hyperlink_bytes); { try testing.expect(s.pages.first == s.pages.last); const page = &s.pages.first.?.data; + + try testing.expectEqual(original_cap * 2, page.capacity.hyperlink_bytes); + for (0..s.rows) |y| { for (0..s.cols) |x| { const rac = page.getRowAndCell(x, y); @@ -6259,39 +7090,193 @@ test "PageList adjustCapacity to increase hyperlinks" { } } -test "PageList adjustCapacity after col shrink" { +test "PageList increaseCapacity to increase string_bytes" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 2, 2, 0); + defer s.deinit(); + + const original_cap = s.pages.first.?.data.capacity.string_bytes; + + { + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + + for (0..s.rows) |y| { + for (0..s.cols) |x| { + const rac = page.getRowAndCell(x, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = @intCast(x) }, + }; + } + } + } + + _ = try s.increaseCapacity(s.pages.first.?, .string_bytes); + + { + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + + try testing.expectEqual(original_cap * 2, page.capacity.string_bytes); + + for (0..s.rows) |y| { + for (0..s.cols) |x| { + const rac = page.getRowAndCell(x, y); + try testing.expectEqual( + @as(u21, @intCast(x)), + rac.cell.content.codepoint, + ); + } + } + } +} + +test "PageList increaseCapacity tracked pins" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 2, 2, 0); + defer s.deinit(); + + // Create a tracked pin on the first page + const tracked = try s.trackPin(s.pin(.{ .active = .{ .x = 1, .y = 1 } }).?); + defer s.untrackPin(tracked); + + const old_node = s.pages.first.?; + try testing.expectEqual(old_node, tracked.node); + + // Increase capacity + const new_node = try s.increaseCapacity(s.pages.first.?, .styles); + + // Pin should now point to the new node + try testing.expectEqual(new_node, tracked.node); + try testing.expectEqual(@as(size.CellCountInt, 1), tracked.x); + try testing.expectEqual(@as(size.CellCountInt, 1), tracked.y); +} + +test "PageList increaseCapacity returns OutOfSpace at max capacity" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 2, 2, 0); + defer s.deinit(); + + // Keep increasing styles capacity until we get OutOfSpace + const max_styles = std.math.maxInt(size.StyleCountInt); + while (true) { + _ = s.increaseCapacity( + s.pages.first.?, + .styles, + ) catch |err| { + // Before OutOfSpace, we should have reached maxInt + try testing.expectEqual(error.OutOfSpace, err); + try testing.expectEqual(max_styles, s.pages.first.?.data.capacity.styles); + break; + }; + } +} + +test "PageList increaseCapacity after col shrink" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 10, 2, 0); defer s.deinit(); - // Shrink columns - this updates size.cols but not capacity.cols + // Shrink columns try s.resize(.{ .cols = 5, .reflow = false }); try testing.expectEqual(5, s.cols); { const page = &s.pages.first.?.data; - // capacity.cols is still 10, but size.cols should be 5 try testing.expectEqual(5, page.size.cols); try testing.expect(page.capacity.cols >= 10); } - // Now adjust capacity (e.g., to increase styles) - // This should preserve the current size.cols, not revert to capacity.cols - _ = try s.adjustCapacity( - s.pages.first.?, - .{ .styles = std_capacity.styles * 2 }, - ); + // Increase capacity + _ = try s.increaseCapacity(s.pages.first.?, .styles); { const page = &s.pages.first.?.data; - // After adjustCapacity, size.cols should still be 5, not 10 + // size.cols should still be 5, not reverted to capacity.cols try testing.expectEqual(5, page.size.cols); try testing.expectEqual(5, s.cols); } } +test "PageList increaseCapacity multi-page" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + // Grow to create a second page + const page1_node = s.pages.last.?; + page1_node.data.pauseIntegrityChecks(true); + for (0..page1_node.data.capacity.rows - page1_node.data.size.rows) |_| { + try testing.expect(try s.grow() == null); + } + page1_node.data.pauseIntegrityChecks(false); + try testing.expect(try s.grow() != null); + + // Now we have two pages + try testing.expect(s.pages.first != s.pages.last); + const page2_node = s.pages.last.?; + + const page1_styles_cap = s.pages.first.?.data.capacity.styles; + const page2_styles_cap = page2_node.data.capacity.styles; + + // Increase capacity on the first page only + _ = try s.increaseCapacity(s.pages.first.?, .styles); + + // First page capacity should be doubled + try testing.expectEqual( + page1_styles_cap * 2, + s.pages.first.?.data.capacity.styles, + ); + + // Second page should be unchanged + try testing.expectEqual( + page2_styles_cap, + s.pages.last.?.data.capacity.styles, + ); +} + +test "PageList increaseCapacity preserves dirty flag" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 2, 4, 0); + defer s.deinit(); + + // Set page dirty flag and mark some rows as dirty + const page = &s.pages.first.?.data; + page.dirty = true; + + const rows = page.rows.ptr(page.memory); + rows[0].dirty = true; + rows[1].dirty = false; + rows[2].dirty = true; + rows[3].dirty = false; + + // Increase capacity + const new_node = try s.increaseCapacity(s.pages.first.?, .styles); + + // The page dirty flag should be preserved + try testing.expect(new_node.data.dirty); + + // Row dirty flags should be preserved + const new_rows = new_node.data.rows.ptr(new_node.data.memory); + try testing.expect(new_rows[0].dirty); + try testing.expect(!new_rows[1].dirty); + try testing.expect(new_rows[2].dirty); + try testing.expect(!new_rows[3].dirty); +} + test "PageList pageIterator single page" { const testing = std.testing; const alloc = testing.allocator; @@ -7018,9 +8003,8 @@ test "PageList clone" { defer s.deinit(); try testing.expectEqual(@as(usize, s.rows), s.totalRows()); - var s2 = try s.clone(.{ + var s2 = try s.clone(alloc, .{ .top = .{ .screen = .{} }, - .memory = .{ .alloc = alloc }, }); defer s2.deinit(); try testing.expectEqual(@as(usize, s.rows), s2.totalRows()); @@ -7035,10 +8019,9 @@ test "PageList clone partial trimmed right" { try testing.expectEqual(@as(usize, s.rows), s.totalRows()); try s.growRows(30); - var s2 = try s.clone(.{ + var s2 = try s.clone(alloc, .{ .top = .{ .screen = .{} }, .bot = .{ .screen = .{ .y = 39 } }, - .memory = .{ .alloc = alloc }, }); defer s2.deinit(); try testing.expectEqual(@as(usize, 40), s2.totalRows()); @@ -7053,9 +8036,8 @@ test "PageList clone partial trimmed left" { try testing.expectEqual(@as(usize, s.rows), s.totalRows()); try s.growRows(30); - var s2 = try s.clone(.{ + var s2 = try s.clone(alloc, .{ .top = .{ .screen = .{ .y = 10 } }, - .memory = .{ .alloc = alloc }, }); defer s2.deinit(); try testing.expectEqual(@as(usize, 40), s2.totalRows()); @@ -7097,9 +8079,8 @@ test "PageList clone partial trimmed left reclaims styles" { try testing.expectEqual(1, page.styles.count()); } - var s2 = try s.clone(.{ + var s2 = try s.clone(alloc, .{ .top = .{ .screen = .{ .y = 10 } }, - .memory = .{ .alloc = alloc }, }); defer s2.deinit(); try testing.expectEqual(@as(usize, 40), s2.totalRows()); @@ -7120,10 +8101,9 @@ test "PageList clone partial trimmed both" { try testing.expectEqual(@as(usize, s.rows), s.totalRows()); try s.growRows(30); - var s2 = try s.clone(.{ + var s2 = try s.clone(alloc, .{ .top = .{ .screen = .{ .y = 10 } }, .bot = .{ .screen = .{ .y = 35 } }, - .memory = .{ .alloc = alloc }, }); defer s2.deinit(); try testing.expectEqual(@as(usize, 26), s2.totalRows()); @@ -7137,9 +8117,8 @@ test "PageList clone less than active" { defer s.deinit(); try testing.expectEqual(@as(usize, s.rows), s.totalRows()); - var s2 = try s.clone(.{ + var s2 = try s.clone(alloc, .{ .top = .{ .active = .{ .y = 5 } }, - .memory = .{ .alloc = alloc }, }); defer s2.deinit(); try testing.expectEqual(@as(usize, s.rows), s2.totalRows()); @@ -7159,9 +8138,8 @@ test "PageList clone remap tracked pin" { var pin_remap = Clone.TrackedPinsRemap.init(alloc); defer pin_remap.deinit(); - var s2 = try s.clone(.{ + var s2 = try s.clone(alloc, .{ .top = .{ .active = .{ .y = 5 } }, - .memory = .{ .alloc = alloc }, .tracked_pins = &pin_remap, }); defer s2.deinit(); @@ -7188,9 +8166,8 @@ test "PageList clone remap tracked pin not in cloned area" { var pin_remap = Clone.TrackedPinsRemap.init(alloc); defer pin_remap.deinit(); - var s2 = try s.clone(.{ + var s2 = try s.clone(alloc, .{ .top = .{ .active = .{ .y = 5 } }, - .memory = .{ .alloc = alloc }, .tracked_pins = &pin_remap, }); defer s2.deinit(); @@ -7212,9 +8189,8 @@ test "PageList clone full dirty" { s.markDirty(.{ .active = .{ .x = 0, .y = 12 } }); s.markDirty(.{ .active = .{ .x = 0, .y = 23 } }); - var s2 = try s.clone(.{ + var s2 = try s.clone(alloc, .{ .top = .{ .screen = .{} }, - .memory = .{ .alloc = alloc }, }); defer s2.deinit(); try testing.expectEqual(@as(usize, s.rows), s2.totalRows()); @@ -8275,7 +9251,7 @@ test "PageList resize reflow invalidates viewport offset cache" { // Verify scrollbar cache was invalidated during reflow try testing.expectEqual(Scrollbar{ .total = s.total_rows, - .offset = 8, + .offset = 5, .len = s.rows, }, s.scrollbar()); } @@ -10758,3 +11734,1073 @@ test "PageList clears history" { .x = 0, }, s.getTopLeft(.active)); } + +test "PageList resize reflow grapheme map capacity exceeded" { + // This test verifies that when reflowing content with many graphemes, + // the grapheme map capacity is correctly increased when needed. + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 4, 10, 0); + defer s.deinit(); + try testing.expectEqual(@as(usize, 1), s.totalPages()); + + // Get the grapheme capacity from the page. We need more than this many + // graphemes in a single destination page to trigger capacity increase + // during reflow. Since each source page can only hold this many graphemes, + // we create two source pages with graphemes that will merge into one + // destination page. + const grapheme_capacity = s.pages.first.?.data.graphemeCapacity(); + // Use slightly more than half the capacity per page, so combined they + // exceed the capacity of a single destination page. + const graphemes_per_page = grapheme_capacity / 2 + grapheme_capacity / 4; + + // Grow to the capacity of the first page and add more rows + // 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(graphemes_per_page); + try testing.expectEqual(@as(usize, 2), s.totalPages()); + + // We now have two pages. + try testing.expect(s.pages.first.? != s.pages.last.?); + try testing.expectEqual(s.pages.last.?, s.pages.first.?.next); + } + + // Add graphemes to both pages. We add graphemes to rows at the END of the + // first page, and graphemes to rows at the START of the second page. + // When reflowing to 2 columns, these rows will wrap and stay together + // on the same destination page, requiring capacity increase. + + // Add graphemes to the end of the first page (last rows) + { + const page = &s.pages.first.?.data; + const start_row = page.size.rows - graphemes_per_page; + for (0..graphemes_per_page) |i| { + const y = start_row + i; + const rac = page.getRowAndCell(0, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 'A' }, + }; + try page.appendGrapheme(rac.row, rac.cell, @as(u21, @intCast(0x0301))); + } + } + + // Add graphemes to the beginning of the second page + { + const page = &s.pages.last.?.data; + const count = @min(graphemes_per_page, page.size.rows); + for (0..count) |y| { + const rac = page.getRowAndCell(0, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 'B' }, + }; + try page.appendGrapheme(rac.row, rac.cell, @as(u21, @intCast(0x0302))); + } + } + + // Resize to fewer columns to trigger reflow. + // The graphemes from both pages will be copied to destination pages. + // They will all end up in a contiguous region of the destination. + // If the bug exists (hyperlink_bytes increased instead of grapheme_bytes), + // this will fail with GraphemeMapOutOfMemory when we exceed capacity. + try s.resize(.{ .cols = 2, .reflow = true }); + + // Verify the resize succeeded + try testing.expectEqual(@as(usize, 2), s.cols); +} + +test "PageList resize grow cols with unwrap fixes viewport pin" { + // Regression test: after resize/reflow, the viewport pin can end up at a + // position where pin.y + rows > total_rows, causing getBottomRight to panic. + + // The plan is to pin viewport in history, then grow columns to unwrap rows. + // The unwrap reduces total_rows, but the tracked pin moves to a position + // that no longer has enough rows below it for the viewport height. + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 2, 10, null); + defer s.deinit(); + + // Make sure we have some history, in this case we have 30 rows of history + try s.growRows(30); + try testing.expectEqual(@as(usize, 40), s.totalRows()); + + // Fill all rows with wrapped content (pairs that unwrap when cols increase) + var it = s.pageIterator(.right_down, .{ .screen = .{} }, null); + while (it.next()) |chunk| { + const page = &chunk.node.data; + for (chunk.start..chunk.end) |y| { + const rac = page.getRowAndCell(0, y); + if (y % 2 == 0) { + rac.row.wrap = true; + } else { + rac.row.wrap_continuation = true; + } + for (0..s.cols) |x| { + page.getRowAndCell(x, y).cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 'A' }, + }; + } + } + } + + // Pin viewport at row 28 (in history, 2 rows before active area at row 30). + // After unwrap: row 28 -> row 14, total_rows 40 -> 20, active starts at 10. + // Pin at 14 needs rows 14-23, but only 0-19 exist -> overflow. + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = 28 } }).? }); + try testing.expect(s.viewport == .pin); + try testing.expect(s.getBottomRight(.viewport) != null); + + // Resize with reflow: unwraps rows, reducing total_rows + try s.resize(.{ .cols = 4, .reflow = true }); + try testing.expectEqual(@as(usize, 4), s.cols); + try testing.expect(s.totalRows() < 40); + + // Used to panic here, so test that we can get the bottom right. + const br_after = s.getBottomRight(.viewport); + try testing.expect(br_after != null); +} + +test "PageList grow reuses non-standard page without leak" { + const testing = std.testing; + const alloc = testing.allocator; + + // Create a PageList with 3 * std_size max so we can fit multiple pages + // but will still trigger reuse. + var s = try init(alloc, 80, 24, 3 * std_size); + defer s.deinit(); + + // Increase the first page capacity to make it non-standard (larger than std_size). + while (s.pages.first.?.data.memory.len <= std_size) { + _ = try s.increaseCapacity(s.pages.first.?, .grapheme_bytes); + } + + // The first page should now have non-standard memory size. + try testing.expect(s.pages.first.?.data.memory.len > std_size); + + // First, fill up the first page's capacity + const first_page = s.pages.first.?; + while (first_page.data.size.rows < first_page.data.capacity.rows) { + _ = try s.grow(); + } + + // Now grow to create a second page + _ = try s.grow(); + try testing.expect(s.pages.first != s.pages.last); + + // Continue growing until we exceed max_size AND the last page is full + while (s.page_size + PagePool.item_size <= s.maxSize() or + s.pages.last.?.data.size.rows < s.pages.last.?.data.capacity.rows) + { + _ = try s.grow(); + } + + // The first page should still be non-standard + try testing.expect(s.pages.first.?.data.memory.len > std_size); + + // Verify we have enough rows for active area (so prune path isn't skipped) + try testing.expect(s.totalRows() >= s.rows); + + // Verify last page is full (so grow will need to allocate/reuse) + try testing.expect(s.pages.last.?.data.size.rows == s.pages.last.?.data.capacity.rows); + + // Remember the first page memory pointer before the reuse attempt + const first_page_ptr = s.pages.first.?; + const first_page_mem_ptr = s.pages.first.?.data.memory.ptr; + + // Create a tracked pin pointing to the non-standard first page + const tracked_pin = try s.trackPin(.{ .node = first_page_ptr, .x = 0, .y = 0 }); + defer s.untrackPin(tracked_pin); + + // Now grow one more time to trigger the reuse path. Since the first page + // is non-standard, it should be destroyed (not reused). The testing + // allocator will detect a leak if destroyNode doesn't properly free + // the non-standard memory. + _ = try s.grow(); + + // After grow, check if the first page is a different one + // (meaning the non-standard page was pruned, not reused at the end) + // The original first page should no longer be the first page + try testing.expect(s.pages.first.? != first_page_ptr); + + // If the non-standard page was properly destroyed and not reused, + // the last page should not have the same memory pointer + try testing.expect(s.pages.last.?.data.memory.ptr != first_page_mem_ptr); + + // The tracked pin should have been moved to the new first page and marked as garbage + try testing.expectEqual(s.pages.first.?, tracked_pin.node); + try testing.expectEqual(0, tracked_pin.x); + try testing.expectEqual(0, tracked_pin.y); + try testing.expect(tracked_pin.garbage); +} + +test "PageList grow non-standard page prune protection" { + const testing = std.testing; + const alloc = testing.allocator; + + // This test specifically verifies the fix for the bug where pruning a + // non-standard page would cause totalRows() < self.rows. + // + // Bug trigger conditions (all must be true simultaneously): + // 1. first page is non-standard (memory.len > std_size) + // 2. page_size + PagePool.item_size > maxSize() (triggers prune consideration) + // 3. pages.first != pages.last (have multiple pages) + // 4. total_rows >= self.rows (have enough rows for active area) + // 5. total_rows - first.size.rows + 1 < self.rows (prune would lose too many) + + // This is kind of magic and likely depends on std_size. + const rows_count = 600; + var s = try init(alloc, 80, rows_count, std_size); + defer s.deinit(); + + // Make the first page non-standard + while (s.pages.first.?.data.memory.len <= std_size) { + _ = try s.increaseCapacity( + s.pages.first.?, + .grapheme_bytes, + ); + } + try testing.expect(s.pages.first.?.data.memory.len > std_size); + + const first_page_node = s.pages.first.?; + const first_page_cap = first_page_node.data.capacity.rows; + + // Fill first page to capacity + while (first_page_node.data.size.rows < first_page_cap) _ = try s.grow(); + + // Grow until we have a second page (first page fills up first) + var second_node: ?*List.Node = null; + while (s.pages.first == s.pages.last) second_node = try s.grow(); + try testing.expect(s.pages.first != s.pages.last); + + // Fill the second page to capacity so that the next grow() triggers prune + const last_node = s.pages.last.?; + const second_cap = last_node.data.capacity.rows; + while (last_node.data.size.rows < second_cap) _ = try s.grow(); + + // Now the last page is full. The next grow must either: + // 1. Prune the first page and reuse it, OR + // 2. Allocate a new page + const total = s.totalRows(); + const would_remain = total - first_page_cap + 1; + + // Verify the bug condition is present: pruning first page would leave < rows + try testing.expect(would_remain < s.rows); + + // Verify prune path conditions are met + try testing.expect(s.pages.first != s.pages.last); + try testing.expect(s.page_size + PagePool.item_size > s.maxSize()); + try testing.expect(s.totalRows() >= s.rows); + + // Verify last page is at capacity (so grow must prune or allocate new) + try testing.expectEqual(second_cap, last_node.data.size.rows); + + // The next grow should trigger prune consideration. + // Without the fix, this would destroy the non-standard first page, + // leaving only second_cap + 1 rows, which is < self.rows. + _ = try s.grow(); + + // Verify the invariant holds - the fix prevents the destructive prune + try testing.expect(s.totalRows() >= s.rows); +} + +test "PageList resize (no reflow) more cols remaps pins in backfill path" { + // Regression test: when resizeWithoutReflowGrowCols copies rows to a previous + // page with spare capacity, tracked pins in those rows must be remapped. + // Without the fix, pins become dangling pointers when the original page is destroyed. + const testing = std.testing; + const alloc = testing.allocator; + + const cols: size.CellCountInt = 5; + const cap = try std_capacity.adjust(.{ .cols = cols }); + var s = try init(alloc, cols, cap.rows, null); + defer s.deinit(); + + // Grow until we have two pages. + while (s.pages.first == s.pages.last) { + _ = try s.grow(); + } + const first_page = s.pages.first.?; + const second_page = s.pages.last.?; + try testing.expect(first_page != second_page); + + // Trim a history row so the first page has spare capacity. + // This triggers the backfill path in resizeWithoutReflowGrowCols. + s.eraseRows(.{ .history = .{} }, .{ .history = .{ .y = 0 } }); + try testing.expect(first_page.data.size.rows < first_page.data.capacity.rows); + + // Ensure the resize takes the slow path (new capacity > current capacity). + const new_cols: size.CellCountInt = cols + 1; + const adjusted = try second_page.data.capacity.adjust(.{ .cols = new_cols }); + try testing.expect(second_page.data.capacity.cols < adjusted.cols); + + // Track a pin in row 0 of the second page. This row will be copied + // to the first page during backfill and the pin must be remapped. + const tracked = try s.trackPin(.{ .node = second_page, .x = 0, .y = 0 }); + defer s.untrackPin(tracked); + + // Write a marker character to the tracked cell so we can verify + // the pin points to the correct cell after resize. + const marker: u21 = 'X'; + tracked.rowAndCell().cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = marker }, + }; + + try s.resize(.{ .cols = new_cols, .reflow = false }); + + // Verify the pin points to a valid node still in the page list. + var found = false; + var it = s.pages.first; + while (it) |node| : (it = node.next) { + if (node == tracked.node) { + found = true; + break; + } + } + try testing.expect(found); + try testing.expect(tracked.y < tracked.node.data.size.rows); + + // Verify the pin still points to the cell with our marker content. + const cell = tracked.rowAndCell().cell; + try testing.expectEqual(.codepoint, cell.content_tag); + try testing.expectEqual(marker, cell.content.codepoint); +} + +test "PageList compact std_size page returns null" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, 0); + defer s.deinit(); + + // A freshly created page should be at std_size + const node = s.pages.first.?; + try testing.expect(node.data.memory.len <= std_size); + + // compact should return null since there's nothing to compact + const result = try s.compact(node); + try testing.expectEqual(null, result); + + // Page should still be the same + try testing.expectEqual(node, s.pages.first.?); +} + +test "PageList compact oversized page" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + // Grow until we have multiple pages + const page1_node = s.pages.first.?; + page1_node.data.pauseIntegrityChecks(true); + for (0..page1_node.data.capacity.rows - page1_node.data.size.rows) |_| { + _ = try s.grow(); + } + page1_node.data.pauseIntegrityChecks(false); + _ = try s.grow(); + try testing.expect(s.pages.first != s.pages.last); + + var node = s.pages.first.?; + + // Write content to verify it's preserved + { + const page = &node.data; + for (0..page.size.rows) |y| { + for (0..s.cols) |x| { + const rac = page.getRowAndCell(x, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = @intCast(x + y * s.cols) }, + }; + } + } + } + + // Create a tracked pin on this page + const tracked = try s.trackPin(.{ .node = node, .x = 5, .y = 10 }); + defer s.untrackPin(tracked); + + // Make the page oversized + while (node.data.memory.len <= std_size) { + node = try s.increaseCapacity(node, .grapheme_bytes); + } + try testing.expect(node.data.memory.len > std_size); + const oversized_len = node.data.memory.len; + const original_size = node.data.size; + const second_node = node.next.?; + + // Set dirty flag after increaseCapacity + node.data.dirty = true; + + // Compact the page + const new_node = try s.compact(node); + try testing.expect(new_node != null); + + // Verify memory is smaller + try testing.expect(new_node.?.data.memory.len < oversized_len); + + // Verify size preserved + try testing.expectEqual(original_size.rows, new_node.?.data.size.rows); + try testing.expectEqual(original_size.cols, new_node.?.data.size.cols); + + // Verify dirty flag preserved + try testing.expect(new_node.?.data.dirty); + + // Verify linked list integrity + try testing.expectEqual(new_node.?, s.pages.first.?); + try testing.expectEqual(null, new_node.?.prev); + try testing.expectEqual(second_node, new_node.?.next); + try testing.expectEqual(new_node.?, second_node.prev); + + // Verify pin updated correctly + try testing.expectEqual(new_node.?, tracked.node); + try testing.expectEqual(@as(size.CellCountInt, 5), tracked.x); + try testing.expectEqual(@as(size.CellCountInt, 10), tracked.y); + + // Verify content preserved + const page = &new_node.?.data; + for (0..page.size.rows) |y| { + for (0..s.cols) |x| { + const rac = page.getRowAndCell(x, y); + try testing.expectEqual( + @as(u21, @intCast(x + y * s.cols)), + rac.cell.content.codepoint, + ); + } + } +} + +test "PageList compact insufficient savings returns null" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, 0); + defer s.deinit(); + + var node = s.pages.first.?; + + // Make the page slightly oversized (just one increase) + // This might not provide enough savings to justify compaction + node = try s.increaseCapacity(node, .grapheme_bytes); + + // If the page is still at or below std_size, compact returns null + if (node.data.memory.len <= std_size) { + const result = try s.compact(node); + try testing.expectEqual(null, result); + } else { + // If it did grow beyond std_size, verify that compaction + // works or returns null based on savings calculation + const result = try s.compact(node); + // Either it compacted or determined insufficient savings + if (result) |new_node| { + try testing.expect(new_node.data.memory.len < node.data.memory.len); + } + } +} + +test "PageList split at middle row" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const page = &s.pages.first.?.data; + + // Write content to rows: row 0 gets codepoint 0, row 1 gets 1, etc. + for (0..page.size.rows) |y| { + const rac = page.getRowAndCell(0, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = @intCast(y) }, + }; + } + + // Split at row 5 (middle) + const split_pin: Pin = .{ .node = s.pages.first.?, .y = 5, .x = 0 }; + try s.split(split_pin); + + // Verify two pages exist + try testing.expect(s.pages.first != null); + try testing.expect(s.pages.first.?.next != null); + + const first_page = &s.pages.first.?.data; + const second_page = &s.pages.first.?.next.?.data; + + // First page should have rows 0-4 (5 rows) + try testing.expectEqual(@as(usize, 5), first_page.size.rows); + // Second page should have rows 5-9 (5 rows) + try testing.expectEqual(@as(usize, 5), second_page.size.rows); + + // Verify content in first page is preserved (rows 0-4 have codepoints 0-4) + for (0..5) |y| { + const rac = first_page.getRowAndCell(0, y); + try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint); + } + + // Verify content in second page (original rows 5-9, now at y=0-4) + for (0..5) |y| { + const rac = second_page.getRowAndCell(0, y); + try testing.expectEqual(@as(u21, @intCast(y + 5)), rac.cell.content.codepoint); + } +} + +test "PageList split at row 0 is no-op" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const page = &s.pages.first.?.data; + + // Write content to all rows + for (0..page.size.rows) |y| { + const rac = page.getRowAndCell(0, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = @intCast(y) }, + }; + } + + // Split at row 0 should be a no-op + const split_pin: Pin = .{ .node = s.pages.first.?, .y = 0, .x = 0 }; + try s.split(split_pin); + + // Verify only one page exists (no split occurred) + try testing.expect(s.pages.first != null); + try testing.expect(s.pages.first.?.next == null); + + // Verify all content is still in the original page + try testing.expectEqual(@as(usize, 10), page.size.rows); + for (0..10) |y| { + const rac = page.getRowAndCell(0, y); + try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint); + } +} + +test "PageList split at last row" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const page = &s.pages.first.?.data; + + // Write content to all rows + for (0..page.size.rows) |y| { + const rac = page.getRowAndCell(0, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = @intCast(y) }, + }; + } + + // Split at last row (row 9) + const split_pin: Pin = .{ .node = s.pages.first.?, .y = 9, .x = 0 }; + try s.split(split_pin); + + // Verify two pages exist + try testing.expect(s.pages.first != null); + try testing.expect(s.pages.first.?.next != null); + + const first_page = &s.pages.first.?.data; + const second_page = &s.pages.first.?.next.?.data; + + // First page should have 9 rows + try testing.expectEqual(@as(usize, 9), first_page.size.rows); + // Second page should have 1 row + try testing.expectEqual(@as(usize, 1), second_page.size.rows); + + // Verify content in second page (original row 9, now at y=0) + const rac = second_page.getRowAndCell(0, 0); + try testing.expectEqual(@as(u21, 9), rac.cell.content.codepoint); +} + +test "PageList split single row page returns OutOfSpace" { + const testing = std.testing; + const alloc = testing.allocator; + + // Initialize with 1 row + var s = try init(alloc, 10, 1, 0); + defer s.deinit(); + + const split_pin: Pin = .{ .node = s.pages.first.?, .y = 0, .x = 0 }; + const result = s.split(split_pin); + + try testing.expectError(error.OutOfSpace, result); +} + +test "PageList split moves tracked pins" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + // Track a pin at row 7 + const tracked = try s.trackPin(.{ .node = s.pages.first.?, .y = 7, .x = 3 }); + defer s.untrackPin(tracked); + + // Split at row 5 + const split_pin: Pin = .{ .node = s.pages.first.?, .y = 5, .x = 0 }; + try s.split(split_pin); + + // The tracked pin should now be in the second page + try testing.expect(tracked.node == s.pages.first.?.next.?); + // y should be adjusted: was 7, split at 5, so new y = 7 - 5 = 2 + try testing.expectEqual(@as(usize, 2), tracked.y); + // x should remain unchanged + try testing.expectEqual(@as(usize, 3), tracked.x); +} + +test "PageList split tracked pin before split point unchanged" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const original_node = s.pages.first.?; + + // Track a pin at row 2 (before the split point) + const tracked = try s.trackPin(.{ .node = original_node, .y = 2, .x = 5 }); + defer s.untrackPin(tracked); + + // Split at row 5 + const split_pin: Pin = .{ .node = original_node, .y = 5, .x = 0 }; + try s.split(split_pin); + + // The tracked pin should remain in the original page + try testing.expect(tracked.node == s.pages.first.?); + // y and x should be unchanged + try testing.expectEqual(@as(usize, 2), tracked.y); + try testing.expectEqual(@as(usize, 5), tracked.x); +} + +test "PageList split tracked pin at split point moves to new page" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const original_node = s.pages.first.?; + + // Track a pin at the exact split point (row 5) + const tracked = try s.trackPin(.{ .node = original_node, .y = 5, .x = 4 }); + defer s.untrackPin(tracked); + + // Split at row 5 + const split_pin: Pin = .{ .node = original_node, .y = 5, .x = 0 }; + try s.split(split_pin); + + // The tracked pin should be in the new page + try testing.expect(tracked.node == s.pages.first.?.next.?); + // y should be 0 since it was at the split point: 5 - 5 = 0 + try testing.expectEqual(@as(usize, 0), tracked.y); + // x should remain unchanged + try testing.expectEqual(@as(usize, 4), tracked.x); +} + +test "PageList split multiple tracked pins across regions" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const original_node = s.pages.first.?; + + // Track multiple pins in different regions + const pin_before = try s.trackPin(.{ .node = original_node, .y = 1, .x = 0 }); + defer s.untrackPin(pin_before); + const pin_at_split = try s.trackPin(.{ .node = original_node, .y = 5, .x = 2 }); + defer s.untrackPin(pin_at_split); + const pin_after1 = try s.trackPin(.{ .node = original_node, .y = 7, .x = 3 }); + defer s.untrackPin(pin_after1); + const pin_after2 = try s.trackPin(.{ .node = original_node, .y = 9, .x = 8 }); + defer s.untrackPin(pin_after2); + + // Split at row 5 + const split_pin: Pin = .{ .node = original_node, .y = 5, .x = 0 }; + try s.split(split_pin); + + const first_page = s.pages.first.?; + const second_page = first_page.next.?; + + // Pin before split point stays in original page + try testing.expect(pin_before.node == first_page); + try testing.expectEqual(@as(usize, 1), pin_before.y); + try testing.expectEqual(@as(usize, 0), pin_before.x); + + // Pin at split point moves to new page with y=0 + try testing.expect(pin_at_split.node == second_page); + try testing.expectEqual(@as(usize, 0), pin_at_split.y); + try testing.expectEqual(@as(usize, 2), pin_at_split.x); + + // Pins after split point move to new page with adjusted y + try testing.expect(pin_after1.node == second_page); + try testing.expectEqual(@as(usize, 2), pin_after1.y); // 7 - 5 = 2 + try testing.expectEqual(@as(usize, 3), pin_after1.x); + + try testing.expect(pin_after2.node == second_page); + try testing.expectEqual(@as(usize, 4), pin_after2.y); // 9 - 5 = 4 + try testing.expectEqual(@as(usize, 8), pin_after2.x); +} + +test "PageList split tracked viewport_pin in split region moves correctly" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const original_node = s.pages.first.?; + + // Set viewport_pin to row 7 (after split point) + s.viewport_pin.node = original_node; + s.viewport_pin.y = 7; + s.viewport_pin.x = 6; + + // Split at row 5 + const split_pin: Pin = .{ .node = original_node, .y = 5, .x = 0 }; + try s.split(split_pin); + + // viewport_pin should be in the new page + try testing.expect(s.viewport_pin.node == s.pages.first.?.next.?); + // y should be adjusted: 7 - 5 = 2 + try testing.expectEqual(@as(usize, 2), s.viewport_pin.y); + // x should remain unchanged + try testing.expectEqual(@as(usize, 6), s.viewport_pin.x); +} + +test "PageList split middle page preserves linked list order" { + const testing = std.testing; + const alloc = testing.allocator; + + // Create a single page with 12 rows + var s = try init(alloc, 10, 12, 0); + defer s.deinit(); + + // Split at row 4 to create: page1 (rows 0-3), page2 (rows 4-11) + const first_node = s.pages.first.?; + const split_pin1: Pin = .{ .node = first_node, .y = 4, .x = 0 }; + try s.split(split_pin1); + + // Now we have 2 pages + const page1 = s.pages.first.?; + const page2 = s.pages.first.?.next.?; + try testing.expectEqual(@as(usize, 4), page1.data.size.rows); + try testing.expectEqual(@as(usize, 8), page2.data.size.rows); + + // Split page2 at row 4 to create: page1 -> page2 (rows 0-3) -> page3 (rows 4-7) + const split_pin2: Pin = .{ .node = page2, .y = 4, .x = 0 }; + try s.split(split_pin2); + + // Now we have 3 pages + const first = s.pages.first.?; + const middle = first.next.?; + const last = middle.next.?; + + // Verify linked list order: first -> middle -> last + try testing.expectEqual(page1, first); + try testing.expectEqual(page2, middle); + try testing.expectEqual(s.pages.last.?, last); + + // Verify prev pointers + try testing.expect(first.prev == null); + try testing.expectEqual(first, middle.prev.?); + try testing.expectEqual(middle, last.prev.?); + + // Verify next pointers + try testing.expectEqual(middle, first.next.?); + try testing.expectEqual(last, middle.next.?); + try testing.expect(last.next == null); + + // Verify row counts + try testing.expectEqual(@as(usize, 4), first.data.size.rows); + try testing.expectEqual(@as(usize, 4), middle.data.size.rows); + try testing.expectEqual(@as(usize, 4), last.data.size.rows); +} + +test "PageList split last page makes new page the last" { + const testing = std.testing; + const alloc = testing.allocator; + + // Create a single page with 10 rows + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + // Split to create 2 pages first + const first_node = s.pages.first.?; + const split_pin1: Pin = .{ .node = first_node, .y = 5, .x = 0 }; + try s.split(split_pin1); + + // Now split the last page + const last_before_split = s.pages.last.?; + try testing.expectEqual(@as(usize, 5), last_before_split.data.size.rows); + + const split_pin2: Pin = .{ .node = last_before_split, .y = 2, .x = 0 }; + try s.split(split_pin2); + + // The new page should be the new last + const new_last = s.pages.last.?; + try testing.expect(new_last != last_before_split); + try testing.expectEqual(last_before_split, new_last.prev.?); + try testing.expect(new_last.next == null); + + // Verify row counts: original last has 2 rows, new last has 3 rows + try testing.expectEqual(@as(usize, 2), last_before_split.data.size.rows); + try testing.expectEqual(@as(usize, 3), new_last.data.size.rows); +} + +test "PageList split first page keeps original as first" { + const testing = std.testing; + const alloc = testing.allocator; + + // Create 2 pages by splitting + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const original_first = s.pages.first.?; + const split_pin1: Pin = .{ .node = original_first, .y = 5, .x = 0 }; + try s.split(split_pin1); + + // Get second page (created by first split) + const second_page = s.pages.first.?.next.?; + + // Now split the first page again + const split_pin2: Pin = .{ .node = s.pages.first.?, .y = 2, .x = 0 }; + try s.split(split_pin2); + + // Original first should still be first + try testing.expectEqual(original_first, s.pages.first.?); + try testing.expect(s.pages.first.?.prev == null); + + // New page should be inserted between first and second + const inserted = s.pages.first.?.next.?; + try testing.expect(inserted != second_page); + try testing.expectEqual(second_page, inserted.next.?); + + // Verify row counts: first has 2, inserted has 3, second has 5 + try testing.expectEqual(@as(usize, 2), s.pages.first.?.data.size.rows); + try testing.expectEqual(@as(usize, 3), inserted.data.size.rows); + try testing.expectEqual(@as(usize, 5), second_page.data.size.rows); +} + +test "PageList split preserves wrap flags" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const page = &s.pages.first.?.data; + + // Set wrap flags on rows that will be in the second page after split + // Row 5: wrap = true (this is the start of a wrapped line) + // Row 6: wrap_continuation = true (this continues the wrap) + // Row 7: wrap = true, wrap_continuation = true (wrapped and continues) + { + const rac5 = page.getRowAndCell(0, 5); + rac5.row.wrap = true; + + const rac6 = page.getRowAndCell(0, 6); + rac6.row.wrap_continuation = true; + + const rac7 = page.getRowAndCell(0, 7); + rac7.row.wrap = true; + rac7.row.wrap_continuation = true; + } + + // Split at row 5 + const split_pin: Pin = .{ .node = s.pages.first.?, .y = 5, .x = 0 }; + try s.split(split_pin); + + const second_page = &s.pages.first.?.next.?.data; + + // Verify wrap flags are preserved in new page + // Original row 5 is now row 0 in second page + { + const rac0 = second_page.getRowAndCell(0, 0); + try testing.expect(rac0.row.wrap); + try testing.expect(!rac0.row.wrap_continuation); + } + + // Original row 6 is now row 1 in second page + { + const rac1 = second_page.getRowAndCell(0, 1); + try testing.expect(!rac1.row.wrap); + try testing.expect(rac1.row.wrap_continuation); + } + + // Original row 7 is now row 2 in second page + { + const rac2 = second_page.getRowAndCell(0, 2); + try testing.expect(rac2.row.wrap); + try testing.expect(rac2.row.wrap_continuation); + } +} + +test "PageList split preserves styled cells" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const page = &s.pages.first.?.data; + + // Create a style and apply it to cells in rows 5-7 (which will be in the second page) + const style: stylepkg.Style = .{ .flags = .{ .bold = true } }; + const style_id = try page.styles.add(page.memory, style); + + for (5..8) |y| { + const rac = page.getRowAndCell(0, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 'S' }, + .style_id = style_id, + }; + rac.row.styled = true; + page.styles.use(page.memory, style_id); + } + // Release the extra ref from add + page.styles.release(page.memory, style_id); + + // Split at row 5 + const split_pin: Pin = .{ .node = s.pages.first.?, .y = 5, .x = 0 }; + try s.split(split_pin); + + const first_page = &s.pages.first.?.data; + const second_page = &s.pages.first.?.next.?.data; + + // First page should have no styles (all styled rows moved to second page) + try testing.expectEqual(@as(usize, 0), first_page.styles.count()); + + // Second page should have exactly 1 style (the bold style, used by 3 cells) + try testing.expectEqual(@as(usize, 1), second_page.styles.count()); + + // Verify styled cells are preserved in new page + for (0..3) |y| { + const rac = second_page.getRowAndCell(0, y); + try testing.expectEqual(@as(u21, 'S'), rac.cell.content.codepoint); + try testing.expect(rac.cell.style_id != 0); + + const got_style = second_page.styles.get(second_page.memory, rac.cell.style_id); + try testing.expect(got_style.flags.bold); + try testing.expect(rac.row.styled); + } +} + +test "PageList split preserves grapheme clusters" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const page = &s.pages.first.?.data; + + // Add a grapheme cluster to row 6 (will be row 1 in second page after split at 5) + { + const rac = page.getRowAndCell(0, 6); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 0x1F468 }, // Man emoji + }; + try page.setGraphemes(rac.row, rac.cell, &.{ + 0x200D, // ZWJ + 0x1F469, // Woman emoji + }); + } + + // Split at row 5 + const split_pin: Pin = .{ .node = s.pages.first.?, .y = 5, .x = 0 }; + try s.split(split_pin); + + const first_page = &s.pages.first.?.data; + const second_page = &s.pages.first.?.next.?.data; + + // First page should have no graphemes (the grapheme row moved to second page) + try testing.expectEqual(@as(usize, 0), first_page.graphemeCount()); + + // Second page should have exactly 1 grapheme + try testing.expectEqual(@as(usize, 1), second_page.graphemeCount()); + + // Verify grapheme is preserved in new page (original row 6 is now row 1) + { + const rac = second_page.getRowAndCell(0, 1); + try testing.expectEqual(@as(u21, 0x1F468), rac.cell.content.codepoint); + try testing.expect(rac.row.grapheme); + + const cps = second_page.lookupGrapheme(rac.cell).?; + try testing.expectEqual(@as(usize, 2), cps.len); + try testing.expectEqual(@as(u21, 0x200D), cps[0]); + try testing.expectEqual(@as(u21, 0x1F469), cps[1]); + } +} + +test "PageList split preserves hyperlinks" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const page = &s.pages.first.?.data; + + // Add a hyperlink to row 7 (will be row 2 in second page after split at 5) + const hyperlink_id = try page.insertHyperlink(.{ + .id = .{ .implicit = 0 }, + .uri = "https://example.com", + }); + { + const rac = page.getRowAndCell(0, 7); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 'L' }, + }; + try page.setHyperlink(rac.row, rac.cell, hyperlink_id); + } + + // Split at row 5 + const split_pin: Pin = .{ .node = s.pages.first.?, .y = 5, .x = 0 }; + try s.split(split_pin); + + const first_page = &s.pages.first.?.data; + const second_page = &s.pages.first.?.next.?.data; + + // First page should have no hyperlinks (the hyperlink row moved to second page) + try testing.expectEqual(@as(usize, 0), first_page.hyperlink_set.count()); + + // Second page should have exactly 1 hyperlink + try testing.expectEqual(@as(usize, 1), second_page.hyperlink_set.count()); + + // Verify hyperlink is preserved in new page (original row 7 is now row 2) + { + const rac = second_page.getRowAndCell(0, 2); + try testing.expectEqual(@as(u21, 'L'), rac.cell.content.codepoint); + try testing.expect(rac.cell.hyperlink); + + const link_id = second_page.lookupHyperlink(rac.cell).?; + const link = second_page.hyperlink_set.get(second_page.memory, link_id); + try testing.expectEqualStrings("https://example.com", link.uri.slice(second_page.memory)); + } +} diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 980906e49..34a23787f 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -359,7 +359,7 @@ inline fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action { break :param null; }, .osc_put => osc_put: { - self.osc_parser.next(c); + @call(.always_inline, osc.Parser.next, .{ &self.osc_parser, c }); break :osc_put null; }, .csi_dispatch => csi_dispatch: { diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index ba2af2473..45fe9dfc6 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -9,6 +9,7 @@ const charsets = @import("charsets.zig"); const fastmem = @import("../fastmem.zig"); const kitty = @import("kitty.zig"); const sgr = @import("sgr.zig"); +const tripwire = @import("../tripwire.zig"); const unicode = @import("../unicode/main.zig"); const Selection = @import("Selection.zig"); const PageList = @import("PageList.zig"); @@ -241,7 +242,7 @@ pub const Options = struct { pub fn init( alloc: Allocator, opts: Options, -) !Screen { +) Allocator.Error!Screen { // Initialize our backing pages. var pages = try PageList.init( alloc, @@ -396,18 +397,6 @@ pub fn clone( alloc: Allocator, top: point.Point, bot: ?point.Point, -) !Screen { - return try self.clonePool(alloc, null, top, bot); -} - -/// Same as clone but you can specify a custom memory pool to use for -/// the screen. -pub fn clonePool( - self: *const Screen, - alloc: Allocator, - pool: ?*PageList.MemoryPool, - top: point.Point, - bot: ?point.Point, ) !Screen { // Create a tracked pin remapper for our selection and cursor. Note // that we may want to expose this generally in the future but at the @@ -415,14 +404,9 @@ pub fn clonePool( var pin_remap = PageList.Clone.TrackedPinsRemap.init(alloc); defer pin_remap.deinit(); - var pages = try self.pages.clone(.{ + var pages = try self.pages.clone(alloc, .{ .top = top, .bot = bot, - .memory = if (pool) |p| .{ - .pool = p, - } else .{ - .alloc = alloc, - }, .tracked_pins = &pin_remap, }); errdefer pages.deinit(); @@ -534,39 +518,38 @@ pub fn clonePool( result.assertIntegrity(); return result; } - -/// Adjust the capacity of a page within the pagelist of this screen. -/// This handles some accounting if the page being modified is the -/// cursor page. -pub fn adjustCapacity( +pub fn increaseCapacity( self: *Screen, node: *PageList.List.Node, - adjustment: PageList.AdjustCapacity, -) PageList.AdjustCapacityError!*PageList.List.Node { + adjustment: ?PageList.IncreaseCapacity, +) PageList.IncreaseCapacityError!*PageList.List.Node { // If the page being modified isn't our cursor page then // this is a quick operation because we have no additional - // accounting. - if (node != self.cursor.page_pin.node) { - return try self.pages.adjustCapacity(node, adjustment); - } + // accounting. We have to do this check here BEFORE calling + // increaseCapacity because increaseCapacity will update all + // our tracked pins (including our cursor). + if (node != self.cursor.page_pin.node) return try self.pages.increaseCapacity( + node, + adjustment, + ); - // We're modifying the cursor page. When we adjust the + // We're modifying the cursor page. When we increase the // capacity below it will be short the ref count on our // current style and hyperlink, so we need to init those. - const new_node = try self.pages.adjustCapacity(node, adjustment); + const new_node = try self.pages.increaseCapacity(node, adjustment); const new_page: *Page = &new_node.data; // Re-add the style, if the page somehow doesn't have enough // memory to add it, we emit a warning and gracefully degrade // to the default style for the cursor. - if (self.cursor.style_id != 0) { + if (self.cursor.style_id != style.default_id) { self.cursor.style_id = new_page.styles.add( new_page.memory, self.cursor.style, ) catch |err| id: { // TODO: Should we increase the capacity further in this case? log.warn( - "(Screen.adjustCapacity) Failed to add cursor style back to page, err={}", + "(Screen.increaseCapacity) Failed to add cursor style back to page, err={}", .{err}, ); @@ -588,7 +571,7 @@ pub fn adjustCapacity( self.startHyperlinkOnce(link.*) catch |err| { // TODO: Should we increase the capacity further in this case? log.warn( - "(Screen.adjustCapacity) Failed to add cursor hyperlink back to page, err={}", + "(Screen.increaseCapacity) Failed to add cursor hyperlink back to page, err={}", .{err}, ); }; @@ -657,9 +640,8 @@ pub fn cursorUp(self: *Screen, n: size.CellCountInt) void { defer self.assertIntegrity(); self.cursor.y -= n; // Must be set before cursorChangePin - const page_pin = self.cursor.page_pin.up(n).?; - self.cursorChangePin(page_pin); - const page_rac = page_pin.rowAndCell(); + self.cursorChangePin(self.cursor.page_pin.up(n).?); + const page_rac = self.cursor.page_pin.rowAndCell(); self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell; } @@ -684,9 +666,8 @@ pub fn cursorDown(self: *Screen, n: size.CellCountInt) void { // We move the offset into our page list to the next row and then // get the pointers to the row/cell and set all the cursor state up. - const page_pin = self.cursor.page_pin.down(n).?; - self.cursorChangePin(page_pin); - const page_rac = page_pin.rowAndCell(); + self.cursorChangePin(self.cursor.page_pin.down(n).?); + const page_rac = self.cursor.page_pin.rowAndCell(); self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell; } @@ -817,31 +798,37 @@ pub fn cursorDownScroll(self: *Screen) !void { // allocate, prune scrollback, whatever. _ = try self.pages.grow(); - // If our pin page change it means that the page that the pin - // was on was pruned. In this case, grow() moves the pin to - // the top-left of the new page. This effectively moves it by - // one already, we just need to fix up the x value. - const page_pin = if (old_pin.node == self.cursor.page_pin.node) - self.cursor.page_pin.down(1).? - else reuse: { - var pin = self.cursor.page_pin.*; - pin.x = self.cursor.x; - break :reuse pin; - }; + self.cursorChangePin(new_pin: { + // We do this all in a block here because referencing this pin + // after cursorChangePin is unsafe, and we want to keep it out + // of scope. - // These assertions help catch some pagelist math errors. Our - // x/y should be unchanged after the grow. - if (build_options.slow_runtime_safety) { - const active = self.pages.pointFromPin( - .active, - page_pin, - ).?.active; - assert(active.x == self.cursor.x); - assert(active.y == self.cursor.y); - } + // If our pin page change it means that the page that the pin + // was on was pruned. In this case, grow() moves the pin to + // the top-left of the new page. This effectively moves it by + // one already, we just need to fix up the x value. + const page_pin = if (old_pin.node == self.cursor.page_pin.node) + self.cursor.page_pin.down(1).? + else reuse: { + var pin = self.cursor.page_pin.*; + pin.x = self.cursor.x; + break :reuse pin; + }; - self.cursorChangePin(page_pin); - const page_rac = page_pin.rowAndCell(); + // These assertions help catch some pagelist math errors. Our + // x/y should be unchanged after the grow. + if (build_options.slow_runtime_safety) { + const active = self.pages.pointFromPin( + .active, + page_pin, + ).?.active; + assert(active.x == self.cursor.x); + assert(active.y == self.cursor.y); + } + + break :new_pin page_pin; + }); + const page_rac = self.cursor.page_pin.rowAndCell(); self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell; @@ -851,7 +838,7 @@ pub fn cursorDownScroll(self: *Screen) !void { // Clear the new row so it gets our bg color. We only do this // if we have a bg color at all. if (self.cursor.style.bg_color != .none) { - const page: *Page = &page_pin.node.data; + const page: *Page = &self.cursor.page_pin.node.data; self.clearCells( page, self.cursor.page_row, @@ -1098,6 +1085,11 @@ pub fn cursorCopy(self: *Screen, other: Cursor, opts: struct { /// page than the old AND we have a style or hyperlink set. In that case, /// we must release our old one and insert the new one, since styles are /// stored per-page. +/// +/// Note that this can change the cursor pin AGAIN if the process of +/// setting up our cursor forces a capacity adjustment of the underlying +/// cursor page, so any references to the page pin should be re-read +/// from `self.cursor.page_pin` after calling this. inline fn cursorChangePin(self: *Screen, new: Pin) void { // Moving the cursor affects text run splitting (ligatures) so // we must mark the old and new page dirty. We do this as long @@ -1114,14 +1106,19 @@ inline fn cursorChangePin(self: *Screen, new: Pin) void { return; } - // If we have a old style then we need to release it from the old page. + // If we have an old style then we need to release it from the old page. const old_style_: ?style.Style = if (self.cursor.style_id == style.default_id) null else self.cursor.style; if (old_style_ != null) { + // Release the style directly from the old page instead of going through + // manualStyleUpdate, because the cursor position may have already been + // updated but the pin has not, which would fail integrity checks. + const old_page: *Page = &self.cursor.page_pin.node.data; + old_page.styles.release(old_page.memory, self.cursor.style_id); self.cursor.style = .{}; - self.manualStyleUpdate() catch unreachable; // Removing a style should never fail + self.cursor.style_id = style.default_id; } // If we have a hyperlink then we need to release it from the old page. @@ -1793,9 +1790,25 @@ fn resizeInternal( /// Set a style attribute for the current cursor. /// -/// This can cause a page split if the current page cannot fit this style. -/// This is the only scenario an error return is possible. -pub fn setAttribute(self: *Screen, attr: sgr.Attribute) !void { +/// If the style can't be set due to any internal errors (memory-related), +/// then this will revert back to the existing style and return an error. +pub fn setAttribute( + self: *Screen, + attr: sgr.Attribute, +) PageList.IncreaseCapacityError!void { + // If we fail to set our style for any reason, we should revert + // back to the old style. If we fail to do that, we revert back to + // the default style. + const old_style = self.cursor.style; + errdefer { + self.cursor.style = old_style; + self.manualStyleUpdate() catch |err| { + log.warn("setAttribute error restoring old style after failure err={}", .{err}); + self.cursor.style = .{}; + self.manualStyleUpdate() catch unreachable; + }; + } + switch (attr) { .unset => { self.cursor.style = .{}; @@ -1938,7 +1951,21 @@ pub fn setAttribute(self: *Screen, attr: sgr.Attribute) !void { } /// Call this whenever you manually change the cursor style. -pub fn manualStyleUpdate(self: *Screen) !void { +/// +/// This function can NOT fail if the cursor style is changing to the +/// default style. +/// +/// If this returns an error, the style change did not take effect and +/// the cursor style is reverted back to the default. The only scenario +/// this returns an error is if there is a physical memory allocation failure +/// or if there is no possible way to increase style capacity to store +/// the style. +/// +/// This function WILL split pages as necessary to accommodate the new style. +/// So if OutOfSpace is returned, it means that even after splitting the page +/// there was still no room for the new style. +pub fn manualStyleUpdate(self: *Screen) PageList.IncreaseCapacityError!void { + defer self.assertIntegrity(); var page: *Page = &self.cursor.page_pin.node.data; // std.log.warn("active styles={}", .{page.styles.count()}); @@ -1957,6 +1984,9 @@ pub fn manualStyleUpdate(self: *Screen) !void { // Clear the cursor style ID to prevent weird things from happening // if the page capacity has to be adjusted which would end up calling // manualStyleUpdate again. + // + // This also ensures that if anything fails below, we fall back to + // clearing our style. self.cursor.style_id = style.default_id; // After setting the style, we need to update our style map. @@ -1968,30 +1998,115 @@ pub fn manualStyleUpdate(self: *Screen) !void { page.memory, self.cursor.style, ) catch |err| id: { - // Our style map is full or needs to be rehashed, - // so we allocate a new page, which will rehash, - // and double the style capacity for it if it was - // full. - const node = try self.adjustCapacity( + // Our style map is full or needs to be rehashed, so we need to + // increase style capacity (or rehash). + const node = self.increaseCapacity( self.cursor.page_pin.node, switch (err) { - error.OutOfMemory => .{ .styles = page.capacity.styles * 2 }, - error.NeedsRehash => .{}, + error.OutOfMemory => .styles, + error.NeedsRehash => null, }, - ); + ) catch |increase_err| switch (increase_err) { + error.OutOfMemory => return error.OutOfMemory, + error.OutOfSpace => space: { + // Out of space, we need to split the page. Split wherever + // is using less capacity and hope that works. If it doesn't + // work, we tried. + try self.splitForCapacity(self.cursor.page_pin.*); + break :space self.cursor.page_pin.node; + }, + }; page = &node.data; - break :id try page.styles.add( + break :id page.styles.add( page.memory, self.cursor.style, - ); + ) catch |err2| switch (err2) { + error.OutOfMemory => { + // This shouldn't happen because increaseCapacity is + // guaranteed to increase our capacity by at least one and + // we only need one space, but again, I don't want to crash + // here so let's log loudly and reset. + log.err("style addition failed after capacity increase", .{}); + return error.OutOfMemory; + }, + error.NeedsRehash => { + // This should be impossible because we rehash above + // and rehashing should never result in a duplicate. But + // we don't want to simply hard crash so log it and + // clear our style. + log.err("style rehash resulted in needs rehash", .{}); + return; + }, + }; }; + errdefer page.styles.release(page.memory, id); + self.cursor.style_id = id; - self.assertIntegrity(); +} + +/// Split at the given pin so that the pinned row moves to the page +/// with less used capacity after the split. +/// +/// The primary use case for this is to handle IncreaseCapacityError +/// OutOfSpace conditions where we need to split the page in order +/// to make room for more managed memory. +/// +/// If the caller cares about where the pin moves to, they should +/// setup a tracked pin before calling this and then check that. +/// In many calling cases, the input pin is tracked (e.g. the cursor +/// pin). +/// +/// If this returns OOM then its a system OOM. If this returns OutOfSpace +/// then it means the page can't be split further. +fn splitForCapacity( + self: *Screen, + pin: Pin, +) PageList.SplitError!void { + // Get our capacities. We include our target row because its + // capacity will be preserved. + const bytes_above = Page.layout(pin.node.data.exactRowCapacity( + 0, + pin.y + 1, + )).total_size; + const bytes_below = Page.layout(pin.node.data.exactRowCapacity( + pin.y, + pin.node.data.size.rows, + )).total_size; + + // We need to track the old cursor pin because if our split + // moves the cursor pin we need to update our accounting. + const old_cursor = self.cursor.page_pin.*; + + // If our bytes above are less than bytes below, we move the pin + // to split down one since splitting includes the pinned row in + // the new node. + try self.pages.split(if (bytes_above < bytes_below) + pin.down(1) orelse pin + else + pin); + + // Cursor didn't change nodes, we're done. + if (self.cursor.page_pin.node == old_cursor.node) return; + + // Cursor changed, we need to restore the old pin then use + // cursorChangePin to move to the new pin. The old node is guaranteed + // to still exist, just not the row. + // + // Note that page_row and all that will be invalid, it points to the + // new node, but at the time of writing this we don't need any of that + // to be right in cursorChangePin. + const new_cursor = self.cursor.page_pin.*; + self.cursor.page_pin.* = old_cursor; + self.cursorChangePin(new_cursor); } /// Append a grapheme to the given cell within the current cursor row. -pub fn appendGrapheme(self: *Screen, cell: *Cell, cp: u21) !void { +pub fn appendGrapheme( + self: *Screen, + cell: *Cell, + cp: u21, +) PageList.IncreaseCapacityError!void { defer self.cursor.page_pin.node.data.assertIntegrity(); self.cursor.page_pin.node.data.appendGrapheme( self.cursor.page_row, @@ -2011,11 +2126,9 @@ pub fn appendGrapheme(self: *Screen, cell: *Cell, cp: u21) !void { // Adjust our capacity. This will update our cursor page pin and // force us to reload. - const original_node = self.cursor.page_pin.node; - const new_bytes = original_node.data.capacity.grapheme_bytes * 2; - _ = try self.adjustCapacity( - original_node, - .{ .grapheme_bytes = new_bytes }, + _ = try self.increaseCapacity( + self.cursor.page_pin.node, + .grapheme_bytes, ); // The cell pointer is now invalid, so we need to get it from @@ -2026,17 +2139,22 @@ pub fn appendGrapheme(self: *Screen, cell: *Cell, cp: u21) !void { .gt => self.cursorCellRight(@intCast(cell_idx - self.cursor.x)), }; - try self.cursor.page_pin.node.data.appendGrapheme( + self.cursor.page_pin.node.data.appendGrapheme( self.cursor.page_row, reloaded_cell, cp, - ); + ) catch |err2| { + comptime assert(@TypeOf(err2) == error{OutOfMemory}); + // This should never happen because we just increased capacity. + // Log loudly but still return an error so we don't just + // crash. + log.err("grapheme append failed after capacity increase", .{}); + return err2; + }; }, }; } -pub const StartHyperlinkError = Allocator.Error || PageList.AdjustCapacityError; - /// Start the hyperlink state. Future cells will be marked as hyperlinks with /// this state. Note that various terminal operations may clear the hyperlink /// state, such as switching screens (alt screen). @@ -2044,7 +2162,7 @@ pub fn startHyperlink( self: *Screen, uri: []const u8, id_: ?[]const u8, -) StartHyperlinkError!void { +) PageList.IncreaseCapacityError!void { // Create our pending entry. const link: hyperlink.Hyperlink = .{ .uri = uri, @@ -2069,21 +2187,21 @@ pub fn startHyperlink( error.OutOfMemory => return error.OutOfMemory, // strings table is out of memory, adjust it up - error.StringsOutOfMemory => _ = try self.adjustCapacity( + error.StringsOutOfMemory => _ = try self.increaseCapacity( self.cursor.page_pin.node, - .{ .string_bytes = self.cursor.page_pin.node.data.capacity.string_bytes * 2 }, + .string_bytes, ), // hyperlink set is out of memory, adjust it up - error.SetOutOfMemory => _ = try self.adjustCapacity( + error.SetOutOfMemory => _ = try self.increaseCapacity( self.cursor.page_pin.node, - .{ .hyperlink_bytes = self.cursor.page_pin.node.data.capacity.hyperlink_bytes * 2 }, + .hyperlink_bytes, ), // hyperlink set is too full, rehash it - error.SetNeedsRehash => _ = try self.adjustCapacity( + error.SetNeedsRehash => _ = try self.increaseCapacity( self.cursor.page_pin.node, - .{}, + null, ), } @@ -2145,7 +2263,7 @@ pub fn endHyperlink(self: *Screen) void { } /// Set the current hyperlink state on the current cell. -pub fn cursorSetHyperlink(self: *Screen) !void { +pub fn cursorSetHyperlink(self: *Screen) PageList.IncreaseCapacityError!void { assert(self.cursor.hyperlink_id != 0); var page = &self.cursor.page_pin.node.data; @@ -2160,40 +2278,38 @@ pub fn cursorSetHyperlink(self: *Screen) !void { } else |err| switch (err) { // hyperlink_map is out of space, realloc the page to be larger error.HyperlinkMapOutOfMemory => { - const uri_size = if (self.cursor.hyperlink) |link| link.uri.len else 0; - - var string_bytes = page.capacity.string_bytes; - // Attempt to allocate the space that would be required to // insert a new copy of the cursor hyperlink uri in to the - // string alloc, since right now adjustCapacity always just + // string alloc, since right now increaseCapacity always just // adds an extra copy even if one already exists in the page. // If this alloc fails then we know we also need to grow our // string bytes. // - // FIXME: This SUCKS - if (page.string_alloc.alloc( - u8, - page.memory, - uri_size, - )) |slice| { - // We don't bother freeing because we're - // about to free the entire page anyway. - _ = &slice; - } else |_| { - // We didn't have enough room, let's just double our - // string bytes until there's definitely enough room - // for our uri. - const before = string_bytes; - while (string_bytes - before < uri_size) string_bytes *= 2; + // FIXME: increaseCapacity should not do this. + while (self.cursor.hyperlink) |link| { + if (page.string_alloc.alloc( + u8, + page.memory, + link.uri.len, + )) |slice| { + // We don't bother freeing because we're + // about to free the entire page anyway. + _ = slice; + break; + } else |_| {} + + // We didn't have enough room, let's increase string bytes + const new_node = try self.increaseCapacity( + self.cursor.page_pin.node, + .string_bytes, + ); + assert(new_node == self.cursor.page_pin.node); + page = &new_node.data; } - _ = try self.adjustCapacity( + _ = try self.increaseCapacity( self.cursor.page_pin.node, - .{ - .hyperlink_bytes = page.capacity.hyperlink_bytes * 2, - .string_bytes = string_bytes, - }, + .hyperlink_bytes, ); // Retry @@ -2209,7 +2325,7 @@ pub fn cursorSetHyperlink(self: *Screen) !void { } /// Set the selection to the given selection. If this is a tracked selection -/// then the screen will take overnship of the selection. If this is untracked +/// then the screen will take ownership of the selection. If this is untracked /// then the screen will convert it to tracked internally. This will automatically /// untrack the prior selection (if any). /// @@ -2218,7 +2334,7 @@ pub fn cursorSetHyperlink(self: *Screen) !void { /// This is always recommended over setting `selection` directly. Beyond /// managing memory for you, it also performs safety checks that the selection /// is always tracked. -pub fn select(self: *Screen, sel_: ?Selection) !void { +pub fn select(self: *Screen, sel_: ?Selection) Allocator.Error!void { const sel = sel_ orelse { self.clearSelection(); return; @@ -2256,6 +2372,10 @@ pub const SelectionString = struct { map: ?*StringMap = null, }; +const selectionString_tw = tripwire.module(enum { + copy_map, +}, selectionString); + /// Returns the raw text associated with a selection. This will unwrap /// soft-wrapped edges. The returned slice is owned by the caller and allocated /// using alloc, not the allocator associated with the screen (unless they match). @@ -2265,7 +2385,7 @@ pub fn selectionString( self: *Screen, alloc: Allocator, opts: SelectionString, -) ![:0]const u8 { +) Allocator.Error![:0]const u8 { // We'll use this as our buffer to build our string. var aw: std.Io.Writer.Allocating = .init(alloc); defer aw.deinit(); @@ -2289,19 +2409,23 @@ pub fn selectionString( .map = &pins, }; - // Emit - try formatter.format(&aw.writer); + // Emit. Since this is an allocating writer, a failed write + // just becomes an OOM. + formatter.format(&aw.writer) catch return error.OutOfMemory; // Build our final text and if we have a string map set that up. const text = try aw.toOwnedSliceSentinel(0); errdefer alloc.free(text); if (opts.map) |map| { + const map_string = try alloc.dupeZ(u8, text); + errdefer alloc.free(map_string); + try selectionString_tw.check(.copy_map); + const map_pins = try pins.toOwnedSlice(alloc); map.* = .{ - .string = try alloc.dupeZ(u8, text), - .map = try pins.toOwnedSlice(alloc), + .string = map_string, + .map = map_pins, }; } - errdefer if (opts.map) |m| m.deinit(alloc); return text; } @@ -2502,11 +2626,15 @@ pub fn selectAll(self: *Screen) ?Selection { /// end_pt (inclusive). Because it selects "nearest" to start point, start /// point can be before or after end point. /// +/// The boundary_codepoints parameter should be a slice of u21 codepoints that +/// mark word boundaries, passed through to selectWord. +/// /// TODO: test this pub fn selectWordBetween( self: *Screen, start: Pin, end: Pin, + boundary_codepoints: []const u21, ) ?Selection { const dir: PageList.Direction = if (start.before(end)) .right_down else .left_up; var it = start.cellIterator(dir, end); @@ -2518,7 +2646,7 @@ pub fn selectWordBetween( } // If we found a word, then return it - if (self.selectWord(pin)) |sel| return sel; + if (self.selectWord(pin, boundary_codepoints)) |sel| return sel; } return null; @@ -2530,33 +2658,16 @@ pub fn selectWordBetween( /// /// This will return null if a selection is impossible. The only scenario /// this happens is if the point pt is outside of the written screen space. -pub fn selectWord(self: *Screen, pin: Pin) ?Selection { +/// +/// The boundary_codepoints parameter should be a slice of u21 codepoints that +/// mark word boundaries. This is expected to be pre-parsed from the config. +pub fn selectWord( + self: *Screen, + pin: Pin, + boundary_codepoints: []const u21, +) ?Selection { _ = self; - // Boundary characters for selection purposes - const boundary = &[_]u32{ - 0, - ' ', - '\t', - '\'', - '"', - '│', - '`', - '|', - ':', - ';', - ',', - '(', - ')', - '[', - ']', - '{', - '}', - '<', - '>', - '$', - }; - // If our cell is empty we can't select a word, because we can't select // areas where the screen is not yet written. const start_cell = pin.rowAndCell().cell; @@ -2564,9 +2675,9 @@ pub fn selectWord(self: *Screen, pin: Pin) ?Selection { // Determine if we are a boundary or not to determine what our boundary is. const expect_boundary = std.mem.indexOfAny( - u32, - boundary, - &[_]u32{start_cell.content.codepoint}, + u21, + boundary_codepoints, + &[_]u21{start_cell.content.codepoint}, ) != null; // Go forwards to find our end boundary @@ -2582,9 +2693,9 @@ pub fn selectWord(self: *Screen, pin: Pin) ?Selection { // If we do not match our expected set, we hit a boundary const this_boundary = std.mem.indexOfAny( - u32, - boundary, - &[_]u32{cell.content.codepoint}, + u21, + boundary_codepoints, + &[_]u21{cell.content.codepoint}, ) != null; if (this_boundary != expect_boundary) break :end prev; @@ -2619,9 +2730,9 @@ pub fn selectWord(self: *Screen, pin: Pin) ?Selection { // If we do not match our expected set, we hit a boundary const this_boundary = std.mem.indexOfAny( - u32, - boundary, - &[_]u32{cell.content.codepoint}, + u21, + boundary_codepoints, + &[_]u21{cell.content.codepoint}, ) != null; if (this_boundary != expect_boundary) break :start prev; @@ -3015,15 +3126,15 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void { .protected = self.cursor.protected, }; - // If we have a hyperlink, add it to the cell. - if (self.cursor.hyperlink_id > 0) try self.cursorSetHyperlink(); - // If we have a ref-counted style, increase. if (self.cursor.style_id != style.default_id) { const page = self.cursor.page_pin.node.data; page.styles.use(page.memory, self.cursor.style_id); self.cursor.page_row.styled = true; } + + // If we have a hyperlink, add it to the cell. + if (self.cursor.hyperlink_id > 0) try self.cursorSetHyperlink(); }, 2 => { @@ -7584,6 +7695,12 @@ test "Screen: selectWord" { defer s.deinit(); try s.testWriteString("ABC DEF\n 123\n456"); + // Default boundary codepoints for word selection + const boundary_codepoints = &[_]u21{ + 0, ' ', '\t', '\'', '"', '│', '`', '|', ':', ';', + ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', + }; + // Outside of active area // try testing.expect(s.selectWord(.{ .x = 9, .y = 0 }) == null); // try testing.expect(s.selectWord(.{ .x = 0, .y = 5 }) == null); @@ -7593,7 +7710,7 @@ test "Screen: selectWord" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 0, .y = 0, - } }).?).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -7610,7 +7727,7 @@ test "Screen: selectWord" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 2, .y = 0, - } }).?).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -7627,7 +7744,7 @@ test "Screen: selectWord" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 0, - } }).?).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -7644,7 +7761,7 @@ test "Screen: selectWord" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 3, .y = 0, - } }).?).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 3, @@ -7661,7 +7778,7 @@ test "Screen: selectWord" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 0, .y = 1, - } }).?).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -7678,7 +7795,7 @@ test "Screen: selectWord" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 2, - } }).?).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -7699,6 +7816,12 @@ test "Screen: selectWord across soft-wrap" { defer s.deinit(); try s.testWriteString(" 1234012\n 123"); + // Default boundary codepoints for word selection + const boundary_codepoints = &[_]u21{ + 0, ' ', '\t', '\'', '"', '│', '`', '|', ':', ';', + ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', + }; + { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); @@ -7710,7 +7833,7 @@ test "Screen: selectWord across soft-wrap" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 0, - } }).?).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, @@ -7727,7 +7850,7 @@ test "Screen: selectWord across soft-wrap" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 1, - } }).?).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, @@ -7744,7 +7867,7 @@ test "Screen: selectWord across soft-wrap" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 3, .y = 0, - } }).?).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, @@ -7765,12 +7888,18 @@ test "Screen: selectWord whitespace across soft-wrap" { defer s.deinit(); try s.testWriteString("1 1\n 123"); + // Default boundary codepoints for word selection + const boundary_codepoints = &[_]u21{ + 0, ' ', '\t', '\'', '"', '│', '`', '|', ':', ';', + ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', + }; + // Going forward { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 0, - } }).?).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, @@ -7787,7 +7916,7 @@ test "Screen: selectWord whitespace across soft-wrap" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 1, - } }).?).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, @@ -7804,7 +7933,7 @@ test "Screen: selectWord whitespace across soft-wrap" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 3, .y = 0, - } }).?).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, @@ -7821,6 +7950,12 @@ test "Screen: selectWord with character boundary" { const testing = std.testing; const alloc = testing.allocator; + // Default boundary codepoints for word selection + const boundary_codepoints = &[_]u21{ + 0, ' ', '\t', '\'', '"', '│', '`', '|', ':', ';', + ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', + }; + const cases = [_][]const u8{ " 'abc' \n123", " \"abc\" \n123", @@ -7851,7 +7986,7 @@ test "Screen: selectWord with character boundary" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 2, .y = 0, - } }).?).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, @@ -7868,7 +8003,7 @@ test "Screen: selectWord with character boundary" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 4, .y = 0, - } }).?).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, @@ -7885,7 +8020,7 @@ test "Screen: selectWord with character boundary" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 3, .y = 0, - } }).?).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, @@ -7904,7 +8039,7 @@ test "Screen: selectWord with character boundary" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 0, - } }).?).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -8904,132 +9039,468 @@ test "Screen: cursorSetHyperlink OOM + URI too large for string alloc" { try testing.expect(base_string_bytes < s.cursor.page_pin.node.data.capacity.string_bytes); } -test "Screen: adjustCapacity cursor style ref count" { +test "Screen: increaseCapacity cursor style ref count preserved" { const testing = std.testing; const alloc = testing.allocator; - var s = try init(alloc, .{ .cols = 5, .rows = 5, .max_scrollback = 0 }); + var s = try init(alloc, .{ + .cols = 5, + .rows = 5, + .max_scrollback = 0, + }); defer s.deinit(); - - try s.setAttribute(.{ .bold = {} }); + try s.setAttribute(.bold); try s.testWriteString("1ABCD"); + // We should have one page and it should be our cursor page + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try testing.expect(s.pages.pages.first == s.cursor.page_pin.node); + + const old_style = s.cursor.style; + { const page = &s.pages.pages.last.?.data; + // 5 chars + cursor = 6 refs try testing.expectEqual( - 6, // All chars + cursor + 6, page.styles.refCount(page.memory, s.cursor.style_id), ); } - // This forces the page to change. - _ = try s.adjustCapacity( + // This forces the page to change via increaseCapacity. + const new_node = try s.increaseCapacity( s.cursor.page_pin.node, - .{ .grapheme_bytes = s.cursor.page_pin.node.data.capacity.grapheme_bytes * 2 }, + .grapheme_bytes, ); - // Our ref counts should still be the same + // Cursor's page_pin should now point to the new node + try testing.expect(s.cursor.page_pin.node == new_node); + + // Verify cursor's page_cell and page_row are correctly reloaded from the pin + const page_rac = s.cursor.page_pin.rowAndCell(); + try testing.expect(s.cursor.page_row == page_rac.row); + try testing.expect(s.cursor.page_cell == page_rac.cell); + + // Style should be preserved + try testing.expectEqual(old_style, s.cursor.style); + try testing.expect(s.cursor.style_id != style.default_id); + + // After increaseCapacity, the 5 chars are cloned (5 refs) and + // the cursor's style is re-added (1 ref) = 6 total. { const page = &s.pages.pages.last.?.data; - try testing.expectEqual( - 6, // All chars + cursor - page.styles.refCount(page.memory, s.cursor.style_id), - ); + const ref_count = page.styles.refCount(page.memory, s.cursor.style_id); + try testing.expectEqual(6, ref_count); } } -test "Screen: adjustCapacity cursor hyperlink exceeds string alloc size" { +test "Screen: increaseCapacity cursor hyperlink ref count preserved" { const testing = std.testing; const alloc = testing.allocator; - var s = try init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }); + var s = try init(alloc, .{ + .cols = 5, + .rows = 5, + .max_scrollback = 0, + }); defer s.deinit(); + try s.startHyperlink("https://example.com/", null); + try s.testWriteString("1ABCD"); - // Start a hyperlink with a URI that just barely fits in the string alloc. - // This will ensure that the redundant copy added in `adjustCapacity` won't - // fit in the available string alloc space. - const uri = "a" ** (pagepkg.std_capacity.string_bytes - 8); - try s.startHyperlink(uri, null); + // We should have one page and it should be our cursor page + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try testing.expect(s.pages.pages.first == s.cursor.page_pin.node); - // Write some characters with this so that the URI - // is copied to the new page when adjusting capacity. - try s.testWriteString("Hello"); + { + const page = &s.pages.pages.last.?.data; + // Cursor has the hyperlink active = 1 count in hyperlink_set + try testing.expectEqual(1, page.hyperlink_set.count()); + try testing.expect(s.cursor.hyperlink_id != 0); + try testing.expect(s.cursor.hyperlink != null); + } - // Adjust the capacity, right now this will cause a redundant copy of - // the URI to be added to the string alloc, but since there isn't room - // for this this will clear the cursor hyperlink. - _ = try s.adjustCapacity(s.cursor.page_pin.node, .{}); + // This forces the page to change via increaseCapacity. + _ = try s.increaseCapacity( + s.cursor.page_pin.node, + .grapheme_bytes, + ); - // The cursor hyperlink should have been cleared by the `adjustCapacity` - // call, because there isn't enough room to add the redundant URI string. - // - // This behavior will change, causing this test to fail, if any of these - // changes are made: - // - // - The string alloc is changed to intern strings. - // - // - The adjustCapacity function is changed to ensure the new - // capacity will fit the redundant copy of the hyperlink uri. - // - // - The cursor managed memory handling is reworked so that it - // doesn't reside in the pages anymore and doesn't need this - // accounting. - // - // In such a case, adjust this test accordingly. - try testing.expectEqual(null, s.cursor.hyperlink); - try testing.expectEqual(0, s.cursor.hyperlink_id); + // Hyperlink should be preserved with correct URI + try testing.expect(s.cursor.hyperlink != null); + try testing.expect(s.cursor.hyperlink_id != 0); + try testing.expectEqualStrings("https://example.com/", s.cursor.hyperlink.?.uri); + + // After increaseCapacity, the hyperlink is re-added to the new page. + { + const page = &s.pages.pages.last.?.data; + try testing.expectEqual(1, page.hyperlink_set.count()); + } } -test "Screen: adjustCapacity cursor style exceeds style set capacity" { +test "Screen: increaseCapacity cursor with both style and hyperlink preserved" { const testing = std.testing; const alloc = testing.allocator; - var s = try init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); + var s = try init(alloc, .{ + .cols = 5, + .rows = 5, + .max_scrollback = 0, + }); defer s.deinit(); + // Set both a non-default style AND an active hyperlink. + // Write one character first with bold to mark the row as styled, + // then start the hyperlink and write more characters. + try s.setAttribute(.bold); + try s.startHyperlink("https://example.com/", null); + try s.testWriteString("1ABCD"); + + // We should have one page and it should be our cursor page + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try testing.expect(s.pages.pages.first == s.cursor.page_pin.node); + + const old_style = s.cursor.style; + + { + const page = &s.pages.pages.last.?.data; + // 5 chars + cursor = 6 refs for bold style + try testing.expectEqual( + 6, + page.styles.refCount(page.memory, s.cursor.style_id), + ); + // Cursor has the hyperlink active = 1 count in hyperlink_set + try testing.expectEqual(1, page.hyperlink_set.count()); + try testing.expect(s.cursor.style_id != style.default_id); + try testing.expect(s.cursor.hyperlink_id != 0); + try testing.expect(s.cursor.hyperlink != null); + } + + // This forces the page to change via increaseCapacity. + _ = try s.increaseCapacity( + s.cursor.page_pin.node, + .grapheme_bytes, + ); + + // Style should be preserved + try testing.expectEqual(old_style, s.cursor.style); + try testing.expect(s.cursor.style_id != style.default_id); + + // Hyperlink should be preserved with correct URI + try testing.expect(s.cursor.hyperlink != null); + try testing.expect(s.cursor.hyperlink_id != 0); + try testing.expectEqualStrings("https://example.com/", s.cursor.hyperlink.?.uri); + + // After increaseCapacity, both style and hyperlink are re-added to the new page. + { + const page = &s.pages.pages.last.?.data; + const ref_count = page.styles.refCount(page.memory, s.cursor.style_id); + try testing.expectEqual(6, ref_count); + try testing.expectEqual(1, page.hyperlink_set.count()); + } +} + +test "Screen: increaseCapacity non-cursor page returns early" { + // Test that calling increaseCapacity on a page that is NOT the cursor's + // page properly delegates to pages.increaseCapacity without doing the + // extra cursor accounting (style/hyperlink re-adding). + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, .{ + .cols = 80, + .rows = 24, + .max_scrollback = 10000, + }); + defer s.deinit(); + + // Set up a custom style and hyperlink on the cursor + try s.setAttribute(.bold); + try s.startHyperlink("https://example.com/", null); + try s.testWriteString("Hello"); + + // Store cursor state before growing pages + const old_style = s.cursor.style; + const old_style_id = s.cursor.style_id; + const old_hyperlink = s.cursor.hyperlink; + const old_hyperlink_id = s.cursor.hyperlink_id; + + // The cursor is on the first (and only) page + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try testing.expect(s.cursor.page_pin.node == s.pages.pages.first.?); + + // Grow pages until we have multiple pages. The cursor's pin stays on + // the first page since we're just adding rows. + const first_page_node = s.pages.pages.first.?; + first_page_node.data.pauseIntegrityChecks(true); + for (0..first_page_node.data.capacity.rows - first_page_node.data.size.rows) |_| { + _ = try s.pages.grow(); + } + first_page_node.data.pauseIntegrityChecks(false); + _ = try s.pages.grow(); + + // Now we have two pages + try testing.expect(s.pages.pages.first != s.pages.pages.last); + const second_page = s.pages.pages.last.?; + + // Cursor should still be on the first page (where it was created) + try testing.expect(s.cursor.page_pin.node == s.pages.pages.first.?); + try testing.expect(s.cursor.page_pin.node != second_page); + + const second_page_styles_cap = second_page.data.capacity.styles; + const cursor_page_styles_cap = s.cursor.page_pin.node.data.capacity.styles; + + // Call increaseCapacity on the second page (NOT the cursor's page) + const new_second_page = try s.increaseCapacity(second_page, .styles); + + // The second page should have increased capacity + try testing.expectEqual( + second_page_styles_cap * 2, + new_second_page.data.capacity.styles, + ); + + // The cursor's page (first page) should be unchanged + try testing.expectEqual( + cursor_page_styles_cap, + s.cursor.page_pin.node.data.capacity.styles, + ); + + // Cursor state should be completely unchanged since we didn't touch its page + try testing.expectEqual(old_style, s.cursor.style); + try testing.expectEqual(old_style_id, s.cursor.style_id); + try testing.expectEqual(old_hyperlink, s.cursor.hyperlink); + try testing.expectEqual(old_hyperlink_id, s.cursor.hyperlink_id); + + // Verify hyperlink is still valid + try testing.expect(s.cursor.hyperlink != null); + try testing.expectEqualStrings("https://example.com/", s.cursor.hyperlink.?.uri); +} + +test "Screen: cursorDown to page with insufficient capacity" { + // Regression test for https://github.com/ghostty-org/ghostty/issues/10282 + // + // This test exposes a use-after-realloc bug in cursorDown (and similar + // cursor movement functions). The bug pattern: + // + // 1. cursorDown creates a by-value copy of the pin via page_pin.down(n) + // 2. cursorChangePin is called, which may trigger adjustCapacity + // if the target page's style map is full + // 3. adjustCapacity frees the old page and creates a new one + // 4. The local pin copy still points to the freed page + // 5. rowAndCell() on the stale pin accesses freed memory + + const testing = std.testing; + const alloc = testing.allocator; + + // Small screen to make page boundary crossing easy to set up + var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 1 }); + defer s.deinit(); + + // Scroll down enough to create a second page + const start_page = &s.pages.pages.last.?.data; + const rem = start_page.capacity.rows; + start_page.pauseIntegrityChecks(true); + for (0..rem) |_| try s.cursorDownOrScroll(); + start_page.pauseIntegrityChecks(false); + + // Cursor should now be on a new page + const new_page = &s.cursor.page_pin.node.data; + try testing.expect(start_page != new_page); + + // Fill new_page's style map to capacity. When we move INTO this page + // with a style set, adjustCapacity will be triggered. + { + new_page.pauseIntegrityChecks(true); + defer new_page.pauseIntegrityChecks(false); + defer new_page.assertIntegrity(); + + var n: u24 = 1; + while (new_page.styles.add( + new_page.memory, + .{ .bg_color = .{ .rgb = @bitCast(n) } }, + )) |_| n += 1 else |_| {} + } + + // Move cursor to start of active area and set a style + s.cursorAbsolute(0, 0); + try s.setAttribute(.bold); + try testing.expect(s.cursor.style.flags.bold); + try testing.expect(s.cursor.style_id != style.default_id); + + // Find the row just before the page boundary + for (0..s.pages.rows - 1) |row| { + s.cursorAbsolute(0, @intCast(row)); + const cur_node = s.cursor.page_pin.node; + if (s.cursor.page_pin.down(1)) |next_pin| { + if (next_pin.node != cur_node) { + // Cursor is at 'row', moving down crosses to new_page + try testing.expect(&next_pin.node.data == new_page); + + // This cursorDown triggers the bug: the local page_pin copy + // becomes stale after adjustCapacity, causing rowAndCell() + // to access freed memory. + s.cursorDown(1); + + // If the fix is applied, verify correct state + try testing.expect(s.cursor.y == row + 1); + try testing.expect(s.cursor.style.flags.bold); + + break; + } + } + } else { + // Didn't find boundary + try testing.expect(false); + } +} + +test "Screen setAttribute increases capacity when style map is full" { + // Tests that setAttribute succeeds when the style map is full by + // increasing page capacity. When capacity is at max and increaseCapacity + // returns OutOfSpace, manualStyleUpdate will split the page instead. + const testing = std.testing; + const alloc = testing.allocator; + + // Use a small screen with multiple rows + var s = try init(alloc, .{ .cols = 10, .rows = 5, .max_scrollback = 10 }); + defer s.deinit(); + + // Write content to multiple rows + try s.testWriteString("line1\nline2\nline3\nline4\nline5"); + + // Get the page and fill its style map to capacity const page = &s.cursor.page_pin.node.data; + const original_styles_capacity = page.capacity.styles; - // We add unique styles to the page until no more will fit. - fill: for (0..255) |bg| { - for (0..255) |fg| { - const st: style.Style = .{ - .bg_color = .{ .palette = @intCast(bg) }, - .fg_color = .{ .palette = @intCast(fg) }, - }; + // Fill the style map to capacity using the StyleSet's layout capacity + // which accounts for the load factor + { + page.pauseIntegrityChecks(true); + defer page.pauseIntegrityChecks(false); + defer page.assertIntegrity(); - s.cursor.style = st; - - // Try to insert the new style, if it doesn't fit then - // we succeeded in filling the style set, so we break. - s.cursor.style_id = page.styles.add( + const max_items = page.styles.layout.cap; + var n: usize = 1; + while (n < max_items) : (n += 1) { + _ = page.styles.add( page.memory, - s.cursor.style, - ) catch break :fill; - - try s.testWriteString("a"); + .{ .bg_color = .{ .rgb = @bitCast(@as(u24, @intCast(n))) } }, + ) catch break; } } - // Adjust the capacity, this should cause the style set to reach the - // same state it was in to begin with, since it will clone the page - // in the same order as the styles were added to begin with, meaning - // the cursor style will not be able to be added to the set, which - // should, right now, result in the cursor style being cleared. - _ = try s.adjustCapacity(s.cursor.page_pin.node, .{}); + // Now try to set a new unique attribute that would require a new style slot + // This should succeed by increasing capacity (or splitting if at max capacity) + try s.setAttribute(.bold); - // The cursor style should have been cleared by the `adjustCapacity`. - // - // This behavior will change, causing this test to fail, if either - // of these changes are made: - // - // - The adjustCapacity function is changed to ensure the - // new capacity will definitely fit the cursor style. - // - // - The cursor managed memory handling is reworked so that it - // doesn't reside in the pages anymore and doesn't need this - // accounting. - // - // In such a case, adjust this test accordingly. - try testing.expect(s.cursor.style.default()); - try testing.expectEqual(style.default_id, s.cursor.style_id); + // The style should have been applied (bold flag set) + try testing.expect(s.cursor.style.flags.bold); + + // The cursor should have a valid non-default style_id + try testing.expect(s.cursor.style_id != style.default_id); + + // Either the capacity increased or the page was split/changed + const current_page = &s.cursor.page_pin.node.data; + const capacity_increased = current_page.capacity.styles > original_styles_capacity; + const page_changed = current_page != page; + try testing.expect(capacity_increased or page_changed); +} + +test "Screen setAttribute splits page on OutOfSpace at max styles" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, .{ + .cols = 10, + .rows = 10, + .max_scrollback = 0, + }); + defer s.deinit(); + + // Write content to multiple rows so we have something to split + try s.testWriteString("line1\nline2\nline3\nline4\nline5"); + + // Remember the original node + const original_node = s.cursor.page_pin.node; + + // Increase the page's style capacity to max by repeatedly calling increaseCapacity + // Use Screen.increaseCapacity to properly maintain cursor state + const max_styles = std.math.maxInt(size.CellCountInt); + while (s.cursor.page_pin.node.data.capacity.styles < max_styles) { + _ = s.increaseCapacity( + s.cursor.page_pin.node, + .styles, + ) catch break; + } + + // Get the page reference after increaseCapacity - cursor may have moved + var page = &s.cursor.page_pin.node.data; + try testing.expectEqual(max_styles, page.capacity.styles); + + // Fill the style map to capacity using the StyleSet's layout capacity + // which accounts for the load factor + { + page.pauseIntegrityChecks(true); + defer page.pauseIntegrityChecks(false); + defer page.assertIntegrity(); + + const max_items = page.styles.layout.cap; + var n: usize = 1; + while (n < max_items) : (n += 1) { + _ = page.styles.add( + page.memory, + .{ .bg_color = .{ .rgb = @bitCast(@as(u24, @intCast(n))) } }, + ) catch break; + } + } + + // Track the node before setAttribute + const node_before_set = s.cursor.page_pin.node; + + // Now try to set a new unique attribute that would require a new style slot + // At max capacity, increaseCapacity will return OutOfSpace, triggering page split + try s.setAttribute(.bold); + + // The style should have been applied (bold flag set) + try testing.expect(s.cursor.style.flags.bold); + + // The cursor should have a valid non-default style_id + try testing.expect(s.cursor.style_id != style.default_id); + + // The page should have been split + const page_was_split = s.cursor.page_pin.node != node_before_set or + node_before_set.next != null or + node_before_set.prev != null or + s.cursor.page_pin.node != original_node; + try testing.expect(page_was_split); +} + +test "selectionString map allocation failure cleanup" { + // This test verifies that if toOwnedSlice fails when building + // the StringMap, we don't leak the already-allocated map.string. + const testing = std.testing; + const alloc = testing.allocator; + var s = try Screen.init(alloc, .{ .cols = 10, .rows = 5, .max_scrollback = 0 }); + defer s.deinit(); + + try s.testWriteString("hello"); + + // Get a selection + const sel = Selection.init( + s.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, + s.pages.pin(.{ .active = .{ .x = 4, .y = 0 } }).?, + false, + ); + + // Trigger allocation failure on toOwnedSlice + var map: StringMap = undefined; + selectionString_tw.errorAlways(.copy_map, error.OutOfMemory); + const result = s.selectionString(alloc, .{ + .sel = sel, + .map = &map, + }); + try testing.expectError(error.OutOfMemory, result); + try selectionString_tw.end(.reset); + + // If this test passes without memory leaks (when run with testing.allocator), + // it means the errdefer properly cleaned up map.string when toOwnedSlice failed. } diff --git a/src/terminal/ScreenSet.zig b/src/terminal/ScreenSet.zig index 418888694..cbaa03f47 100644 --- a/src/terminal/ScreenSet.zig +++ b/src/terminal/ScreenSet.zig @@ -32,7 +32,7 @@ all: std.EnumMap(Key, *Screen), pub fn init( alloc: Allocator, opts: Screen.Options, -) !ScreenSet { +) Allocator.Error!ScreenSet { // We need to initialize our initial primary screen const screen = try alloc.create(Screen); errdefer alloc.destroy(screen); @@ -64,7 +64,7 @@ pub fn getInit( alloc: Allocator, key: Key, opts: Screen.Options, -) !*Screen { +) Allocator.Error!*Screen { if (self.get(key)) |screen| return screen; const screen = try alloc.create(Screen); errdefer alloc.destroy(screen); diff --git a/src/terminal/Selection.zig b/src/terminal/Selection.zig index bc597fc2e..8cb52816c 100644 --- a/src/terminal/Selection.zig +++ b/src/terminal/Selection.zig @@ -3,6 +3,7 @@ const Selection = @This(); const std = @import("std"); const assert = @import("../quirks.zig").inlineAssert; +const Allocator = std.mem.Allocator; const page = @import("page.zig"); const point = @import("point.zig"); const PageList = @import("PageList.zig"); @@ -126,7 +127,7 @@ pub fn tracked(self: *const Selection) bool { /// Convert this selection a tracked selection. It is asserted this is /// an untracked selection. The tracked selection is returned. -pub fn track(self: *const Selection, s: *Screen) !Selection { +pub fn track(self: *const Selection, s: *Screen) Allocator.Error!Selection { assert(!self.tracked()); // Track our pins diff --git a/src/terminal/StringMap.zig b/src/terminal/StringMap.zig index 4ac47eeab..f7d88d1c8 100644 --- a/src/terminal/StringMap.zig +++ b/src/terminal/StringMap.zig @@ -147,3 +147,131 @@ test "StringMap searchIterator" { try testing.expect(try it.next() == null); } + +test "StringMap searchIterator URL detection" { + if (comptime !build_options.oniguruma) return error.SkipZigTest; + + const testing = std.testing; + const alloc = testing.allocator; + const url = @import("../config/url.zig"); + + // Initialize URL regex + try oni.testing.ensureInit(); + var re = try oni.Regex.init( + url.regex, + .{}, + oni.Encoding.utf8, + oni.Syntax.default, + null, + ); + defer re.deinit(); + + // Initialize our screen with text containing a URL + var s = try Screen.init(alloc, .{ .cols = 40, .rows = 5, .max_scrollback = 0 }); + defer s.deinit(); + try s.testWriteString("hello https://example.com/path world"); + + // Get the line + const line = s.selectLine(.{ + .pin = s.pages.pin(.{ .active = .{ + .x = 10, + .y = 0, + } }).?, + }).?; + var map: StringMap = undefined; + const sel_str = try s.selectionString(alloc, .{ + .sel = line, + .trim = false, + .map = &map, + }); + alloc.free(sel_str); + defer map.deinit(alloc); + + // Search for URL match + var it = map.searchIterator(re); + { + var match = (try it.next()).?; + defer match.deinit(); + + const sel = match.selection(); + // URL should start at x=6 ("https://example.com/path" starts after "hello ") + try testing.expectEqual(point.Point{ .screen = .{ + .x = 6, + .y = 0, + } }, s.pages.pointFromPin(.screen, sel.start()).?); + // URL should end at x=29 (end of "/path") + try testing.expectEqual(point.Point{ .screen = .{ + .x = 29, + .y = 0, + } }, s.pages.pointFromPin(.screen, sel.end()).?); + } + + try testing.expect(try it.next() == null); +} + +test "StringMap searchIterator URL with click position" { + if (comptime !build_options.oniguruma) return error.SkipZigTest; + + const testing = std.testing; + const alloc = testing.allocator; + const url = @import("../config/url.zig"); + + // Initialize URL regex + try oni.testing.ensureInit(); + var re = try oni.Regex.init( + url.regex, + .{}, + oni.Encoding.utf8, + oni.Syntax.default, + null, + ); + defer re.deinit(); + + // Initialize our screen with text containing a URL + var s = try Screen.init(alloc, .{ .cols = 40, .rows = 5, .max_scrollback = 0 }); + defer s.deinit(); + try s.testWriteString("hello https://example.com world"); + + // Simulate clicking on "example" (x=14) + const click_pin = s.pages.pin(.{ .active = .{ + .x = 14, + .y = 0, + } }).?; + + // Get the line + const line = s.selectLine(.{ + .pin = click_pin, + }).?; + var map: StringMap = undefined; + const sel_str = try s.selectionString(alloc, .{ + .sel = line, + .trim = false, + .map = &map, + }); + alloc.free(sel_str); + defer map.deinit(alloc); + + // Search for URL match and verify click position is within URL + var it = map.searchIterator(re); + var found_url = false; + while (true) { + var match = (try it.next()) orelse break; + defer match.deinit(); + + const sel = match.selection(); + if (sel.contains(&s, click_pin)) { + found_url = true; + // Verify URL bounds + try testing.expectEqual(point.Point{ .screen = .{ + .x = 6, + .y = 0, + } }, s.pages.pointFromPin(.screen, sel.start()).?); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 24, + .y = 0, + } }, s.pages.pointFromPin(.screen, sel.end()).?); + break; + } + } + try testing.expect(found_url); +} diff --git a/src/terminal/Tabstops.zig b/src/terminal/Tabstops.zig index 13d6dc52e..68138cbf8 100644 --- a/src/terminal/Tabstops.zig +++ b/src/terminal/Tabstops.zig @@ -10,6 +10,7 @@ const Tabstops = @This(); const std = @import("std"); +const tripwire = @import("../tripwire.zig"); const Allocator = std.mem.Allocator; const testing = std.testing; const assert = @import("../quirks.zig").inlineAssert; @@ -58,7 +59,11 @@ inline fn index(col: usize) usize { return @mod(col, unit_bits); } -pub fn init(alloc: Allocator, cols: usize, interval: usize) !Tabstops { +pub fn init( + alloc: Allocator, + cols: usize, + interval: usize, +) Allocator.Error!Tabstops { var res: Tabstops = .{}; try res.resize(alloc, cols); res.reset(interval); @@ -114,21 +119,36 @@ pub fn get(self: Tabstops, col: usize) bool { return unit & mask == mask; } +const resize_tw = tripwire.module(enum { + dynamic_alloc, +}, resize); + /// Resize this to support up to cols columns. // TODO: needs interval to set new tabstops -pub fn resize(self: *Tabstops, alloc: Allocator, cols: usize) !void { - // Set our new value - self.cols = cols; +pub fn resize( + self: *Tabstops, + alloc: Allocator, + cols: usize, +) Allocator.Error!void { + const tw = resize_tw; // Do nothing if it fits. - if (cols <= prealloc_columns) return; + if (cols <= prealloc_columns) { + self.cols = cols; + return; + } // What we need in the dynamic size const size = cols - prealloc_columns; - if (size < self.dynamic_stops.len) return; + if (size < self.dynamic_stops.len) { + self.cols = cols; + return; + } // Note: we can probably try to realloc here but I'm not sure it matters. + try tw.check(.dynamic_alloc); const new = try alloc.alloc(Unit, size); + errdefer comptime unreachable; @memset(new, 0); if (self.dynamic_stops.len > 0) { fastmem.copy(Unit, new, self.dynamic_stops); @@ -136,6 +156,7 @@ pub fn resize(self: *Tabstops, alloc: Allocator, cols: usize) !void { } self.dynamic_stops = new; + self.cols = cols; } /// Return the maximum number of columns this can support currently. @@ -230,3 +251,21 @@ test "Tabstops: count on 80" { try testing.expectEqual(@as(usize, 9), count); } + +test "Tabstops: resize alloc failure preserves state" { + // This test verifies that if resize() fails during allocation, + // the original cols value is preserved (not corrupted). + var t: Tabstops = try init(testing.allocator, 80, 8); + defer t.deinit(testing.allocator); + + const original_cols = t.cols; + + // Trigger allocation failure when resizing beyond prealloc + resize_tw.errorAlways(.dynamic_alloc, error.OutOfMemory); + const result = t.resize(testing.allocator, prealloc_columns * 2); + try testing.expectError(error.OutOfMemory, result); + try resize_tw.end(.reset); + + // cols should be unchanged after failed resize + try testing.expectEqual(original_cols, t.cols); +} diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index f2e83a3f8..c3a82a4bf 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -737,7 +737,7 @@ fn printCell( // TODO: this case was not handled in the old terminal implementation // but it feels like we should do something. investigate other - // terminals (xterm mainly) and see whats up. + // terminals (xterm mainly) and see what's up. .spacer_head => {}, } } @@ -1058,7 +1058,7 @@ pub fn saveCursor(self: *Terminal) void { /// /// The primary and alternate screen have distinct save state. /// If no save was done before values are reset to their initial values. -pub fn restoreCursor(self: *Terminal) !void { +pub fn restoreCursor(self: *Terminal) void { const saved: Screen.SavedCursor = self.screens.active.saved_cursor orelse .{ .x = 0, .y = 0, @@ -1070,10 +1070,17 @@ pub fn restoreCursor(self: *Terminal) !void { }; // Set the style first because it can fail - const old_style = self.screens.active.cursor.style; self.screens.active.cursor.style = saved.style; - errdefer self.screens.active.cursor.style = old_style; - try self.screens.active.manualStyleUpdate(); + self.screens.active.manualStyleUpdate() catch |err| { + // Regardless of the error here, we revert back to an unstyled + // cursor. It is more important that the restore succeeds in + // other attributes because terminals have no way to communicate + // failure back. + log.warn("restoreCursor error updating style err={}", .{err}); + const screen: *Screen = self.screens.active; + screen.cursor.style = .{}; + self.screens.active.manualStyleUpdate() catch unreachable; + }; self.screens.active.charset = saved.charset; self.modes.set(.origin, saved.origin); @@ -1172,7 +1179,7 @@ pub fn cursorIsAtPrompt(self: *Terminal) bool { /// Horizontal tab moves the cursor to the next tabstop, clearing /// the screen to the left the tabstop. -pub fn horizontalTab(self: *Terminal) !void { +pub fn horizontalTab(self: *Terminal) void { while (self.screens.active.cursor.x < self.scrolling_region.right) { // Move the cursor right self.screens.active.cursorRight(1); @@ -1185,7 +1192,7 @@ pub fn horizontalTab(self: *Terminal) !void { } // Same as horizontalTab but moves to the previous tabstop instead of the next. -pub fn horizontalTabBack(self: *Terminal) !void { +pub fn horizontalTabBack(self: *Terminal) void { // With origin mode enabled, our leftmost limit is the left margin. const left_limit = if (self.modes.get(.origin)) self.scrolling_region.left else 0; @@ -1281,7 +1288,7 @@ pub fn index(self: *Terminal) !void { // this check. !self.screens.active.blankCell().isZero()) { - self.scrollUp(1); + try self.scrollUp(1); return; } @@ -1460,7 +1467,7 @@ pub fn scrollDown(self: *Terminal, count: usize) void { /// The new lines are created according to the current SGR state. /// /// Does not change the (absolute) cursor position. -pub fn scrollUp(self: *Terminal, count: usize) void { +pub fn scrollUp(self: *Terminal, count: usize) !void { // Preserve our x/y to restore. const old_x = self.screens.active.cursor.x; const old_y = self.screens.active.cursor.y; @@ -1470,6 +1477,32 @@ pub fn scrollUp(self: *Terminal, count: usize) void { self.screens.active.cursor.pending_wrap = old_wrap; } + // If our scroll region is at the top and we have no left/right + // margins then we move the scrolled out text into the scrollback. + if (self.scrolling_region.top == 0 and + self.scrolling_region.left == 0 and + self.scrolling_region.right == self.cols - 1) + { + // Scrolling dirties the images because it updates their placements pins. + if (comptime build_options.kitty_graphics) { + self.screens.active.kitty_images.dirty = true; + } + + // Clamp count to the scroll region height. + const region_height = self.scrolling_region.bottom + 1; + const adjusted_count = @min(count, region_height); + + // TODO: Create an optimized version that can scroll N times + // This isn't critical because in most cases, scrollUp is used + // with count=1, but it's still a big optimization opportunity. + + // Move our cursor to the bottom of the scroll region so we can + // use the cursorScrollAbove function to create scrollback + self.screens.active.cursorAbsolute(0, self.scrolling_region.bottom); + for (0..adjusted_count) |_| try self.screens.active.cursorScrollAbove(); + return; + } + // Move to the top of the scroll region self.screens.active.cursorAbsolute(self.scrolling_region.left, self.scrolling_region.top); self.deleteLines(count); @@ -1638,9 +1671,6 @@ pub fn insertLines(self: *Terminal, count: usize) void { const cur_rac = cur_p.rowAndCell(); const cur_row: *Row = cur_rac.row; - // Mark the row as dirty - cur_p.markDirty(); - // If this is one of the lines we need to shift, do so if (y > adjusted_count) { const off_p = cur_p.up(adjusted_count).?; @@ -1673,54 +1703,48 @@ pub fn insertLines(self: *Terminal, count: usize) void { self.scrolling_region.left, self.scrolling_region.right + 1, ) catch |err| { - const cap = dst_p.node.data.capacity; // Adjust our page capacity to make // room for we didn't have space for - _ = self.screens.active.adjustCapacity( + _ = self.screens.active.increaseCapacity( dst_p.node, switch (err) { // Rehash the sets error.StyleSetNeedsRehash, error.HyperlinkSetNeedsRehash, - => .{}, + => null, // Increase style memory error.StyleSetOutOfMemory, - => .{ .styles = cap.styles * 2 }, + => .styles, // Increase string memory error.StringAllocOutOfMemory, - => .{ .string_bytes = cap.string_bytes * 2 }, + => .string_bytes, // Increase hyperlink memory error.HyperlinkSetOutOfMemory, error.HyperlinkMapOutOfMemory, - => .{ .hyperlink_bytes = cap.hyperlink_bytes * 2 }, + => .hyperlink_bytes, // Increase grapheme memory error.GraphemeMapOutOfMemory, error.GraphemeAllocOutOfMemory, - => .{ .grapheme_bytes = cap.grapheme_bytes * 2 }, + => .grapheme_bytes, }, ) catch |e| switch (e) { - // This shouldn't be possible because above we're only - // adjusting capacity _upwards_. So it should have all - // the existing capacity it had to fit the adjusted - // data. Panic since we don't expect this. - error.StyleSetOutOfMemory, - error.StyleSetNeedsRehash, - error.StringAllocOutOfMemory, - error.HyperlinkSetOutOfMemory, - error.HyperlinkSetNeedsRehash, - error.HyperlinkMapOutOfMemory, - error.GraphemeMapOutOfMemory, - error.GraphemeAllocOutOfMemory, - => @panic("adjustCapacity resulted in capacity errors"), - - // The system allocator is OOM. We can't currently do - // anything graceful here. We panic. + // System OOM. We have no way to recover from this + // currently. We should probably change insertLines + // to raise an error here. error.OutOfMemory, - => @panic("adjustCapacity system allocator OOM"), + => @panic("increaseCapacity system allocator OOM"), + + // The page can't accommodate the managed memory required + // for this operation. We previously just corrupted + // memory here so a crash is better. The right long + // term solution is to allocate a new page here + // move this row to the new page, and start over. + error.OutOfSpace, + => @panic("increaseCapacity OutOfSpace"), }; // Continue the loop to try handling this row again. @@ -1735,9 +1759,6 @@ pub fn insertLines(self: *Terminal, count: usize) void { dst_row.* = src_row.*; src_row.* = dst; - // Make sure the row is marked as dirty though. - dst_row.dirty = true; - // Ensure what we did didn't corrupt the page cur_p.node.data.assertIntegrity(); } else { @@ -1764,6 +1785,9 @@ pub fn insertLines(self: *Terminal, count: usize) void { ); } + // Mark the row as dirty + cur_p.markDirty(); + // We have successfully processed a line. y -= 1; // Move our pin up to the next row. @@ -1841,9 +1865,6 @@ pub fn deleteLines(self: *Terminal, count: usize) void { const cur_rac = cur_p.rowAndCell(); const cur_row: *Row = cur_rac.row; - // Mark the row as dirty - cur_p.markDirty(); - // If this is one of the lines we need to shift, do so if (y < rem - adjusted_count) { const off_p = cur_p.down(adjusted_count).?; @@ -1876,49 +1897,41 @@ pub fn deleteLines(self: *Terminal, count: usize) void { self.scrolling_region.left, self.scrolling_region.right + 1, ) catch |err| { - const cap = dst_p.node.data.capacity; // Adjust our page capacity to make // room for we didn't have space for - _ = self.screens.active.adjustCapacity( + _ = self.screens.active.increaseCapacity( dst_p.node, switch (err) { // Rehash the sets error.StyleSetNeedsRehash, error.HyperlinkSetNeedsRehash, - => .{}, + => null, // Increase style memory error.StyleSetOutOfMemory, - => .{ .styles = cap.styles * 2 }, + => .styles, // Increase string memory error.StringAllocOutOfMemory, - => .{ .string_bytes = cap.string_bytes * 2 }, + => .string_bytes, // Increase hyperlink memory error.HyperlinkSetOutOfMemory, error.HyperlinkMapOutOfMemory, - => .{ .hyperlink_bytes = cap.hyperlink_bytes * 2 }, + => .hyperlink_bytes, // Increase grapheme memory error.GraphemeMapOutOfMemory, error.GraphemeAllocOutOfMemory, - => .{ .grapheme_bytes = cap.grapheme_bytes * 2 }, + => .grapheme_bytes, }, ) catch |e| switch (e) { - // See insertLines which has the same error capture. - error.StyleSetOutOfMemory, - error.StyleSetNeedsRehash, - error.StringAllocOutOfMemory, - error.HyperlinkSetOutOfMemory, - error.HyperlinkSetNeedsRehash, - error.HyperlinkMapOutOfMemory, - error.GraphemeMapOutOfMemory, - error.GraphemeAllocOutOfMemory, - => @panic("adjustCapacity resulted in capacity errors"), - + // See insertLines error.OutOfMemory, - => @panic("adjustCapacity system allocator OOM"), + => @panic("increaseCapacity system allocator OOM"), + + error.OutOfSpace, + => @panic("increaseCapacity OutOfSpace"), }; // Continue the loop to try handling this row again. @@ -1933,9 +1946,6 @@ pub fn deleteLines(self: *Terminal, count: usize) void { dst_row.* = src_row.*; src_row.* = dst; - // Make sure the row is marked as dirty though. - dst_row.dirty = true; - // Ensure what we did didn't corrupt the page cur_p.node.data.assertIntegrity(); } else { @@ -1962,6 +1972,9 @@ pub fn deleteLines(self: *Terminal, count: usize) void { ); } + // Mark the row as dirty + cur_p.markDirty(); + // We have successfully processed a line. y += 1; // Move our pin down to the next row. @@ -2803,12 +2816,7 @@ pub fn switchScreenMode( } } else { assert(self.screens.active_key == .primary); - self.restoreCursor() catch |err| { - log.warn( - "restore cursor on switch screen failed to={} err={}", - .{ to, err }, - ); - }; + self.restoreCursor(); }, } } @@ -5042,17 +5050,17 @@ test "Terminal: horizontal tabs" { // HT try t.print('1'); - try t.horizontalTab(); + t.horizontalTab(); try testing.expectEqual(@as(usize, 8), t.screens.active.cursor.x); // HT - try t.horizontalTab(); + t.horizontalTab(); try testing.expectEqual(@as(usize, 16), t.screens.active.cursor.x); // HT at the end - try t.horizontalTab(); + t.horizontalTab(); try testing.expectEqual(@as(usize, 19), t.screens.active.cursor.x); - try t.horizontalTab(); + t.horizontalTab(); try testing.expectEqual(@as(usize, 19), t.screens.active.cursor.x); } @@ -5064,7 +5072,7 @@ test "Terminal: horizontal tabs starting on tabstop" { t.setCursorPos(t.screens.active.cursor.y, 9); try t.print('X'); t.setCursorPos(t.screens.active.cursor.y, 9); - try t.horizontalTab(); + t.horizontalTab(); try t.print('A'); { @@ -5083,7 +5091,7 @@ test "Terminal: horizontal tabs with right margin" { t.scrolling_region.right = 5; t.setCursorPos(t.screens.active.cursor.y, 1); try t.print('X'); - try t.horizontalTab(); + t.horizontalTab(); try t.print('A'); { @@ -5102,17 +5110,17 @@ test "Terminal: horizontal tabs back" { t.setCursorPos(t.screens.active.cursor.y, 20); // HT - try t.horizontalTabBack(); + t.horizontalTabBack(); try testing.expectEqual(@as(usize, 16), t.screens.active.cursor.x); // HT - try t.horizontalTabBack(); + t.horizontalTabBack(); try testing.expectEqual(@as(usize, 8), t.screens.active.cursor.x); // HT - try t.horizontalTabBack(); + t.horizontalTabBack(); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); - try t.horizontalTabBack(); + t.horizontalTabBack(); try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x); } @@ -5124,7 +5132,7 @@ test "Terminal: horizontal tabs back starting on tabstop" { t.setCursorPos(t.screens.active.cursor.y, 9); try t.print('X'); t.setCursorPos(t.screens.active.cursor.y, 9); - try t.horizontalTabBack(); + t.horizontalTabBack(); try t.print('A'); { @@ -5144,7 +5152,7 @@ test "Terminal: horizontal tabs with left margin in origin mode" { t.scrolling_region.right = 5; t.setCursorPos(1, 2); try t.print('X'); - try t.horizontalTabBack(); + t.horizontalTabBack(); try t.print('A'); { @@ -5163,8 +5171,8 @@ test "Terminal: horizontal tab back with cursor before left margin" { t.saveCursor(); t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(5, 0); - try t.restoreCursor(); - try t.horizontalTabBack(); + t.restoreCursor(); + t.horizontalTabBack(); try t.print('X'); { @@ -5755,6 +5763,52 @@ test "Terminal: insertLines top/bottom scroll region" { } } +test "Terminal: insertLines across page boundary marks all shifted rows dirty" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .rows = 5, .cols = 10, .max_scrollback = 1024 }); + defer t.deinit(alloc); + + const first_page = t.screens.active.pages.pages.first.?; + const first_page_nrows = first_page.data.capacity.rows; + + // Fill up the first page minus 3 rows + for (0..first_page_nrows - 3) |_| try t.linefeed(); + + // Add content that will cross a page boundary + try t.printString("1AAAA"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("2BBBB"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("3CCCC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("4DDDD"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("5EEEE"); + + // Verify we now have a second page + try testing.expect(first_page.next != null); + + t.setCursorPos(1, 1); + t.clearDirty(); + t.insertLines(1); + + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } })); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\n1AAAA\n2BBBB\n3CCCC\n4DDDD", str); + } +} + test "Terminal: insertLines (legacy test)" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 5 }); @@ -5997,14 +6051,16 @@ test "Terminal: scrollUp simple" { t.setCursorPos(2, 2); const cursor = t.screens.active.cursor; - t.clearDirty(); - t.scrollUp(1); + const viewport_before = t.screens.active.pages.getTopLeft(.viewport); + try t.scrollUp(1); try testing.expectEqual(cursor.x, t.screens.active.cursor.x); try testing.expectEqual(cursor.y, t.screens.active.cursor.y); - try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); - try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); - try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + // Viewport should have moved. Our entire page should've scrolled! + // The viewport moving will cause our render state to make the full + // frame as dirty. + const viewport_after = t.screens.active.pages.getTopLeft(.viewport); + try testing.expect(!viewport_before.eql(viewport_after)); { const str = try t.plainString(testing.allocator); @@ -6028,7 +6084,7 @@ test "Terminal: scrollUp moves hyperlink" { try t.linefeed(); try t.printString("GHI"); t.setCursorPos(2, 2); - t.scrollUp(1); + try t.scrollUp(1); { const str = try t.plainString(testing.allocator); @@ -6079,7 +6135,7 @@ test "Terminal: scrollUp clears hyperlink" { try t.linefeed(); try t.printString("GHI"); t.setCursorPos(2, 2); - t.scrollUp(1); + try t.scrollUp(1); { const str = try t.plainString(testing.allocator); @@ -6117,7 +6173,7 @@ test "Terminal: scrollUp top/bottom scroll region" { t.setCursorPos(1, 1); t.clearDirty(); - t.scrollUp(1); + try t.scrollUp(1); // This is dirty because the cursor moves from this row try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); @@ -6149,7 +6205,7 @@ test "Terminal: scrollUp left/right scroll region" { const cursor = t.screens.active.cursor; t.clearDirty(); - t.scrollUp(1); + try t.scrollUp(1); try testing.expectEqual(cursor.x, t.screens.active.cursor.x); try testing.expectEqual(cursor.y, t.screens.active.cursor.y); @@ -6181,7 +6237,7 @@ test "Terminal: scrollUp left/right scroll region hyperlink" { t.scrolling_region.left = 1; t.scrolling_region.right = 3; t.setCursorPos(2, 2); - t.scrollUp(1); + try t.scrollUp(1); { const str = try t.plainString(testing.allocator); @@ -6281,7 +6337,7 @@ test "Terminal: scrollUp preserves pending wrap" { try t.print('B'); t.setCursorPos(3, 5); try t.print('C'); - t.scrollUp(1); + try t.scrollUp(1); try t.print('X'); { @@ -6302,7 +6358,7 @@ test "Terminal: scrollUp full top/bottom region" { t.setTopAndBottomMargin(2, 5); t.clearDirty(); - t.scrollUp(4); + try t.scrollUp(4); // This is dirty because the cursor moves from this row try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); @@ -6328,7 +6384,7 @@ test "Terminal: scrollUp full top/bottomleft/right scroll region" { t.setLeftAndRightMargin(2, 4); t.clearDirty(); - t.scrollUp(4); + try t.scrollUp(4); // This is dirty because the cursor moves from this row try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); @@ -6344,6 +6400,143 @@ test "Terminal: scrollUp full top/bottomleft/right scroll region" { } } +test "Terminal: scrollUp creates scrollback in primary screen" { + // When in primary screen with full-width scroll region at top, + // scrollUp (CSI S) should push lines into scrollback like xterm. + const alloc = testing.allocator; + var t = try init(alloc, .{ .rows = 5, .cols = 5, .max_scrollback = 10 }); + defer t.deinit(alloc); + + // Fill the screen with content + try t.printString("AAAAA"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("BBBBB"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("CCCCC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DDDDD"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("EEEEE"); + + t.clearDirty(); + + // Scroll up by 1, which should push "AAAAA" into scrollback + try t.scrollUp(1); + + // The cursor row (new empty row) should be dirty + try testing.expect(t.screens.active.cursor.page_row.dirty); + + // The active screen should now show BBBBB through EEEEE plus one blank line + { + const str = try t.plainString(alloc); + defer alloc.free(str); + try testing.expectEqualStrings("BBBBB\nCCCCC\nDDDDD\nEEEEE", str); + } + + // Now scroll to the top to see scrollback - AAAAA should be there + t.screens.active.scroll(.{ .top = {} }); + { + const str = try t.plainString(alloc); + defer alloc.free(str); + // Should see AAAAA in scrollback + try testing.expectEqualStrings("AAAAA\nBBBBB\nCCCCC\nDDDDD\nEEEEE", str); + } +} + +test "Terminal: scrollUp with max_scrollback zero" { + // When max_scrollback is 0, scrollUp should still work but not retain history + const alloc = testing.allocator; + var t = try init(alloc, .{ .rows = 5, .cols = 5, .max_scrollback = 0 }); + defer t.deinit(alloc); + + try t.printString("AAAAA"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("BBBBB"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("CCCCC"); + + try t.scrollUp(1); + + // Active screen should show scrolled content + { + const str = try t.plainString(alloc); + defer alloc.free(str); + try testing.expectEqualStrings("BBBBB\nCCCCC", str); + } + + // Scroll to top - should be same as active since no scrollback + t.screens.active.scroll(.{ .top = {} }); + { + const str = try t.plainString(alloc); + defer alloc.free(str); + try testing.expectEqualStrings("BBBBB\nCCCCC", str); + } +} + +test "Terminal: scrollUp with max_scrollback zero and top margin" { + // When max_scrollback is 0 and top margin is set, should use deleteLines path + const alloc = testing.allocator; + var t = try init(alloc, .{ .rows = 5, .cols = 5, .max_scrollback = 0 }); + defer t.deinit(alloc); + + try t.printString("AAAAA"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("BBBBB"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("CCCCC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DDDDD"); + + // Set top margin (not at row 0) + t.setTopAndBottomMargin(2, 5); + + try t.scrollUp(1); + + { + const str = try t.plainString(alloc); + defer alloc.free(str); + // First row preserved, rest scrolled + try testing.expectEqualStrings("AAAAA\nCCCCC\nDDDDD", str); + } +} + +test "Terminal: scrollUp with max_scrollback zero and left/right margin" { + // When max_scrollback is 0 with left/right margins, uses deleteLines path + const alloc = testing.allocator; + var t = try init(alloc, .{ .rows = 5, .cols = 10, .max_scrollback = 0 }); + defer t.deinit(alloc); + + try t.printString("AAAAABBBBB"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("CCCCCDDDDD"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("EEEEEFFFFF"); + + // Set left/right margins (columns 2-6, 1-indexed = indices 1-5) + t.modes.set(.enable_left_and_right_margin, true); + t.setLeftAndRightMargin(2, 6); + + try t.scrollUp(1); + + { + const str = try t.plainString(alloc); + defer alloc.free(str); + // cols 1-5 scroll, col 0 and cols 6+ preserved + try testing.expectEqualStrings("ACCCCDBBBB\nCEEEEFDDDD\nE FFFF", str); + } +} + test "Terminal: scrollDown simple" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); @@ -8239,6 +8432,52 @@ test "Terminal: deleteLines colors with bg color" { } } +test "Terminal: deleteLines across page boundary marks all shifted rows dirty" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .rows = 5, .cols = 10, .max_scrollback = 1024 }); + defer t.deinit(alloc); + + const first_page = t.screens.active.pages.pages.first.?; + const first_page_nrows = first_page.data.capacity.rows; + + // Fill up the first page minus 3 rows + for (0..first_page_nrows - 3) |_| try t.linefeed(); + + // Add content that will cross a page boundary + try t.printString("1AAAA"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("2BBBB"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("3CCCC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("4DDDD"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("5EEEE"); + + // Verify we now have a second page + try testing.expect(first_page.next != null); + + t.setCursorPos(1, 1); + t.clearDirty(); + t.deleteLines(1); + + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } })); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("2BBBB\n3CCCC\n4DDDD\n5EEEE", str); + } +} + test "Terminal: deleteLines (legacy)" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 80, .rows = 80 }); @@ -9255,7 +9494,7 @@ test "Terminal: insertBlanks shift graphemes" { var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc); - // Disable grapheme clustering + // Enable grapheme clustering t.modes.set(.grapheme_cluster, true); try t.printString("A"); @@ -9998,7 +10237,7 @@ test "Terminal: saveCursor" { t.screens.active.charset.gr = .G0; try t.setAttribute(.{ .unset = {} }); t.modes.set(.origin, false); - try t.restoreCursor(); + t.restoreCursor(); try testing.expect(t.screens.active.cursor.style.flags.bold); try testing.expect(t.screens.active.charset.gr == .G3); try testing.expect(t.modes.get(.origin)); @@ -10014,7 +10253,7 @@ test "Terminal: saveCursor position" { t.saveCursor(); t.setCursorPos(1, 1); try t.print('B'); - try t.restoreCursor(); + t.restoreCursor(); try t.print('X'); { @@ -10034,7 +10273,7 @@ test "Terminal: saveCursor pending wrap state" { t.saveCursor(); t.setCursorPos(1, 1); try t.print('B'); - try t.restoreCursor(); + t.restoreCursor(); try t.print('X'); { @@ -10054,7 +10293,7 @@ test "Terminal: saveCursor origin mode" { t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(3, 5); t.setTopAndBottomMargin(2, 4); - try t.restoreCursor(); + t.restoreCursor(); try t.print('X'); { @@ -10072,7 +10311,7 @@ test "Terminal: saveCursor resize" { t.setCursorPos(1, 10); t.saveCursor(); try t.resize(alloc, 5, 5); - try t.restoreCursor(); + t.restoreCursor(); try t.print('X'); { @@ -10093,7 +10332,7 @@ test "Terminal: saveCursor protected pen" { t.saveCursor(); t.setProtectedMode(.off); try testing.expect(!t.screens.active.cursor.protected); - try t.restoreCursor(); + t.restoreCursor(); try testing.expect(t.screens.active.cursor.protected); } @@ -10106,10 +10345,67 @@ test "Terminal: saveCursor doesn't modify hyperlink state" { const id = t.screens.active.cursor.hyperlink_id; t.saveCursor(); try testing.expectEqual(id, t.screens.active.cursor.hyperlink_id); - try t.restoreCursor(); + t.restoreCursor(); try testing.expectEqual(id, t.screens.active.cursor.hyperlink_id); } +test "Terminal: restoreCursor uses default style on OutOfSpace" { + // Tests that restoreCursor falls back to default style when + // manualStyleUpdate fails with OutOfSpace (can't split a 1-row page + // and styles are at max capacity). + const alloc = testing.allocator; + + // Use a single row so the page can't be split + var t = try init(alloc, .{ .cols = 10, .rows = 1 }); + defer t.deinit(alloc); + + // Set a style and save the cursor + try t.setAttribute(.{ .bold = {} }); + t.saveCursor(); + + // Clear the style + try t.setAttribute(.{ .unset = {} }); + try testing.expect(!t.screens.active.cursor.style.flags.bold); + + // Fill the style map to max capacity + const max_styles = std.math.maxInt(size.CellCountInt); + while (t.screens.active.cursor.page_pin.node.data.capacity.styles < max_styles) { + _ = t.screens.active.increaseCapacity( + t.screens.active.cursor.page_pin.node, + .styles, + ) catch break; + } + + const page = &t.screens.active.cursor.page_pin.node.data; + try testing.expectEqual(max_styles, page.capacity.styles); + + // Fill all style slots using the StyleSet's layout capacity which accounts + // for the load factor. The capacity in the layout is the actual max number + // of items that can be stored. + { + page.pauseIntegrityChecks(true); + defer page.pauseIntegrityChecks(false); + defer page.assertIntegrity(); + + const max_items = page.styles.layout.cap; + var n: usize = 1; + while (n < max_items) : (n += 1) { + _ = page.styles.add( + page.memory, + .{ .bg_color = .{ .rgb = @bitCast(@as(u24, @intCast(n))) } }, + ) catch break; + } + } + + // Restore cursor - should fall back to default style since page + // can't be split (1 row) and styles are at max capacity + t.restoreCursor(); + + // The style should be reset to default because OutOfSpace occurred + try testing.expect(!t.screens.active.cursor.style.flags.bold); + try testing.expectEqual(style.default_id, t.screens.active.cursor.style_id); +} + test "Terminal: setProtectedMode" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 3, .rows = 3 }); @@ -10611,11 +10907,11 @@ test "Terminal: tabClear single" { var t = try init(alloc, .{ .cols = 30, .rows = 5 }); defer t.deinit(alloc); - try t.horizontalTab(); + t.horizontalTab(); t.tabClear(.current); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); t.setCursorPos(1, 1); - try t.horizontalTab(); + t.horizontalTab(); try testing.expectEqual(@as(usize, 16), t.screens.active.cursor.x); } @@ -10627,7 +10923,7 @@ test "Terminal: tabClear all" { t.tabClear(.all); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); t.setCursorPos(1, 1); - try t.horizontalTab(); + t.horizontalTab(); try testing.expectEqual(@as(usize, 29), t.screens.active.cursor.x); } @@ -11501,7 +11797,7 @@ test "Terminal: resize with reflow and saved cursor" { t.saveCursor(); try t.resize(alloc, 5, 3); - try t.restoreCursor(); + t.restoreCursor(); { const str = try t.plainString(testing.allocator); @@ -11542,7 +11838,7 @@ test "Terminal: resize with reflow and saved cursor pending wrap" { t.saveCursor(); try t.resize(alloc, 5, 3); - try t.restoreCursor(); + t.restoreCursor(); { const str = try t.plainString(testing.allocator); diff --git a/src/terminal/bitmap_allocator.zig b/src/terminal/bitmap_allocator.zig index 258d73071..23a5048e1 100644 --- a/src/terminal/bitmap_allocator.zig +++ b/src/terminal/bitmap_allocator.zig @@ -63,6 +63,14 @@ pub fn BitmapAllocator(comptime chunk_size: comptime_int) type { }; } + /// Returns the number of bytes required to allocate n elements of + /// type T. This accounts for the chunk size alignment used by the + /// bitmap allocator. + pub fn bytesRequired(comptime T: type, n: usize) usize { + const byte_count = @sizeOf(T) * n; + return alignForward(usize, byte_count, chunk_size); + } + /// Allocate n elements of type T. This will return error.OutOfMemory /// if there isn't enough space in the backing buffer. /// @@ -955,3 +963,45 @@ test "BitmapAllocator alloc and free two 1.5 bitmaps offset 0.75" { bm.bitmap.ptr(buf)[0..4], ); } + +test "BitmapAllocator bytesRequired" { + const testing = std.testing; + + // Chunk size of 16 bytes (like grapheme_chunk in page.zig) + { + const Alloc = BitmapAllocator(16); + + // Single byte rounds up to chunk size + try testing.expectEqual(16, Alloc.bytesRequired(u8, 1)); + try testing.expectEqual(16, Alloc.bytesRequired(u8, 16)); + try testing.expectEqual(32, Alloc.bytesRequired(u8, 17)); + + // u21 (4 bytes each) + try testing.expectEqual(16, Alloc.bytesRequired(u21, 1)); // 4 bytes -> 16 + try testing.expectEqual(16, Alloc.bytesRequired(u21, 4)); // 16 bytes -> 16 + try testing.expectEqual(32, Alloc.bytesRequired(u21, 5)); // 20 bytes -> 32 + try testing.expectEqual(32, Alloc.bytesRequired(u21, 6)); // 24 bytes -> 32 + } + + // Chunk size of 4 bytes + { + const Alloc = BitmapAllocator(4); + + try testing.expectEqual(4, Alloc.bytesRequired(u8, 1)); + try testing.expectEqual(4, Alloc.bytesRequired(u8, 4)); + try testing.expectEqual(8, Alloc.bytesRequired(u8, 5)); + + // u32 (4 bytes each) - exactly one chunk per element + try testing.expectEqual(4, Alloc.bytesRequired(u32, 1)); + try testing.expectEqual(8, Alloc.bytesRequired(u32, 2)); + } + + // Chunk size of 32 bytes (like string_chunk in page.zig) + { + const Alloc = BitmapAllocator(32); + + try testing.expectEqual(32, Alloc.bytesRequired(u8, 1)); + try testing.expectEqual(32, Alloc.bytesRequired(u8, 32)); + try testing.expectEqual(64, Alloc.bytesRequired(u8, 33)); + } +} diff --git a/src/terminal/color.zig b/src/terminal/color.zig index 07c3e72f5..1e9e4b642 100644 --- a/src/terminal/color.zig +++ b/src/terminal/color.zig @@ -168,7 +168,7 @@ pub const Name = enum(u8) { } /// Default colors for tagged values. - pub fn default(self: Name) !RGB { + pub fn default(self: Name) error{NoDefaultValue}!RGB { return switch (self) { .black => RGB{ .r = 0x1D, .g = 0x1F, .b = 0x21 }, .red => RGB{ .r = 0xCC, .g = 0x66, .b = 0x66 }, @@ -355,7 +355,7 @@ pub const RGB = packed struct(u24) { /// Parse a color from a floating point intensity value. /// /// The value should be between 0.0 and 1.0, inclusive. - fn fromIntensity(value: []const u8) !u8 { + fn fromIntensity(value: []const u8) error{InvalidFormat}!u8 { const i = std.fmt.parseFloat(f64, value) catch { @branchHint(.cold); return error.InvalidFormat; @@ -372,7 +372,7 @@ pub const RGB = packed struct(u24) { /// /// The string can contain 1, 2, 3, or 4 characters and represents the color /// value scaled in 4, 8, 12, or 16 bits, respectively. - fn fromHex(value: []const u8) !u8 { + fn fromHex(value: []const u8) error{InvalidFormat}!u8 { if (value.len == 0 or value.len > 4) { @branchHint(.cold); return error.InvalidFormat; @@ -414,7 +414,7 @@ pub const RGB = packed struct(u24) { /// where `r`, `g`, and `b` are a single hexadecimal digit. /// These specify a color with 4, 8, 12, and 16 bits of precision /// per color channel. - pub fn parse(value: []const u8) !RGB { + pub fn parse(value: []const u8) error{InvalidFormat}!RGB { if (value.len == 0) { @branchHint(.cold); return error.InvalidFormat; diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index 74bbfe482..4249187a7 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -1042,6 +1042,13 @@ pub const PageFormatter = struct { } if (blank_rows > 0) { + // Reset style before emitting newlines to prevent background + // colors from bleeding into the next line's leading cells. + if (!style.default()) { + try self.formatStyleClose(writer); + style = .{}; + } + const sequence: []const u8 = switch (self.opts.emit) { // Plaintext just uses standard newlines because newlines // on their own usually move the cursor back in anywhere @@ -1114,13 +1121,26 @@ pub const PageFormatter = struct { // If we have a zero value, then we accumulate a counter. We // only want to turn zero values into spaces if we have a non-zero // char sometime later. - if (!cell.hasText()) { - blank_cells += 1; - continue; - } - if (cell.codepoint() == ' ' and self.opts.trim) { - blank_cells += 1; - continue; + blank: { + // If we're emitting styled output (not plaintext) and + // the cell has some kind of styling or is not empty + // then this isn't blank. + if (self.opts.emit.styled() and + (!cell.isEmpty() or cell.hasStyling())) break :blank; + + // Cells with no text are blank + if (!cell.hasText()) { + blank_cells += 1; + continue; + } + + // Trailing spaces are blank. We know it is trailing + // because if we get a non-empty cell later we'll + // fill the blanks. + if (cell.codepoint() == ' ' and self.opts.trim) { + blank_cells += 1; + continue; + } } // This cell is not blank. If we have accumulated blank cells @@ -1158,63 +1178,64 @@ pub const PageFormatter = struct { blank_cells = 0; } + style: { + // If we aren't emitting styled output then we don't + // have to worry about styles. + if (!self.opts.emit.styled()) break :style; + + // Get our cell style. + const cell_style = self.cellStyle(cell); + + // If the style hasn't changed, don't bloat output. + if (cell_style.eql(style)) break :style; + + // If we had a previous style, we need to close it, + // because we've confirmed we have some new style + // (which is maybe default). + if (!style.default()) switch (self.opts.emit) { + .html => try self.formatStyleClose(writer), + + // For VT, we only close if we're switching to a default + // style because any non-default style will emit + // a \x1b[0m as the start of a VT coloring sequence. + .vt => if (cell_style.default()) try self.formatStyleClose(writer), + + // Unreachable because of the styled() check at the + // top of this block. + .plain => unreachable, + }; + + // At this point, we can copy our style over + style = cell_style; + + // If we're just the default style now, we're done. + if (cell_style.default()) break :style; + + // New style, emit it. + try self.formatStyleOpen( + writer, + &style, + ); + + // If we have a point map, we map the style to + // this cell. + if (self.point_map) |*map| { + var discarding: std.Io.Writer.Discarding = .init(&.{}); + try self.formatStyleOpen( + &discarding.writer, + &style, + ); + for (0..discarding.count) |_| map.map.append(map.alloc, .{ + .x = x, + .y = y, + }) catch return error.WriteFailed; + } + } + switch (cell.content_tag) { // We combine codepoint and graphemes because both have // shared style handling. We use comptime to dup it. inline .codepoint, .codepoint_grapheme => |tag| { - // Handle closing our styling if we go back to unstyled - // content. - if (self.opts.emit.styled() and - !cell.hasStyling() and - !style.default()) - { - try self.formatStyleClose(writer); - style = .{}; - } - - // If we're emitting styling and we have styles, then - // we need to load the style and emit any sequences - // as necessary. - if (self.opts.emit.styled() and cell.hasStyling()) style: { - // Get the style. - const cell_style = self.page.styles.get( - self.page.memory, - cell.style_id, - ); - - // If the style hasn't changed since our last - // emitted style, don't bloat the output. - if (cell_style.eql(style)) break :style; - - // We need to emit a closing tag if the style - // was non-default before, which means we set - // styles once. - const closing = !style.default(); - - // New style, emit it. - style = cell_style.*; - try self.formatStyleOpen( - writer, - &style, - closing, - ); - - // If we have a point map, we map the style to - // this cell. - if (self.point_map) |*map| { - var discarding: std.Io.Writer.Discarding = .init(&.{}); - try self.formatStyleOpen( - &discarding.writer, - &style, - closing, - ); - for (0..discarding.count) |_| map.map.append(map.alloc, .{ - .x = x, - .y = y, - }) catch return error.WriteFailed; - } - } - try self.writeCell(tag, writer, cell); // If we have a point map, all codepoints map to this @@ -1229,10 +1250,15 @@ pub const PageFormatter = struct { } }, - // Unreachable since we do hasText() above - .bg_color_palette, - .bg_color_rgb, - => unreachable, + // Cells with only background color (no text). Emit a space + // with the appropriate background color SGR sequence. + .bg_color_palette, .bg_color_rgb => { + try writer.writeByte(' '); + if (self.point_map) |*map| map.map.append( + map.alloc, + .{ .x = x, .y = y }, + ) catch return error.WriteFailed; + }, } } } @@ -1265,6 +1291,14 @@ pub const PageFormatter = struct { writer: *std.Io.Writer, cell: *const Cell, ) !void { + // Blank cells get an empty space that isn't replaced by anything + // because it isn't really a space. We do this so that formatting + // is preserved if we're emitting styles. + if (!cell.hasText()) { + try writer.writeByte(' '); + return; + } + try self.writeCodepointWithReplacement(writer, cell.content.codepoint); if (comptime tag == .codepoint_grapheme) { for (self.page.lookupGrapheme(cell).?) |cp| { @@ -1348,18 +1382,47 @@ pub const PageFormatter = struct { } } + /// Returns the style for the given cell. If there is no styling this + /// will return the default style. + fn cellStyle( + self: *const PageFormatter, + cell: *const Cell, + ) Style { + return switch (cell.content_tag) { + inline .codepoint, .codepoint_grapheme => if (!cell.hasStyling()) + .{} + else + self.page.styles.get( + self.page.memory, + cell.style_id, + ).*, + + .bg_color_palette => .{ + .bg_color = .{ + .palette = cell.content.color_palette, + }, + }, + + .bg_color_rgb => .{ + .bg_color = .{ + .rgb = .{ + .r = cell.content.color_rgb.r, + .g = cell.content.color_rgb.g, + .b = cell.content.color_rgb.b, + }, + }, + }, + }; + } + fn formatStyleOpen( self: PageFormatter, writer: *std.Io.Writer, style: *const Style, - closing: bool, ) std.Io.Writer.Error!void { switch (self.opts.emit) { .plain => unreachable, - // Note: we don't use closing on purpose because VT sequences - // always reset the prior style. Our formatter always emits a - // \x1b[0m before emitting a new style if necessary. .vt => { var formatter = style.formatterVt(); formatter.palette = self.opts.palette; @@ -1369,7 +1432,6 @@ pub const PageFormatter = struct { // We use `display: inline` so that the div doesn't impact // layout since we're primarily using it as a CSS wrapper. .html => { - if (closing) try writer.writeAll(""); var formatter = style.formatterHtml(); formatter.palette = self.opts.palette; try writer.print( @@ -3345,7 +3407,9 @@ test "Page VT multi-line with styles" { try formatter.format(&builder.writer); const output = builder.writer.buffered(); - try testing.expectEqualStrings("\x1b[0m\x1b[1mfirst\r\n\x1b[0m\x1b[3msecond\x1b[0m", output); + // Note: style is reset before newline to prevent background colors from + // bleeding to the next line's leading cells. + try testing.expectEqualStrings("\x1b[0m\x1b[1mfirst\x1b[0m\r\n\x1b[0m\x1b[3msecond\x1b[0m", output); // Verify point map matches output length try testing.expectEqual(output.len, point_map.items.len); @@ -5819,3 +5883,57 @@ test "Page codepoint_map empty map" { const output = builder.writer.buffered(); try testing.expectEqualStrings("hello world", output); } + +test "Page VT background color on trailing blank cells" { + // This test reproduces a bug where trailing cells with background color + // but no text are emitted as plain spaces without SGR sequences. + // This causes TUIs like htop to lose background colors on rehydration. + const testing = std.testing; + const alloc = testing.allocator; + + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var t = try Terminal.init(alloc, .{ + .cols = 20, + .rows = 5, + }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + + // Simulate a TUI row: "CPU:" with text, then trailing cells with red background + // to end of line (no text after the colored region). + // \x1b[41m sets red background, then EL fills rest of row with that bg. + try s.nextSlice("CPU:\x1b[41m\x1b[K"); + // Reset colors and move to next line with different content + try s.nextSlice("\x1b[0m\r\nline2"); + + const pages = &t.screens.active.pages; + const page = &pages.pages.last.?.data; + + var formatter: PageFormatter = .init(page, .vt); + formatter.opts.trim = false; // Don't trim so we can see the trailing behavior + + try formatter.format(&builder.writer); + const output = builder.writer.buffered(); + + // The output should preserve the red background SGR for trailing cells on line 1. + // Bug: the first row outputs "CPU:\r\n" only - losing the background color fill. + // The red background should appear BEFORE the newline, not after. + + // Find position of CRLF + const crlf_pos = std.mem.indexOf(u8, output, "\r\n") orelse { + // No CRLF found, fail the test + return error.TestUnexpectedResult; + }; + + // Check that red background (48;5;1) appears BEFORE the newline (on line 1) + const line1 = output[0..crlf_pos]; + const has_red_bg_line1 = std.mem.indexOf(u8, line1, "\x1b[41m") != null or + std.mem.indexOf(u8, line1, "\x1b[48;5;1m") != null; + + // This should be true but currently fails due to the bug + try testing.expect(has_red_bg_line1); +} diff --git a/src/terminal/hash_map.zig b/src/terminal/hash_map.zig index e06050605..96dfcfdf3 100644 --- a/src/terminal/hash_map.zig +++ b/src/terminal/hash_map.zig @@ -855,13 +855,17 @@ fn HashMapUnmanaged( pub fn layoutForCapacity(new_capacity: Size) Layout { assert(new_capacity == 0 or std.math.isPowerOfTwo(new_capacity)); + // Cast to usize to prevent overflow in size calculations. + // See: https://github.com/ziglang/zig/pull/19048 + const cap: usize = new_capacity; + // Pack our metadata, keys, and values. const meta_start = @sizeOf(Header); - const meta_end = @sizeOf(Header) + new_capacity * @sizeOf(Metadata); + const meta_end = @sizeOf(Header) + cap * @sizeOf(Metadata); const keys_start = std.mem.alignForward(usize, meta_end, key_align); - const keys_end = keys_start + new_capacity * @sizeOf(K); + const keys_end = keys_start + cap * @sizeOf(K); const vals_start = std.mem.alignForward(usize, keys_end, val_align); - const vals_end = vals_start + new_capacity * @sizeOf(V); + const vals_end = vals_start + cap * @sizeOf(V); // Our total memory size required is the end of our values // aligned to the base required alignment. @@ -1511,3 +1515,26 @@ test "OffsetHashMap remake map" { try expectEqual(5, map.get(5).?); } } + +test "layoutForCapacity no overflow for large capacity" { + // Test that layoutForCapacity correctly handles large capacities without overflow. + // Prior to the fix, new_capacity (u32) was multiplied before widening to usize, + // causing overflow when new_capacity * @sizeOf(K) exceeded 2^32. + // See: https://github.com/ghostty-org/ghostty/issues/9862 + const Map = AutoHashMapUnmanaged(u64, u64); + + // Use 2^30 capacity - this would overflow in u32 when multiplied by @sizeOf(u64)=8 + // 0x40000000 * 8 = 0x2_0000_0000 which wraps to 0 in u32 + const large_cap: Map.Size = 1 << 30; + const layout = Map.layoutForCapacity(large_cap); + + // With the fix, total_size should be at least cap * (sizeof(K) + sizeof(V)) + // = 2^30 * 16 = 2^34 bytes = 16 GiB + // Without the fix, this would wrap and produce a much smaller value. + const min_expected: usize = @as(usize, large_cap) * (@sizeOf(u64) + @sizeOf(u64)); + try expect(layout.total_size >= min_expected); + + // Also verify the individual offsets don't wrap + try expect(layout.keys_start > 0); + try expect(layout.vals_start > layout.keys_start); +} diff --git a/src/terminal/highlight.zig b/src/terminal/highlight.zig index 582ef6f06..bc3a1758e 100644 --- a/src/terminal/highlight.zig +++ b/src/terminal/highlight.zig @@ -180,6 +180,15 @@ pub const Flattened = struct { }; } + pub fn endPin(self: Flattened) Pin { + const slice = self.chunks.slice(); + return .{ + .node = slice.items(.node)[slice.len - 1], + .x = self.bot_x, + .y = slice.items(.end)[slice.len - 1] - 1, + }; + } + /// Convert to an Untracked highlight. pub fn untracked(self: Flattened) Untracked { // Note: we don't use startPin/endPin here because it is slightly diff --git a/src/terminal/hyperlink.zig b/src/terminal/hyperlink.zig index 975e6f30e..94f86466c 100644 --- a/src/terminal/hyperlink.zig +++ b/src/terminal/hyperlink.zig @@ -13,8 +13,9 @@ const autoHash = std.hash.autoHash; const autoHashStrat = std.hash.autoHashStrat; /// The unique identifier for a hyperlink. This is at most the number of cells -/// that can fit in a single terminal page. -pub const Id = size.CellCountInt; +/// that can fit in a single terminal page, since each cell can only contain +/// at most one hyperlink. +pub const Id = size.HyperlinkCountInt; // The mapping of cell to hyperlink. We use an offset hash map to save space // since its very unlikely a cell is a hyperlink, so its a waste to store diff --git a/src/terminal/kitty/color.zig b/src/terminal/kitty/color.zig index deeabcfb7..c1072c390 100644 --- a/src/terminal/kitty/color.zig +++ b/src/terminal/kitty/color.zig @@ -17,6 +17,10 @@ pub const OSC = struct { /// request. terminator: Terminator = .st, + pub fn deinit(self: *OSC, alloc: std.mem.Allocator) void { + self.list.deinit(alloc); + } + /// We don't currently support encoding this to C in any way. pub const C = void; diff --git a/src/terminal/kitty/graphics_unicode.zig b/src/terminal/kitty/graphics_unicode.zig index ceadf63ee..a223797ba 100644 --- a/src/terminal/kitty/graphics_unicode.zig +++ b/src/terminal/kitty/graphics_unicode.zig @@ -256,7 +256,7 @@ pub const Placement = struct { if (img_scale_source.y < img_scaled.y_offset) { // If our source rect y is within the offset area, we need to // adjust our source rect and destination since the source texture - // doesnt actually have the offset area blank. + // doesn't actually have the offset area blank. const offset: f64 = img_scaled.y_offset - img_scale_source.y; img_scale_source.height -= offset; y_offset = offset; @@ -286,7 +286,7 @@ pub const Placement = struct { if (img_scale_source.x < img_scaled.x_offset) { // If our source rect x is within the offset area, we need to // adjust our source rect and destination since the source texture - // doesnt actually have the offset area blank. + // doesn't actually have the offset area blank. const offset: f64 = img_scaled.x_offset - img_scale_source.x; img_scale_source.width -= offset; x_offset = offset; diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index f62b7a6cd..b9061e2e9 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -12,11 +12,11 @@ const mem = std.mem; const assert = @import("../quirks.zig").inlineAssert; const Allocator = mem.Allocator; const LibEnum = @import("../lib/enum.zig").Enum; -const RGB = @import("color.zig").RGB; const kitty_color = @import("kitty/color.zig"); -const osc_color = @import("osc/color.zig"); -const string_encoding = @import("../os/string_encoding.zig"); -pub const color = osc_color; +const parsers = @import("osc/parsers.zig"); +const encoding = @import("osc/encoding.zig"); + +pub const color = parsers.color; const log = std.log.scoped(.osc); @@ -41,74 +41,8 @@ pub const Command = union(Key) { /// in the log. change_window_icon: [:0]const u8, - /// First do a fresh-line. Then start a new command, and enter prompt mode: - /// Subsequent text (until a OSC "133;B" or OSC "133;I" command) is a - /// prompt string (as if followed by OSC 133;P;k=i\007). Note: I've noticed - /// not all shells will send the prompt end code. - prompt_start: struct { - /// "aid" is an optional "application identifier" that helps disambiguate - /// nested shell sessions. It can be anything but is usually a process ID. - aid: ?[:0]const u8 = null, - /// "kind" tells us which kind of semantic prompt sequence this is: - /// - primary: normal, left-aligned first-line prompt (initial, default) - /// - continuation: an editable continuation line - /// - secondary: a non-editable continuation line - /// - right: a right-aligned prompt that may need adjustment during reflow - kind: enum { primary, continuation, secondary, right } = .primary, - /// If true, the shell will not redraw the prompt on resize so don't erase it. - /// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - redraw: bool = true, - /// Use a special key instead of arrow keys to move the cursor on - /// mouse click. Useful if arrow keys have side-effets like triggering - /// auto-complete. The shell integration script should bind the special - /// key as needed. - /// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - special_key: bool = false, - /// If true, the shell is capable of handling mouse click events. - /// Ghostty will then send a click event to the shell when the user - /// clicks somewhere in the prompt. The shell can then move the cursor - /// to that position or perform some other appropriate action. If false, - /// Ghostty may generate a number of fake key events to move the cursor - /// which is not very robust. - /// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - click_events: bool = false, - }, - - /// End of prompt and start of user input, terminated by a OSC "133;C" - /// or another prompt (OSC "133;P"). - prompt_end: void, - - /// The OSC "133;C" command can be used to explicitly end - /// the input area and begin the output area. However, some applications - /// don't provide a convenient way to emit that command. - /// That is why we also specify an implicit way to end the input area - /// at the end of the line. In the case of multiple input lines: If the - /// cursor is on a fresh (empty) line and we see either OSC "133;P" or - /// OSC "133;I" then this is the start of a continuation input line. - /// If we see anything else, it is the start of the output area (or end - /// of command). - end_of_input: struct { - /// The command line that the user entered. - /// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - cmdline: ?[:0]const u8 = null, - }, - - /// End of current command. - /// - /// The exit-code need not be specified if there are no options, - /// or if the command was cancelled (no OSC "133;C"), such as by typing - /// an interrupt/cancel character (typically ctrl-C) during line-editing. - /// Otherwise, it must be an integer code, where 0 means the command - /// succeeded, and other values indicate failure. In additing to the - /// exit-code there may be an err= option, which non-legacy terminals - /// should give precedence to. The err=_value_ option is more general: - /// an empty string is success, and any non-empty value (which need not - /// be an integer) is an error code. So to indicate success both ways you - /// could send OSC "133;D;0;err=\007", though `OSC "133;D;0\007" is shorter. - end_of_command: struct { - exit_code: ?u8 = null, - // TODO: err option - }, + /// Semantic prompt command: https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md + semantic_prompt: SemanticPrompt, /// Set or get clipboard contents. If data is null, then the current /// clipboard contents are sent to the pty. If data is set, this @@ -146,8 +80,8 @@ pub const Command = union(Key) { /// /// 4, 5, 10-19, 104, 105, 110-119 color_operation: struct { - op: osc_color.Operation, - requests: osc_color.List = .{}, + op: color.Operation, + requests: color.List = .{}, terminator: Terminator = .st, }, @@ -193,6 +127,33 @@ pub const Command = union(Key) { /// ConEmu GUI macro (OSC 9;6) conemu_guimacro: [:0]const u8, + /// ConEmu run process (OSC 9;7) + conemu_run_process: [:0]const u8, + + /// ConEmu output environment variable (OSC 9;8) + conemu_output_environment_variable: [:0]const u8, + + /// ConEmu XTerm keyboard and output emulation (OSC 9;10) + /// https://conemu.github.io/en/TerminalModes.html + conemu_xterm_emulation: struct { + /// null => do not change + /// false => turn off + /// true => turn on + keyboard: ?bool, + /// null => do not change + /// false => turn off + /// true => turn on + output: ?bool, + }, + + /// ConEmu comment (OSC 9;11) + conemu_comment: [:0]const u8, + + /// Kitty text sizing protocol (OSC 66) + kitty_text_sizing: parsers.kitty_text_sizing.OSC, + + pub const SemanticPrompt = parsers.semantic_prompt.Command; + pub const Key = LibEnum( if (build_options.c_abi) .c else .zig, // NOTE: Order matters, see LibEnum documentation. @@ -200,10 +161,7 @@ pub const Command = union(Key) { "invalid", "change_window_title", "change_window_icon", - "prompt_start", - "prompt_end", - "end_of_input", - "end_of_command", + "semantic_prompt", "clipboard_contents", "report_pwd", "mouse_shape", @@ -218,6 +176,11 @@ pub const Command = union(Key) { "conemu_progress_report", "conemu_wait_input", "conemu_guimacro", + "conemu_run_process", + "conemu_output_environment_variable", + "conemu_xterm_emulation", + "conemu_comment", + "kitty_text_sizing", }, ); @@ -309,6 +272,9 @@ pub const Terminator = enum { }; pub const Parser = struct { + /// Maximum size of a "normal" OSC. + pub const MAX_BUF = 2048; + /// 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. @@ -317,162 +283,82 @@ pub const Parser = struct { /// Current state of the parser. state: State, - /// Current command of the parser, this accumulates. + /// Buffer for temporary storage of OSC data + buffer: [MAX_BUF]u8, + /// Fixed writer for accumulating OSC data + fixed: ?std.Io.Writer, + /// Allocating writer for accumulating OSC data + allocating: ?std.Io.Writer.Allocating, + /// Pointer to the active writer for accumulating OSC data + writer: ?*std.Io.Writer, + + /// The command that is the result of parsing. 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, - buf_start: usize, - buf_idx: usize, - buf_dynamic: ?*std.ArrayListUnmanaged(u8), - - /// True when a command is complete/valid to return. - complete: bool, - - /// Temporary state that is dependent on the current state. - temp_state: union { - /// Current string parameter being populated - str: *[:0]const u8, - - /// Current numeric parameter being populated - num: u16, - - /// Temporary state for key/value pairs - key: []const u8, - }, - - // Maximum length of a single OSC command. This is the full OSC command - // sequence length (excluding ESC ]). This is arbitrary, I couldn't find - // any definitive resource on how long this should be. - // - // NOTE: This does mean certain OSC sequences such as OSC 8 (hyperlinks) - // won't work if their parameters are larger than fit in the buffer. - const MAX_BUF = 2048; - pub const State = enum { - empty, + start, invalid, - swallow, - // Command prefixes. We could just accumulate and compare (mem.eql) - // but the state space is small enough that we just build it up this way. + // OSC command prefixes. Not all of these are valid OSCs, but may be + // needed to "bridge" to a valid OSC (e.g. to support OSC 777 we need to + // have a state "77" even though there is no OSC 77). @"0", @"1", + @"2", + @"4", + @"5", + @"6", + @"7", + @"8", + @"9", @"10", - @"104", @"11", @"12", @"13", - @"133", @"14", @"15", @"16", @"17", @"18", @"19", - @"2", @"21", @"22", - @"4", - @"5", @"52", - @"7", + @"66", @"77", + @"104", + @"110", + @"111", + @"112", + @"113", + @"114", + @"115", + @"116", + @"117", + @"118", + @"119", + @"133", @"777", - @"8", - @"9", - - // We're in a semantic prompt OSC command but we aren't sure - // what the command is yet, i.e. `133;` - semantic_prompt, - semantic_option_start, - semantic_option_key, - semantic_option_value, - semantic_exit_code_start, - semantic_exit_code, - - // Get/set clipboard states - clipboard_kind, - clipboard_kind_end, - - // OSC color operation. - osc_color, - - // Hyperlinks - hyperlink_param_key, - hyperlink_param_value, - hyperlink_uri, - - // rxvt extension. Only used for OSC 777 and only the value "notify" is - // supported - rxvt_extension, - - // Title of a desktop notification - notification_title, - - // Expect a string parameter. param_str must be set as well as - // buf_start. - string, - - // A string that can grow beyond MAX_BUF. This uses the allocator. - // If the parser has no allocator then it is treated as if the - // buffer is full. - allocable_string, - - // Kitty color protocol - // https://sw.kovidgoyal.net/kitty/color-stack/#id1 - kitty_color_protocol_key, - kitty_color_protocol_value, - - // OSC 9 is used by ConEmu and iTerm2 for different things. - // iTerm2 uses it to post a notification[1]. - // ConEmu uses it to implement many custom functions[2]. - // - // Some Linux applications (namely systemd and flatpak) have - // adopted the ConEmu implementation but this causes bogus - // notifications on iTerm2 compatible terminal emulators. - // - // Ghostty supports both by disallowing ConEmu-specific commands - // from being shown as desktop notifications. - // - // [1]: https://iterm2.com/documentation-escape-codes.html - // [2]: https://conemu.github.io/en/AnsiEscapeCodes.html#OSC_Operating_system_commands - osc_9, - - // ConEmu specific substates - conemu_sleep, - conemu_sleep_value, - conemu_message_box, - conemu_tab, - conemu_tab_txt, - conemu_progress_prestate, - conemu_progress_state, - conemu_progress_prevalue, - conemu_progress_value, - conemu_guimacro, + @"1337", }; pub fn init(alloc: ?Allocator) Parser { var result: Parser = .{ .alloc = alloc, - .state = .empty, + .state = .start, + .fixed = null, + .allocating = null, + .writer = null, .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, + .buffer = 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; + result.buffer = undefined; } return result; @@ -485,107 +371,124 @@ pub const Parser = struct { /// Reset the parser state. pub fn reset(self: *Parser) void { - // If the state is already empty then we do nothing because - // we may touch uninitialized memory. - if (self.state == .empty) { - assert(self.buf_start == 0); - assert(self.buf_idx == 0); - assert(!self.complete); - assert(self.buf_dynamic == null); - return; - } + // If we set up an allocating writer, free up that memory. + if (self.allocating) |*allocating| allocating.deinit(); - // Some commands have their own memory management we need to clear. + // Handle any cleanup that individual OSCs require. switch (self.command) { - .kitty_color_protocol => |*v| v.list.deinit(self.alloc.?), - .color_operation => |*v| v.requests.deinit(self.alloc.?), - else => {}, + .kitty_color_protocol => |*v| kitty_color_protocol: { + v.deinit(self.alloc orelse break :kitty_color_protocol); + }, + .change_window_icon, + .change_window_title, + .clipboard_contents, + .color_operation, + .conemu_change_tab_title, + .conemu_comment, + .conemu_guimacro, + .conemu_output_environment_variable, + .conemu_progress_report, + .conemu_run_process, + .conemu_show_message_box, + .conemu_sleep, + .conemu_wait_input, + .conemu_xterm_emulation, + .hyperlink_end, + .hyperlink_start, + .invalid, + .mouse_shape, + .report_pwd, + .semantic_prompt, + .show_desktop_notification, + .kitty_text_sizing, + => {}, } - self.state = .empty; - self.buf_start = 0; - self.buf_idx = 0; + self.state = .start; + self.fixed = null; + self.allocating = null; + self.writer = null; self.command = .invalid; - self.complete = false; - if (self.buf_dynamic) |ptr| { - const alloc = self.alloc.?; - ptr.deinit(alloc); - alloc.destroy(ptr); - self.buf_dynamic = null; + + if (std.valgrind.runningOnValgrind() > 0) { + // Initialize our undefined fields so Valgrind can catch it. + // https://github.com/ziglang/zig/issues/19148 + self.buffer = undefined; } } + /// Make sure that we have an allocator. If we don't, set the state to + /// invalid so that any additional OSC data is discarded. + inline fn ensureAllocator(self: *Parser) bool { + if (self.alloc != null) return true; + log.warn("An allocator is required to process OSC {t} but none was provided.", .{self.state}); + self.state = .invalid; + return false; + } + + /// Set up a fixed Writer to collect the rest of the OSC data. + inline fn writeToFixed(self: *Parser) void { + self.fixed = .fixed(&self.buffer); + self.writer = &self.fixed.?; + } + + /// Set up an allocating Writer to collect the rest of the OSC data. If we + /// don't have an allocator or setting up the allocator fails, fall back to + /// writing to a fixed buffer and hope that it's big enough. + inline fn writeToAllocating(self: *Parser) void { + const alloc = self.alloc orelse { + // We don't have an allocator - fall back to a fixed buffer and hope + // that it's big enough. + self.writeToFixed(); + return; + }; + + self.allocating = std.Io.Writer.Allocating.initCapacity(alloc, 2048) catch { + // The allocator failed for some reason, fall back to a fixed buffer + // and hope that it's big enough. + self.writeToFixed(); + return; + }; + + self.writer = &self.allocating.?.writer; + } + /// Consume the next character c and advance the parser state. pub fn next(self: *Parser, c: u8) void { - // If our buffer is full then we're invalid, so we set our state - // accordingly and indicate the sequence is incomplete so that we - // don't accidentally issue a command when ending. - // - // We always keep space for 1 byte at the end to null-terminate - // values. - if (self.buf_idx >= self.buf.len - 1) { - @branchHint(.cold); - if (self.state != .invalid) { - log.warn( - "OSC sequence too long (> {d}), ignoring. state={}", - .{ self.buf.len, self.state }, - ); - } + // If the state becomes invalid for any reason, just discard + // any further input. + if (self.state == .invalid) return; - self.state = .invalid; - - // We have to do this here because it will never reach the - // switch statement below, since our buf_idx will always be - // too high after this. - self.complete = false; + // If a writer has been initialized, we just accumulate the rest of the + // OSC sequence in the writer's buffer and skip the state machine. + if (self.writer) |writer| { + writer.writeByte(c) catch |err| switch (err) { + // We have overflowed our buffer or had some other error, set the + // state to invalid so that we discard any further input. + error.WriteFailed => self.state = .invalid, + }; return; } - // We store everything in the buffer so we can do a better job - // logging if we get to an invalid command. - self.buf[self.buf_idx] = c; - self.buf_idx += 1; - - // log.warn("state = {} c = {x}", .{ self.state, c }); - switch (self.state) { - // If we get something during the invalid state, we've - // ruined our entry. - .invalid => self.complete = false, + // handled above, so should never be here + .invalid => unreachable, - .empty => switch (c) { + .start => switch (c) { '0' => self.state = .@"0", '1' => self.state = .@"1", '2' => self.state = .@"2", '4' => self.state = .@"4", '5' => self.state = .@"5", + '6' => self.state = .@"6", '7' => self.state = .@"7", '8' => self.state = .@"8", '9' => self.state = .@"9", else => self.state = .invalid, }, - .swallow => {}, - - .@"0" => switch (c) { - ';' => { - self.command = .{ .change_window_title = undefined }; - self.complete = true; - self.state = .string; - self.temp_state = .{ .str = &self.command.change_window_title }; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - .@"1" => switch (c) { - ';' => { - self.command = .{ .change_window_icon = undefined }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.change_window_icon }; - self.buf_start = self.buf_idx; - }, + ';' => self.writeToFixed(), '0' => self.state = .@"10", '1' => self.state = .@"11", '2' => self.state = .@"12", @@ -600,390 +503,88 @@ pub const Parser = struct { }, .@"10" => switch (c) { - ';' => osc_10: { - if (self.alloc == null) { - log.warn("OSC 10 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_10; - } - self.command = .{ .color_operation = .{ - .op = .osc_10, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, - '4' => { - self.state = .@"104"; - // If we have an allocator, then we can complete the OSC104 - if (self.alloc != null) self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), + '4' => self.state = .@"104", else => self.state = .invalid, }, .@"104" => switch (c) { - ';' => osc_104: { - if (self.alloc == null) { - log.warn("OSC 104 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_104; - } - self.command = .{ - .color_operation = .{ - .op = .osc_104, - }, - }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"11" => switch (c) { - ';' => osc_11: { - if (self.alloc == null) { - log.warn("OSC 11 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_11; - } - self.command = .{ .color_operation = .{ - .op = .osc_11, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, - '0'...'9' => blk: { - if (self.alloc == null) { - log.warn("OSC 11{c} requires an allocator, but none was provided", .{c}); - self.state = .invalid; - break :blk; - } - - self.command = .{ - .color_operation = .{ - .op = switch (c) { - '0' => .osc_110, - '1' => .osc_111, - '2' => .osc_112, - '3' => .osc_113, - '4' => .osc_114, - '5' => .osc_115, - '6' => .osc_116, - '7' => .osc_117, - '8' => .osc_118, - '9' => .osc_119, - else => unreachable, - }, - }, - }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), + '0' => self.state = .@"110", + '1' => self.state = .@"111", + '2' => self.state = .@"112", + '3' => self.state = .@"113", + '4' => self.state = .@"114", + '5' => self.state = .@"115", + '6' => self.state = .@"116", + '7' => self.state = .@"117", + '8' => self.state = .@"118", + '9' => self.state = .@"119", else => self.state = .invalid, }, - .@"12" => switch (c) { - ';' => osc_12: { - if (self.alloc == null) { - log.warn("OSC 12 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_12; - } - self.command = .{ .color_operation = .{ - .op = .osc_12, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + .@"4", + .@"12", + .@"14", + .@"15", + .@"16", + .@"17", + .@"18", + .@"19", + .@"21", + .@"110", + .@"111", + .@"112", + .@"113", + .@"114", + .@"115", + .@"116", + .@"117", + .@"118", + .@"119", + => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"13" => switch (c) { - ';' => osc_13: { - if (self.alloc == null) { - log.warn("OSC 13 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_13; - } - self.command = .{ .color_operation = .{ - .op = .osc_13, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), '3' => self.state = .@"133", else => self.state = .invalid, }, - .@"133" => switch (c) { - ';' => self.state = .semantic_prompt, - else => self.state = .invalid, - }, - - .@"14" => switch (c) { - ';' => osc_14: { - if (self.alloc == null) { - log.warn("OSC 14 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_14; - } - self.command = .{ .color_operation = .{ - .op = .osc_14, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, - else => self.state = .invalid, - }, - - .@"15" => switch (c) { - ';' => osc_15: { - if (self.alloc == null) { - log.warn("OSC 15 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_15; - } - self.command = .{ .color_operation = .{ - .op = .osc_15, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, - else => self.state = .invalid, - }, - - .@"16" => switch (c) { - ';' => osc_16: { - if (self.alloc == null) { - log.warn("OSC 16 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_16; - } - self.command = .{ .color_operation = .{ - .op = .osc_16, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, - else => self.state = .invalid, - }, - - .@"17" => switch (c) { - ';' => osc_17: { - if (self.alloc == null) { - log.warn("OSC 17 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_17; - } - self.command = .{ .color_operation = .{ - .op = .osc_17, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, - else => self.state = .invalid, - }, - - .@"18" => switch (c) { - ';' => osc_18: { - if (self.alloc == null) { - log.warn("OSC 18 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_18; - } - self.command = .{ .color_operation = .{ - .op = .osc_18, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, - else => self.state = .invalid, - }, - - .@"19" => switch (c) { - ';' => osc_19: { - if (self.alloc == null) { - log.warn("OSC 19 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_19; - } - self.command = .{ .color_operation = .{ - .op = .osc_19, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, - else => self.state = .invalid, - }, - - .osc_color => {}, - .@"2" => switch (c) { + ';' => self.writeToFixed(), '1' => self.state = .@"21", '2' => self.state = .@"22", - ';' => { - self.command = .{ .change_window_title = undefined }; - self.complete = true; - self.state = .string; - self.temp_state = .{ .str = &self.command.change_window_title }; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .@"21" => switch (c) { - ';' => kitty: { - if (self.alloc == null) { - log.info("OSC 21 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :kitty; - } - - self.command = .{ - .kitty_color_protocol = .{ - .list = .empty, - }, - }; - - self.temp_state = .{ .key = "" }; - self.state = .kitty_color_protocol_key; - self.complete = true; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .kitty_color_protocol_key => switch (c) { - ';' => { - self.temp_state = .{ .key = self.buf[self.buf_start .. self.buf_idx - 1] }; - self.endKittyColorProtocolOption(.key_only, false); - self.state = .kitty_color_protocol_key; - self.buf_start = self.buf_idx; - }, - '=' => { - self.temp_state = .{ .key = self.buf[self.buf_start .. self.buf_idx - 1] }; - self.state = .kitty_color_protocol_value; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - - .kitty_color_protocol_value => switch (c) { - ';' => { - self.endKittyColorProtocolOption(.key_and_value, false); - self.state = .kitty_color_protocol_key; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - - .@"22" => switch (c) { - ';' => { - self.command = .{ .mouse_shape = undefined }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.mouse_shape.value }; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .@"4" => switch (c) { - ';' => osc_4: { - if (self.alloc == null) { - log.info("OSC 4 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_4; - } - self.command = .{ - .color_operation = .{ - .op = .osc_4, - }, - }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, else => self.state = .invalid, }, .@"5" => switch (c) { - ';' => osc_5: { - if (self.alloc == null) { - log.info("OSC 5 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_5; - } - self.command = .{ - .color_operation = .{ - .op = .osc_5, - }, - }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), '2' => self.state = .@"52", else => self.state = .invalid, }, - .@"52" => switch (c) { - ';' => { - self.command = .{ .clipboard_contents = undefined }; - self.state = .clipboard_kind; - }, + .@"6" => switch (c) { + '6' => self.state = .@"66", else => self.state = .invalid, }, - .clipboard_kind => switch (c) { - ';' => { - self.command.clipboard_contents.kind = 'c'; - self.temp_state = .{ .str = &self.command.clipboard_contents.data }; - self.buf_start = self.buf_idx; - self.prepAllocableString(); - - // See clipboard_kind_end - self.complete = true; - }, - else => { - self.command.clipboard_contents.kind = c; - self.state = .clipboard_kind_end; - }, - }, - - .clipboard_kind_end => switch (c) { - ';' => { - self.temp_state = .{ .str = &self.command.clipboard_contents.data }; - self.buf_start = self.buf_idx; - self.prepAllocableString(); - - // OSC 52 can have empty payloads (quoting xterm ctlseqs): - // "If the second parameter is neither a base64 string nor ?, - // then the selection is cleared." - self.complete = true; - }, + .@"52", + .@"66", + => switch (c) { + ';' => self.writeToAllocating(), else => self.state = .invalid, }, .@"7" => switch (c) { - ';' => { - self.command = .{ .report_pwd = .{ .value = "" } }; - self.complete = true; - self.state = .string; - self.temp_state = .{ .str = &self.command.report_pwd.value }; - self.buf_start = self.buf_idx; - }, + ';' => self.writeToFixed(), '7' => self.state = .@"77", else => self.state = .invalid, }, @@ -993,711 +594,31 @@ pub const Parser = struct { else => self.state = .invalid, }, - .@"777" => switch (c) { - ';' => { - self.state = .rxvt_extension; - self.buf_start = self.buf_idx; - }, + .@"133", + => switch (c) { + ';' => self.writeToFixed(), + '7' => self.state = .@"1337", else => self.state = .invalid, }, - .@"8" => switch (c) { - ';' => { - self.command = .{ .hyperlink_start = .{ - .uri = "", - } }; - - self.state = .hyperlink_param_key; - self.buf_start = self.buf_idx; - }, + .@"1337", + => switch (c) { + ';' => self.writeToFixed(), else => self.state = .invalid, }, - .hyperlink_param_key => switch (c) { - ';' => { - self.complete = true; - self.state = .hyperlink_uri; - self.buf_start = self.buf_idx; - }, - '=' => { - self.temp_state = .{ .key = self.buf[self.buf_start .. self.buf_idx - 1] }; - self.state = .hyperlink_param_value; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - - .hyperlink_param_value => switch (c) { - ':' => { - self.endHyperlinkOptionValue(); - self.state = .hyperlink_param_key; - self.buf_start = self.buf_idx; - }, - ';' => { - self.endHyperlinkOptionValue(); - self.state = .string; - self.temp_state = .{ .str = &self.command.hyperlink_start.uri }; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - - .hyperlink_uri => {}, - - .rxvt_extension => switch (c) { - 'a'...'z' => {}, - ';' => { - const ext = self.buf[self.buf_start .. self.buf_idx - 1]; - if (!std.mem.eql(u8, ext, "notify")) { - @branchHint(.cold); - log.warn("unknown rxvt extension: {s}", .{ext}); - self.state = .invalid; - return; - } - - self.command = .{ .show_desktop_notification = undefined }; - self.buf_start = self.buf_idx; - self.state = .notification_title; - }, + .@"0", + .@"22", + .@"777", + .@"8", + .@"9", + => switch (c) { + ';' => self.writeToFixed(), else => self.state = .invalid, }, - - .notification_title => switch (c) { - ';' => { - self.buf[self.buf_idx - 1] = 0; - self.command.show_desktop_notification.title = self.buf[self.buf_start .. self.buf_idx - 1 :0]; - self.temp_state = .{ .str = &self.command.show_desktop_notification.body }; - self.buf_start = self.buf_idx; - self.state = .string; - }, - else => {}, - }, - - .@"9" => switch (c) { - ';' => { - self.buf_start = self.buf_idx; - self.state = .osc_9; - }, - else => self.state = .invalid, - }, - - .osc_9 => switch (c) { - '1' => { - self.state = .conemu_sleep; - // This will end up being either a ConEmu sleep OSC 9;1, - // or a desktop notification OSC 9 that begins with '1', so - // mark as complete. - self.complete = true; - }, - '2' => { - self.state = .conemu_message_box; - // This will end up being either a ConEmu message box OSC 9;2, - // or a desktop notification OSC 9 that begins with '2', so - // mark as complete. - self.complete = true; - }, - '3' => { - self.state = .conemu_tab; - // This will end up being either a ConEmu message box OSC 9;3, - // or a desktop notification OSC 9 that begins with '3', so - // mark as complete. - self.complete = true; - }, - '4' => { - self.state = .conemu_progress_prestate; - // This will end up being either a ConEmu progress report - // OSC 9;4, or a desktop notification OSC 9 that begins with - // '4', so mark as complete. - self.complete = true; - }, - '5' => { - // Note that sending an OSC 9 desktop notification that - // starts with 5 is impossible due to this. - self.state = .swallow; - self.command = .conemu_wait_input; - self.complete = true; - }, - '6' => { - self.state = .conemu_guimacro; - // This will end up being either a ConEmu GUI macro OSC 9;6, - // or a desktop notification OSC 9 that begins with '6', so - // mark as complete. - self.complete = true; - }, - - // Todo: parse out other ConEmu operating system commands. Even - // if we don't support them we probably don't want them showing - // up as desktop notifications. - - else => self.showDesktopNotification(), - }, - - .conemu_sleep => switch (c) { - ';' => { - self.command = .{ .conemu_sleep = .{ .duration_ms = 100 } }; - self.buf_start = self.buf_idx; - self.complete = true; - self.state = .conemu_sleep_value; - }, - - // OSC 9;1 is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .conemu_sleep_value => switch (c) { - else => self.complete = true, - }, - - .conemu_message_box => switch (c) { - ';' => { - self.command = .{ .conemu_show_message_box = undefined }; - self.temp_state = .{ .str = &self.command.conemu_show_message_box }; - self.buf_start = self.buf_idx; - self.complete = true; - self.prepAllocableString(); - }, - - // OSC 9;2 is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .conemu_tab => switch (c) { - ';' => { - self.state = .conemu_tab_txt; - self.command = .{ .conemu_change_tab_title = .reset }; - self.buf_start = self.buf_idx; - self.complete = true; - }, - - // OSC 9;3 is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .conemu_tab_txt => { - self.command = .{ .conemu_change_tab_title = .{ .value = undefined } }; - self.temp_state = .{ .str = &self.command.conemu_change_tab_title.value }; - self.complete = true; - self.prepAllocableString(); - }, - - .conemu_progress_prestate => switch (c) { - ';' => { - self.command = .{ .conemu_progress_report = .{ - .state = undefined, - } }; - self.state = .conemu_progress_state; - }, - - // OSC 9;4 is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .conemu_progress_state => switch (c) { - '0' => { - self.command.conemu_progress_report.state = .remove; - self.state = .swallow; - self.complete = true; - }, - '1' => { - self.command.conemu_progress_report.state = .set; - self.command.conemu_progress_report.progress = 0; - self.state = .conemu_progress_prevalue; - }, - '2' => { - self.command.conemu_progress_report.state = .@"error"; - self.complete = true; - self.state = .conemu_progress_prevalue; - }, - '3' => { - self.command.conemu_progress_report.state = .indeterminate; - self.complete = true; - self.state = .swallow; - }, - '4' => { - self.command.conemu_progress_report.state = .pause; - self.complete = true; - self.state = .conemu_progress_prevalue; - }, - - // OSC 9;4; is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .conemu_progress_prevalue => switch (c) { - ';' => { - self.state = .conemu_progress_value; - }, - - // OSC 9;4;<0-4> is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .conemu_progress_value => switch (c) { - '0'...'9' => value: { - // No matter what substate we're in, a number indicates - // a completed ConEmu progress command. - self.complete = true; - - // If we aren't a set substate, then we don't care - // about the value. - const p = &self.command.conemu_progress_report; - switch (p.state) { - .remove, - .indeterminate, - => break :value, - .set, - .@"error", - .pause, - => {}, - } - - if (p.state == .set) - assert(p.progress != null) - else if (p.progress == null) - p.progress = 0; - - // If we're over 100% we're done. - if (p.progress.? >= 100) break :value; - - // If we're over 10 then any new digit forces us to - // be 100. - if (p.progress.? >= 10) - p.progress = 100 - else { - const d = std.fmt.charToDigit(c, 10) catch 0; - p.progress = @min(100, (p.progress.? * 10) + d); - } - }, - - else => { - self.state = .swallow; - self.complete = true; - }, - }, - - .conemu_guimacro => switch (c) { - ';' => { - self.command = .{ .conemu_guimacro = undefined }; - self.temp_state = .{ .str = &self.command.conemu_guimacro }; - self.buf_start = self.buf_idx; - self.state = .string; - self.complete = true; - }, - - // OSC 9;6 is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .semantic_prompt => switch (c) { - 'A' => { - self.state = .semantic_option_start; - self.command = .{ .prompt_start = .{} }; - self.complete = true; - }, - - 'B' => { - self.state = .semantic_option_start; - self.command = .{ .prompt_end = {} }; - self.complete = true; - }, - - 'C' => { - self.state = .semantic_option_start; - self.command = .{ .end_of_input = .{} }; - self.complete = true; - }, - - 'D' => { - self.state = .semantic_exit_code_start; - self.command = .{ .end_of_command = .{} }; - self.complete = true; - }, - - else => self.state = .invalid, - }, - - .semantic_option_start => switch (c) { - ';' => { - self.state = .semantic_option_key; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .semantic_option_key => switch (c) { - '=' => { - self.temp_state = .{ .key = self.buf[self.buf_start .. self.buf_idx - 1] }; - self.state = .semantic_option_value; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - - .semantic_option_value => switch (c) { - ';' => { - self.endSemanticOptionValue(); - self.state = .semantic_option_key; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - - .semantic_exit_code_start => switch (c) { - ';' => { - // No longer complete, if ';' shows up we expect some code. - self.complete = false; - self.state = .semantic_exit_code; - self.temp_state = .{ .num = 0 }; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .semantic_exit_code => switch (c) { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { - self.complete = true; - - const idx = self.buf_idx - self.buf_start; - if (idx > 0) self.temp_state.num *|= 10; - self.temp_state.num +|= c - '0'; - }, - ';' => { - self.endSemanticExitCode(); - self.state = .semantic_option_key; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .allocable_string => { - const alloc = self.alloc.?; - const list = self.buf_dynamic.?; - list.append(alloc, c) catch { - self.state = .invalid; - return; - }; - - // Never consume buffer space for allocable strings - self.buf_idx -= 1; - - // We can complete at any time - self.complete = true; - }, - - .string => self.complete = true, } } - fn showDesktopNotification(self: *Parser) void { - self.command = .{ .show_desktop_notification = .{ - .title = "", - .body = undefined, - } }; - - self.temp_state = .{ .str = &self.command.show_desktop_notification.body }; - self.state = .string; - // Set as complete as we've already seen one character that should be - // part of the notification. If we wait for another character to set - // `complete` when the state is `.string` we won't be able to send any - // single character notifications. - self.complete = true; - } - - fn prepAllocableString(self: *Parser) void { - assert(self.buf_dynamic == null); - - // We need an allocator. If we don't have an allocator, we - // pretend we're just a fixed buffer string and hope we fit! - const alloc = self.alloc orelse { - self.state = .string; - return; - }; - - // Allocate our dynamic buffer - const list = alloc.create(std.ArrayListUnmanaged(u8)) catch { - self.state = .string; - return; - }; - list.* = .{}; - - self.buf_dynamic = list; - self.state = .allocable_string; - } - - fn endHyperlink(self: *Parser) void { - switch (self.command) { - .hyperlink_start => |*v| { - self.buf[self.buf_idx] = 0; - const value = self.buf[self.buf_start..self.buf_idx :0]; - if (v.id == null and value.len == 0) { - self.command = .{ .hyperlink_end = {} }; - return; - } - - v.uri = value; - }, - - else => unreachable, - } - } - - fn endHyperlinkOptionValue(self: *Parser) void { - const value: [:0]const u8 = if (self.buf_start == self.buf_idx) - "" - else buf: { - self.buf[self.buf_idx - 1] = 0; - break :buf self.buf[self.buf_start .. self.buf_idx - 1 :0]; - }; - - if (mem.eql(u8, self.temp_state.key, "id")) { - switch (self.command) { - .hyperlink_start => |*v| { - // We treat empty IDs as null ids so that we can - // auto-assign. - if (value.len > 0) v.id = value; - }, - else => {}, - } - } else log.info("unknown hyperlink option: {s}", .{self.temp_state.key}); - } - - fn endSemanticOptionValue(self: *Parser) void { - const value = value: { - self.buf[self.buf_idx] = 0; - defer self.buf_idx += 1; - break :value self.buf[self.buf_start..self.buf_idx :0]; - }; - - if (mem.eql(u8, self.temp_state.key, "aid")) { - switch (self.command) { - .prompt_start => |*v| v.aid = value, - else => {}, - } - } else if (mem.eql(u8, self.temp_state.key, "cmdline")) { - // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - switch (self.command) { - .end_of_input => |*v| v.cmdline = string_encoding.printfQDecode(value) catch null, - else => {}, - } - } else if (mem.eql(u8, self.temp_state.key, "cmdline_url")) { - // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - switch (self.command) { - .end_of_input => |*v| v.cmdline = string_encoding.urlPercentDecode(value) catch null, - else => {}, - } - } else if (mem.eql(u8, self.temp_state.key, "redraw")) { - // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - switch (self.command) { - .prompt_start => |*v| { - const valid = if (value.len == 1) valid: { - switch (value[0]) { - '0' => v.redraw = false, - '1' => v.redraw = true, - else => break :valid false, - } - - break :valid true; - } else false; - - if (!valid) { - log.info("OSC 133 A invalid redraw value: {s}", .{value}); - } - }, - else => {}, - } - } else if (mem.eql(u8, self.temp_state.key, "special_key")) { - // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - switch (self.command) { - .prompt_start => |*v| { - const valid = if (value.len == 1) valid: { - switch (value[0]) { - '0' => v.special_key = false, - '1' => v.special_key = true, - else => break :valid false, - } - - break :valid true; - } else false; - - if (!valid) { - log.info("OSC 133 A invalid special_key value: {s}", .{value}); - } - }, - else => {}, - } - } else if (mem.eql(u8, self.temp_state.key, "click_events")) { - // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - switch (self.command) { - .prompt_start => |*v| { - const valid = if (value.len == 1) valid: { - switch (value[0]) { - '0' => v.click_events = false, - '1' => v.click_events = true, - else => break :valid false, - } - - break :valid true; - } else false; - - if (!valid) { - log.info("OSC 133 A invalid click_events value: {s}", .{value}); - } - }, - else => {}, - } - } else if (mem.eql(u8, self.temp_state.key, "k")) { - // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - // The "k" marks the kind of prompt, or "primary" if we don't know. - // This can be used to distinguish between the first (initial) prompt, - // a continuation, etc. - switch (self.command) { - .prompt_start => |*v| if (value.len == 1) { - v.kind = switch (value[0]) { - 'c' => .continuation, - 's' => .secondary, - 'r' => .right, - 'i' => .primary, - else => .primary, - }; - }, - else => {}, - } - } else log.info("unknown semantic prompts option: {s}", .{self.temp_state.key}); - } - - fn endSemanticExitCode(self: *Parser) void { - switch (self.command) { - .end_of_command => |*v| v.exit_code = @truncate(self.temp_state.num), - else => {}, - } - } - - fn endString(self: *Parser) void { - self.buf[self.buf_idx] = 0; - defer self.buf_idx += 1; - self.temp_state.str.* = self.buf[self.buf_start..self.buf_idx :0]; - } - - fn endConEmuSleepValue(self: *Parser) void { - switch (self.command) { - .conemu_sleep => |*v| v.duration_ms = value: { - const str = self.buf[self.buf_start..self.buf_idx]; - if (str.len == 0) break :value 100; - - if (std.fmt.parseUnsigned(u16, str, 10)) |num| { - break :value @min(num, 10_000); - } else |_| { - break :value 100; - } - }, - else => {}, - } - } - - fn endKittyColorProtocolOption(self: *Parser, kind: enum { key_only, key_and_value }, final: bool) void { - if (self.temp_state.key.len == 0) { - @branchHint(.cold); - log.warn("zero length key in kitty color protocol", .{}); - return; - } - - const key = kitty_color.Kind.parse(self.temp_state.key) orelse { - @branchHint(.cold); - log.warn("unknown key in kitty color protocol: {s}", .{self.temp_state.key}); - return; - }; - - const value = value: { - if (self.buf_start == self.buf_idx) break :value ""; - if (final) break :value std.mem.trim(u8, self.buf[self.buf_start..self.buf_idx], " "); - break :value std.mem.trim(u8, self.buf[self.buf_start .. self.buf_idx - 1], " "); - }; - - switch (self.command) { - .kitty_color_protocol => |*v| { - // Cap our allocation amount for our list. - if (v.list.items.len >= @as(usize, kitty_color.Kind.max) * 2) { - @branchHint(.cold); - self.state = .invalid; - log.warn("exceeded limit for number of keys in kitty color protocol, ignoring", .{}); - return; - } - - // Asserted when the command is set to kitty_color_protocol - // that we have an allocator. - const alloc = self.alloc.?; - - if (kind == .key_only or value.len == 0) { - v.list.append(alloc, .{ .reset = key }) catch |err| { - @branchHint(.cold); - log.warn("unable to append kitty color protocol option: {}", .{err}); - return; - }; - } else if (mem.eql(u8, "?", value)) { - v.list.append(alloc, .{ .query = key }) catch |err| { - @branchHint(.cold); - log.warn("unable to append kitty color protocol option: {}", .{err}); - return; - }; - } else { - v.list.append(alloc, .{ - .set = .{ - .key = key, - .color = RGB.parse(value) catch |err| switch (err) { - error.InvalidFormat => { - log.warn("invalid color format in kitty color protocol: {s}", .{value}); - return; - }, - }, - }, - }) catch |err| { - @branchHint(.cold); - log.warn("unable to append kitty color protocol option: {}", .{err}); - return; - }; - } - }, - else => {}, - } - } - - fn endOscColor(self: *Parser) void { - const alloc = self.alloc.?; - assert(self.command == .color_operation); - const data = self.buf[self.buf_start..self.buf_idx]; - self.command.color_operation.requests = osc_color.parse( - alloc, - self.command.color_operation.op, - data, - ) catch |err| list: { - log.info( - "failed to parse OSC color request err={} data={s}", - .{ err, data }, - ); - break :list .{}; - }; - } - - fn endAllocableString(self: *Parser) void { - const alloc = self.alloc.?; - const list = self.buf_dynamic.?; - list.append(alloc, 0) catch { - @branchHint(.cold); - log.warn("allocation failed on allocable string termination", .{}); - self.temp_state.str.* = ""; - return; - }; - - self.temp_state.str.* = list.items[0 .. list.items.len - 1 :0]; - } - /// End the sequence and return the command, if any. If the return value /// is null, then no valid command was found. The optional terminator_ch /// is the final character in the OSC sequence. This is used to determine @@ -1706,1634 +627,70 @@ pub const Parser = struct { /// The returned pointer is only valid until the next call to the parser. /// Callers should copy out any data they wish to retain across calls. pub fn end(self: *Parser, terminator_ch: ?u8) ?*Command { - if (!self.complete) { - if (comptime !builtin.is_test) log.warn( - "invalid OSC command: {s}", - .{self.buf[0..self.buf_idx]}, - ); - return null; - } + return switch (self.state) { + .start => null, - // Other cleanup we may have to do depending on state. - switch (self.state) { - .allocable_string => self.endAllocableString(), - .semantic_exit_code => self.endSemanticExitCode(), - .semantic_option_value => self.endSemanticOptionValue(), - .hyperlink_uri => self.endHyperlink(), - .string => self.endString(), - .conemu_sleep_value => self.endConEmuSleepValue(), - .kitty_color_protocol_key => self.endKittyColorProtocolOption(.key_only, true), - .kitty_color_protocol_value => self.endKittyColorProtocolOption(.key_and_value, true), - .osc_color => self.endOscColor(), + .invalid => null, - // 104 abruptly ended turns into a reset palette command. - .@"104" => { - self.command = .{ .color_operation = .{ - .op = .osc_104, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.endOscColor(); - }, + .@"0", + .@"2", + => parsers.change_window_title.parse(self, terminator_ch), - // We received OSC 9;X ST, but nothing else, finish off as a - // desktop notification with "X" as the body. - .conemu_sleep, - .conemu_message_box, - .conemu_tab, - .conemu_progress_prestate, - .conemu_progress_state, - .conemu_guimacro, - => { - self.showDesktopNotification(); - self.endString(); - }, + .@"1" => parsers.change_window_icon.parse(self, terminator_ch), - // A ConEmu progress report that has reached these states is - // complete, don't do anything to them. - .conemu_progress_prevalue, - .conemu_progress_value, - => {}, + .@"4", + .@"5", + .@"10", + .@"11", + .@"12", + .@"13", + .@"14", + .@"15", + .@"16", + .@"17", + .@"18", + .@"19", + .@"104", + .@"110", + .@"111", + .@"112", + .@"113", + .@"114", + .@"115", + .@"116", + .@"117", + .@"118", + .@"119", + => parsers.color.parse(self, terminator_ch), - else => {}, - } + .@"7" => parsers.report_pwd.parse(self, terminator_ch), - switch (self.command) { - .kitty_color_protocol => |*c| c.terminator = .init(terminator_ch), - .color_operation => |*c| c.terminator = .init(terminator_ch), - else => {}, - } + .@"8" => parsers.hyperlink.parse(self, terminator_ch), - return &self.command; + .@"9" => parsers.osc9.parse(self, terminator_ch), + + .@"21" => parsers.kitty_color.parse(self, terminator_ch), + + .@"22" => parsers.mouse_shape.parse(self, terminator_ch), + + .@"52" => parsers.clipboard_operation.parse(self, terminator_ch), + + .@"6" => null, + + .@"66" => parsers.kitty_text_sizing.parse(self, terminator_ch), + + .@"77" => null, + + .@"133" => parsers.semantic_prompt.parse(self, terminator_ch), + + .@"777" => parsers.rxvt_extension.parse(self, terminator_ch), + + .@"1337" => parsers.iterm2.parse(self, terminator_ch), + }; } }; test { - _ = osc_color; -} - -test "OSC 0: change_window_title" { - const testing = std.testing; - - var p: Parser = .init(null); - p.next('0'); - p.next(';'); - p.next('a'); - p.next('b'); - const cmd = p.end(null).?.*; - try testing.expect(cmd == .change_window_title); - try testing.expectEqualStrings("ab", cmd.change_window_title); -} - -test "OSC 0: longer than buffer" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "0;" ++ "a" ** (Parser.MAX_BUF + 2); - for (input) |ch| p.next(ch); - - try testing.expect(p.end(null) == null); - try testing.expect(p.complete == false); -} - -test "OSC 0: one shorter than buffer length" { - const testing = std.testing; - - var p: Parser = .init(null); - - const prefix = "0;"; - const title = "a" ** (Parser.MAX_BUF - prefix.len - 1); - const input = prefix ++ title; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .change_window_title); - try testing.expectEqualStrings(title, cmd.change_window_title); -} - -test "OSC 0: exactly at buffer length" { - const testing = std.testing; - - var p: Parser = .init(null); - - const prefix = "0;"; - const title = "a" ** (Parser.MAX_BUF - prefix.len); - const input = prefix ++ title; - for (input) |ch| p.next(ch); - - // This should be null because we always reserve space for a null terminator. - try testing.expect(p.end(null) == null); - try testing.expect(p.complete == false); -} - -test "OSC 1: change_window_icon" { - const testing = std.testing; - - var p: Parser = .init(null); - p.next('1'); - p.next(';'); - p.next('a'); - p.next('b'); - const cmd = p.end(null).?.*; - try testing.expect(cmd == .change_window_icon); - try testing.expectEqualStrings("ab", cmd.change_window_icon); -} - -test "OSC 2: change_window_title with 2" { - const testing = std.testing; - - var p: Parser = .init(null); - p.next('2'); - p.next(';'); - p.next('a'); - p.next('b'); - const cmd = p.end(null).?.*; - try testing.expect(cmd == .change_window_title); - try testing.expectEqualStrings("ab", cmd.change_window_title); -} - -test "OSC 2: change_window_title with utf8" { - const testing = std.testing; - - var p: Parser = .init(null); - p.next('2'); - p.next(';'); - // '—' EM DASH U+2014 (E2 80 94) - p.next(0xE2); - p.next(0x80); - p.next(0x94); - - p.next(' '); - // '‐' HYPHEN U+2010 (E2 80 90) - // Intententionally chosen to conflict with the 0x90 C1 control - p.next(0xE2); - p.next(0x80); - p.next(0x90); - const cmd = p.end(null).?.*; - try testing.expect(cmd == .change_window_title); - try testing.expectEqualStrings("— ‐", cmd.change_window_title); -} - -test "OSC 2: change_window_title empty" { - const testing = std.testing; - - var p: Parser = .init(null); - p.next('2'); - p.next(';'); - const cmd = p.end(null).?.*; - try testing.expect(cmd == .change_window_title); - try testing.expectEqualStrings("", cmd.change_window_title); -} - -test "OSC 4: empty param" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "4;;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b'); - try testing.expect(cmd == null); -} - -// See src/terminal/osc/color.zig for more OSC 4 tests. - -// See src/terminal/osc/color.zig for OSC 5 tests. - -test "OSC 7: report pwd" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "7;file:///tmp/example"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .report_pwd); - try testing.expectEqualStrings("file:///tmp/example", cmd.report_pwd.value); -} - -test "OSC 7: report pwd empty" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "7;"; - for (input) |ch| p.next(ch); - const cmd = p.end(null).?.*; - try testing.expect(cmd == .report_pwd); - try testing.expectEqualStrings("", cmd.report_pwd.value); -} - -test "OSC 8: hyperlink" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "8;;http://example.com"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .hyperlink_start); - try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); -} - -test "OSC 8: hyperlink with id set" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "8;id=foo;http://example.com"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .hyperlink_start); - try testing.expectEqualStrings(cmd.hyperlink_start.id.?, "foo"); - try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); -} - -test "OSC 8: hyperlink with empty id" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "8;id=;http://example.com"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .hyperlink_start); - try testing.expectEqual(null, cmd.hyperlink_start.id); - try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); -} - -test "OSC 8: hyperlink with incomplete key" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "8;id;http://example.com"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .hyperlink_start); - try testing.expectEqual(null, cmd.hyperlink_start.id); - try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); -} - -test "OSC 8: hyperlink with empty key" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "8;=value;http://example.com"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .hyperlink_start); - try testing.expectEqual(null, cmd.hyperlink_start.id); - try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); -} - -test "OSC 8: hyperlink with empty key and id" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "8;=value:id=foo;http://example.com"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .hyperlink_start); - try testing.expectEqualStrings(cmd.hyperlink_start.id.?, "foo"); - try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); -} - -test "OSC 8: hyperlink with empty uri" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "8;id=foo;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b'); - try testing.expect(cmd == null); -} - -test "OSC 8: hyperlink end" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "8;;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .hyperlink_end); -} - -test "OSC 9: show desktop notification" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;Hello world"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("", cmd.show_desktop_notification.title); - try testing.expectEqualStrings("Hello world", cmd.show_desktop_notification.body); -} - -test "OSC 9: show single character desktop notification" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;H"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("", cmd.show_desktop_notification.title); - try testing.expectEqualStrings("H", cmd.show_desktop_notification.body); -} - -test "OSC 9;1: ConEmu sleep" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;1;420"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .conemu_sleep); - try testing.expectEqual(420, cmd.conemu_sleep.duration_ms); -} - -test "OSC 9;1: ConEmu sleep with no value default to 100ms" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;1;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .conemu_sleep); - try testing.expectEqual(100, cmd.conemu_sleep.duration_ms); -} - -test "OSC 9;1: conemu sleep cannot exceed 10000ms" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;1;12345"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .conemu_sleep); - try testing.expectEqual(10000, cmd.conemu_sleep.duration_ms); -} - -test "OSC 9;1: conemu sleep invalid input" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;1;foo"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .conemu_sleep); - try testing.expectEqual(100, cmd.conemu_sleep.duration_ms); -} - -test "OSC 9;1: conemu sleep -> desktop notification 1" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;1"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("1", cmd.show_desktop_notification.body); -} - -test "OSC 9;1: conemu sleep -> desktop notification 2" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;1a"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("1a", cmd.show_desktop_notification.body); -} - -test "OSC 9;2: ConEmu message box" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;2;hello world"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_show_message_box); - try testing.expectEqualStrings("hello world", cmd.conemu_show_message_box); -} - -test "OSC 9;2: ConEmu message box invalid input" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;2"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("2", cmd.show_desktop_notification.body); -} - -test "OSC 9;2: ConEmu message box empty message" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;2;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_show_message_box); - try testing.expectEqualStrings("", cmd.conemu_show_message_box); -} - -test "OSC 9;2: ConEmu message box spaces only message" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;2; "; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_show_message_box); - try testing.expectEqualStrings(" ", cmd.conemu_show_message_box); -} - -test "OSC 9;2: message box -> desktop notification 1" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;2"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("2", cmd.show_desktop_notification.body); -} - -test "OSC 9;2: message box -> desktop notification 2" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;2a"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("2a", cmd.show_desktop_notification.body); -} - -test "OSC 9;3: ConEmu change tab title" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;3;foo bar"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_change_tab_title); - try testing.expectEqualStrings("foo bar", cmd.conemu_change_tab_title.value); -} - -test "OSC 9;3: ConEmu change tab title reset" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;3;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - const expected_command: Command = .{ .conemu_change_tab_title = .reset }; - try testing.expectEqual(expected_command, cmd); -} - -test "OSC 9;3: ConEmu change tab title spaces only" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;3; "; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .conemu_change_tab_title); - try testing.expectEqualStrings(" ", cmd.conemu_change_tab_title.value); -} - -test "OSC 9;3: change tab title -> desktop notification 1" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;3"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("3", cmd.show_desktop_notification.body); -} - -test "OSC 9;3: message box -> desktop notification 2" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;3a"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("3a", cmd.show_desktop_notification.body); -} - -test "OSC 9;4: ConEmu progress set" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;4;1;100"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .set); - try testing.expect(cmd.conemu_progress_report.progress == 100); -} - -test "OSC 9;4: ConEmu progress set overflow" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;4;1;900"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .set); - try testing.expectEqual(100, cmd.conemu_progress_report.progress); -} - -test "OSC 9;4: ConEmu progress set single digit" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;4;1;9"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .set); - try testing.expect(cmd.conemu_progress_report.progress == 9); -} - -test "OSC 9;4: ConEmu progress set double digit" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;4;1;94"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .set); - try testing.expectEqual(94, cmd.conemu_progress_report.progress); -} - -test "OSC 9;4: ConEmu progress set extra semicolon ignored" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;4;1;100"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .set); - try testing.expectEqual(100, cmd.conemu_progress_report.progress); -} - -test "OSC 9;4: ConEmu progress remove with no progress" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;4;0;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .remove); - try testing.expect(cmd.conemu_progress_report.progress == null); -} - -test "OSC 9;4: ConEmu progress remove with double semicolon" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;4;0;;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .remove); - try testing.expect(cmd.conemu_progress_report.progress == null); -} - -test "OSC 9;4: ConEmu progress remove ignores progress" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;4;0;100"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .remove); - try testing.expect(cmd.conemu_progress_report.progress == null); -} - -test "OSC 9;4: ConEmu progress remove extra semicolon" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;4;0;100;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .remove); -} - -test "OSC 9;4: ConEmu progress error" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;4;2"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .@"error"); - try testing.expect(cmd.conemu_progress_report.progress == null); -} - -test "OSC 9;4: ConEmu progress error with progress" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;4;2;100"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .@"error"); - try testing.expect(cmd.conemu_progress_report.progress == 100); -} - -test "OSC 9;4: progress pause" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;4;4"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .pause); - try testing.expect(cmd.conemu_progress_report.progress == null); -} - -test "OSC 9;4: ConEmu progress pause with progress" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;4;4;100"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_progress_report); - try testing.expect(cmd.conemu_progress_report.state == .pause); - try testing.expect(cmd.conemu_progress_report.progress == 100); -} - -test "OSC 9;4: progress -> desktop notification 1" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;4"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("4", cmd.show_desktop_notification.body); -} - -test "OSC 9;4: progress -> desktop notification 2" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;4;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("4;", cmd.show_desktop_notification.body); -} - -test "OSC 9;4: progress -> desktop notification 3" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;4;5"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("4;5", cmd.show_desktop_notification.body); -} - -test "OSC 9;4: progress -> desktop notification 4" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;4;5a"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("4;5a", cmd.show_desktop_notification.body); -} - -test "OSC 9;5: ConEmu wait input" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;5"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_wait_input); -} - -test "OSC 9;5: ConEmu wait ignores trailing characters" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "9;5;foo"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_wait_input); -} - -test "OSC 9;6: ConEmu guimacro 1" { - const testing = std.testing; - - var p: Parser = .init(testing.allocator); - defer p.deinit(); - - const input = "9;6;a"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_guimacro); - try testing.expectEqualStrings("a", cmd.conemu_guimacro); -} - -test "OSC: 9;6: ConEmu guimacro 2" { - const testing = std.testing; - - var p: Parser = .init(testing.allocator); - defer p.deinit(); - - const input = "9;6;ab"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .conemu_guimacro); - try testing.expectEqualStrings("ab", cmd.conemu_guimacro); -} - -test "OSC: 9;6: ConEmu guimacro 3 incomplete -> desktop notification" { - const testing = std.testing; - - var p: Parser = .init(testing.allocator); - defer p.deinit(); - - const input = "9;6"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings("6", cmd.show_desktop_notification.body); -} - -// See src/terminal/osc/color.zig for OSC 10 tests. - -// See src/terminal/osc/color.zig for OSC 11 tests. - -// See src/terminal/osc/color.zig for OSC 12 tests. - -// See src/terminal/osc/color.zig for OSC 13 tests. - -// See src/terminal/osc/color.zig for OSC 14 tests. - -// See src/terminal/osc/color.zig for OSC 15 tests. - -// See src/terminal/osc/color.zig for OSC 16 tests. - -// See src/terminal/osc/color.zig for OSC 17 tests. - -// See src/terminal/osc/color.zig for OSC 18 tests. - -// See src/terminal/osc/color.zig for OSC 19 tests. - -test "OSC 21: kitty color protocol" { - const testing = std.testing; - const Kind = kitty_color.Kind; - - var p: Parser = .init(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"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .kitty_color_protocol); - try testing.expectEqual(@as(usize, 9), cmd.kitty_color_protocol.list.items.len); - { - const item = cmd.kitty_color_protocol.list.items[0]; - try testing.expect(item == .query); - try testing.expectEqual(Kind{ .special = .foreground }, item.query); - } - { - const item = cmd.kitty_color_protocol.list.items[1]; - try testing.expect(item == .set); - try testing.expectEqual(Kind{ .special = .background }, item.set.key); - try testing.expectEqual(@as(u8, 0xf0), item.set.color.r); - try testing.expectEqual(@as(u8, 0xf8), item.set.color.g); - try testing.expectEqual(@as(u8, 0xff), item.set.color.b); - } - { - const item = cmd.kitty_color_protocol.list.items[2]; - try testing.expect(item == .set); - try testing.expectEqual(Kind{ .special = .cursor }, item.set.key); - try testing.expectEqual(@as(u8, 0xf0), item.set.color.r); - try testing.expectEqual(@as(u8, 0xf8), item.set.color.g); - try testing.expectEqual(@as(u8, 0xff), item.set.color.b); - } - { - const item = cmd.kitty_color_protocol.list.items[3]; - try testing.expect(item == .reset); - try testing.expectEqual(Kind{ .special = .cursor_text }, item.reset); - } - { - const item = cmd.kitty_color_protocol.list.items[4]; - try testing.expect(item == .reset); - try testing.expectEqual(Kind{ .special = .visual_bell }, item.reset); - } - { - const item = cmd.kitty_color_protocol.list.items[5]; - try testing.expect(item == .query); - try testing.expectEqual(Kind{ .special = .selection_background }, item.query); - } - { - const item = cmd.kitty_color_protocol.list.items[6]; - try testing.expect(item == .set); - try testing.expectEqual(Kind{ .special = .selection_background }, item.set.key); - try testing.expectEqual(@as(u8, 0xaa), item.set.color.r); - try testing.expectEqual(@as(u8, 0xbb), item.set.color.g); - try testing.expectEqual(@as(u8, 0xcc), item.set.color.b); - } - { - const item = cmd.kitty_color_protocol.list.items[7]; - try testing.expect(item == .query); - try testing.expectEqual(Kind{ .palette = 2 }, item.query); - } - { - const item = cmd.kitty_color_protocol.list.items[8]; - try testing.expect(item == .set); - try testing.expectEqual(Kind{ .palette = 3 }, item.set.key); - try testing.expectEqual(@as(u8, 0xff), item.set.color.r); - try testing.expectEqual(@as(u8, 0xff), item.set.color.g); - try testing.expectEqual(@as(u8, 0xff), item.set.color.b); - } -} - -test "OSC 21: kitty color protocol without allocator" { - const testing = std.testing; - - var p: Parser = .init(null); - defer p.deinit(); - - const input = "21;foreground=?"; - for (input) |ch| p.next(ch); - try testing.expect(p.end('\x1b') == null); -} - -test "OSC 21: kitty color protocol double reset" { - const testing = std.testing; - - var p: Parser = .init(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"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .kitty_color_protocol); - - p.reset(); - p.reset(); -} - -test "OSC 21: kitty color protocol reset after invalid" { - const testing = std.testing; - - var p: Parser = .init(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"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .kitty_color_protocol); - - p.reset(); - - try testing.expectEqual(Parser.State.empty, p.state); - p.next('X'); - try testing.expectEqual(Parser.State.invalid, p.state); - - p.reset(); -} - -test "OSC 21: kitty color protocol no key" { - const testing = std.testing; - - var p: Parser = .init(testing.allocator); - defer p.deinit(); - - const input = "21;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .kitty_color_protocol); - try testing.expectEqual(0, cmd.kitty_color_protocol.list.items.len); -} - -test "OSC 22: pointer cursor" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "22;pointer"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .mouse_shape); - try testing.expectEqualStrings("pointer", cmd.mouse_shape.value); -} - -test "OSC 52: get/set clipboard" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "52;s;?"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .clipboard_contents); - try testing.expect(cmd.clipboard_contents.kind == 's'); - try testing.expectEqualStrings("?", cmd.clipboard_contents.data); -} - -test "OSC 52: get/set clipboard (optional parameter)" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "52;;?"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .clipboard_contents); - try testing.expect(cmd.clipboard_contents.kind == 'c'); - try testing.expectEqualStrings("?", cmd.clipboard_contents.data); -} - -test "OSC 52: get/set clipboard with allocator" { - const testing = std.testing; - - var p: Parser = .init(testing.allocator); - defer p.deinit(); - - const input = "52;s;?"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .clipboard_contents); - try testing.expect(cmd.clipboard_contents.kind == 's'); - try testing.expectEqualStrings("?", cmd.clipboard_contents.data); -} - -test "OSC 52: clear clipboard" { - const testing = std.testing; - - var p: Parser = .init(null); - defer p.deinit(); - - const input = "52;;"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .clipboard_contents); - try testing.expect(cmd.clipboard_contents.kind == 'c'); - try testing.expectEqualStrings("", cmd.clipboard_contents.data); -} - -// See src/terminal/osc/color.zig for OSC 104 tests. - -// See src/terminal/osc/color.zig for OSC 105 tests. - -// See src/terminal/osc/color.zig for OSC 110 tests. - -// See src/terminal/osc/color.zig for OSC 111 tests. - -// See src/terminal/osc/color.zig for OSC 112 tests. - -// See src/terminal/osc/color.zig for OSC 113 tests. - -// See src/terminal/osc/color.zig for OSC 114 tests. - -// See src/terminal/osc/color.zig for OSC 115 tests. - -// See src/terminal/osc/color.zig for OSC 116 tests. - -// See src/terminal/osc/color.zig for OSC 117 tests. - -// See src/terminal/osc/color.zig for OSC 118 tests. - -// See src/terminal/osc/color.zig for OSC 119 tests. - -test "OSC 133: prompt_start" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.aid == null); - try testing.expect(cmd.prompt_start.redraw); -} - -test "OSC 133: prompt_start with single option" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;aid=14"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expectEqualStrings("14", cmd.prompt_start.aid.?); -} - -test "OSC 133: prompt_start with redraw disabled" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;redraw=0"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(!cmd.prompt_start.redraw); -} - -test "OSC 133: prompt_start with redraw invalid value" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;redraw=42"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.redraw); - try testing.expect(cmd.prompt_start.kind == .primary); -} - -test "OSC 133: prompt_start with continuation" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;k=c"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.kind == .continuation); -} - -test "OSC 133: prompt_start with secondary" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;k=s"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.kind == .secondary); -} - -test "OSC 133: prompt_start with special_key" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;special_key=1"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.special_key == true); -} - -test "OSC 133: prompt_start with special_key invalid" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;special_key=bobr"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.special_key == false); -} - -test "OSC 133: prompt_start with special_key 0" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;special_key=0"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.special_key == false); -} - -test "OSC 133: prompt_start with special_key empty" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;special_key="; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.special_key == false); -} - -test "OSC 133: prompt_start with click_events true" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;click_events=1"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.click_events == true); -} - -test "OSC 133: prompt_start with click_events false" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;click_events=0"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.click_events == false); -} - -test "OSC 133: prompt_start with click_events empty" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;click_events="; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expect(cmd.prompt_start.click_events == false); -} - -test "OSC 133: end_of_command no exit code" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;D"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_command); -} - -test "OSC 133: end_of_command with exit code" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;D;25"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_command); - try testing.expectEqual(@as(u8, 25), cmd.end_of_command.exit_code.?); -} - -test "OSC 133: prompt_end" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;B"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_end); -} - -test "OSC 133: end_of_input" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); -} - -test "OSC 133: end_of_input with cmdline 1" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C;cmdline=echo bobr kurwa"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline 2" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C;cmdline=echo bobr\\ kurwa"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline 3" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C;cmdline=echo bobr\\nkurwa"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("echo bobr\nkurwa", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline 4" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C;cmdline=$'echo bobr kurwa'"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline 5" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C;cmdline='echo bobr kurwa'"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline 6" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C;cmdline='echo bobr kurwa"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline == null); -} - -test "OSC 133: end_of_input with cmdline 7" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C;cmdline=$'echo bobr kurwa"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline == null); -} - -test "OSC 133: end_of_input with cmdline 8" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C;cmdline=$'"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline == null); -} - -test "OSC 133: end_of_input with cmdline 9" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C;cmdline=$'"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline == null); -} - -test "OSC 133: end_of_input with cmdline 10" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C;cmdline="; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline_url 1" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C;cmdline_url=echo bobr kurwa"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline_url 2" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C;cmdline_url=echo bobr%20kurwa"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("echo bobr kurwa", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline_url 3" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C;cmdline_url=echo bobr%3bkurwa"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("echo bobr;kurwa", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline_url 4" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C;cmdline_url=echo bobr%3kurwa"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline == null); -} - -test "OSC 133: end_of_input with cmdline_url 5" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C;cmdline_url=echo bobr%kurwa"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline == null); -} - -test "OSC 133: end_of_input with cmdline_url 6" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C;cmdline_url=echo bobr%kurwa"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline == null); -} - -test "OSC 133: end_of_input with cmdline_url 7" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C;cmdline_url=echo bobr kurwa%20"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline != null); - try testing.expectEqualStrings("echo bobr kurwa ", cmd.end_of_input.cmdline.?); -} - -test "OSC 133: end_of_input with cmdline_url 8" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C;cmdline_url=echo bobr kurwa%2"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline == null); -} - -test "OSC 133: end_of_input with cmdline_url 9" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;C;cmdline_url=echo bobr kurwa%2"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); - try testing.expect(cmd.end_of_input.cmdline == null); -} - -test "OSC: OSC 777 show desktop notification with title" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "777;notify;Title;Body"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings(cmd.show_desktop_notification.title, "Title"); - try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Body"); + _ = parsers; + _ = encoding; } diff --git a/src/terminal/osc/encoding.zig b/src/terminal/osc/encoding.zig new file mode 100644 index 000000000..7491d10c2 --- /dev/null +++ b/src/terminal/osc/encoding.zig @@ -0,0 +1,38 @@ +//! Specialized encodings used in some OSC protocols. +const std = @import("std"); + +/// Kitty defines "Escape code safe UTF-8" as valid UTF-8 with the +/// additional requirement of not containing any C0 escape codes +/// (0x00-0x1f), DEL (0x7f) and C1 escape codes (0x80-0x9f). +/// +/// Used by OSC 66 (text sizing) and OSC 99 (Kitty notifications). +/// +/// See: https://sw.kovidgoyal.net/kitty/desktop-notifications/#safe-utf8 +pub fn isSafeUtf8(s: []const u8) bool { + const utf8 = std.unicode.Utf8View.init(s) catch { + @branchHint(.cold); + return false; + }; + + var it = utf8.iterator(); + while (it.nextCodepoint()) |cp| switch (cp) { + 0x00...0x1f, 0x7f, 0x80...0x9f => { + @branchHint(.cold); + return false; + }, + else => {}, + }; + + return true; +} + +test isSafeUtf8 { + const testing = std.testing; + + try testing.expect(isSafeUtf8("Hello world!")); + try testing.expect(isSafeUtf8("安全的ユニコード☀️")); + try testing.expect(!isSafeUtf8("No linebreaks\nallowed")); + try testing.expect(!isSafeUtf8("\x07no bells")); + try testing.expect(!isSafeUtf8("\x1b]9;no OSCs\x1b\\\x1b[m")); + try testing.expect(!isSafeUtf8("\x9f8-bit escapes are clever, but no")); +} diff --git a/src/terminal/osc/parsers.zig b/src/terminal/osc/parsers.zig new file mode 100644 index 000000000..fb84785f2 --- /dev/null +++ b/src/terminal/osc/parsers.zig @@ -0,0 +1,19 @@ +const std = @import("std"); + +pub const change_window_icon = @import("parsers/change_window_icon.zig"); +pub const change_window_title = @import("parsers/change_window_title.zig"); +pub const clipboard_operation = @import("parsers/clipboard_operation.zig"); +pub const color = @import("parsers/color.zig"); +pub const hyperlink = @import("parsers/hyperlink.zig"); +pub const iterm2 = @import("parsers/iterm2.zig"); +pub const kitty_color = @import("parsers/kitty_color.zig"); +pub const kitty_text_sizing = @import("parsers/kitty_text_sizing.zig"); +pub const mouse_shape = @import("parsers/mouse_shape.zig"); +pub const osc9 = @import("parsers/osc9.zig"); +pub const report_pwd = @import("parsers/report_pwd.zig"); +pub const rxvt_extension = @import("parsers/rxvt_extension.zig"); +pub const semantic_prompt = @import("parsers/semantic_prompt.zig"); + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/terminal/osc/parsers/change_window_icon.zig b/src/terminal/osc/parsers/change_window_icon.zig new file mode 100644 index 000000000..aefe17696 --- /dev/null +++ b/src/terminal/osc/parsers/change_window_icon.zig @@ -0,0 +1,33 @@ +const std = @import("std"); +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +/// Parse OSC 1 +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + parser.command = .{ + .change_window_icon = data[0 .. data.len - 1 :0], + }; + return &parser.command; +} + +test "OSC 1: change_window_icon" { + const testing = std.testing; + + var p: Parser = .init(null); + p.next('1'); + p.next(';'); + p.next('a'); + p.next('b'); + const cmd = p.end(null).?.*; + try testing.expect(cmd == .change_window_icon); + try testing.expectEqualStrings("ab", cmd.change_window_icon); +} diff --git a/src/terminal/osc/parsers/change_window_title.zig b/src/terminal/osc/parsers/change_window_title.zig new file mode 100644 index 000000000..b0bf44dd3 --- /dev/null +++ b/src/terminal/osc/parsers/change_window_title.zig @@ -0,0 +1,119 @@ +const std = @import("std"); + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +/// Parse OSC 0 and OSC 2 +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + parser.command = .{ + .change_window_title = data[0 .. data.len - 1 :0], + }; + return &parser.command; +} + +test "OSC 0: change_window_title" { + const testing = std.testing; + + var p: Parser = .init(null); + p.next('0'); + p.next(';'); + p.next('a'); + p.next('b'); + const cmd = p.end(null).?.*; + try testing.expect(cmd == .change_window_title); + try testing.expectEqualStrings("ab", cmd.change_window_title); +} + +test "OSC 0: longer than buffer" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "0;" ++ "a" ** (Parser.MAX_BUF + 2); + for (input) |ch| p.next(ch); + + try testing.expect(p.end(null) == null); +} + +test "OSC 0: one shorter than buffer length" { + const testing = std.testing; + + var p: Parser = .init(null); + + const prefix = "0;"; + const title = "a" ** (Parser.MAX_BUF - 1); + const input = prefix ++ title; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .change_window_title); + try testing.expectEqualStrings(title, cmd.change_window_title); +} + +test "OSC 0: exactly at buffer length" { + const testing = std.testing; + + var p: Parser = .init(null); + + const prefix = "0;"; + const title = "a" ** Parser.MAX_BUF; + const input = prefix ++ title; + for (input) |ch| p.next(ch); + + // This should be null because we always reserve space for a null terminator. + try testing.expect(p.end(null) == null); +} +test "OSC 2: change_window_title with 2" { + const testing = std.testing; + + var p: Parser = .init(null); + p.next('2'); + p.next(';'); + p.next('a'); + p.next('b'); + const cmd = p.end(null).?.*; + try testing.expect(cmd == .change_window_title); + try testing.expectEqualStrings("ab", cmd.change_window_title); +} + +test "OSC 2: change_window_title with utf8" { + const testing = std.testing; + + var p: Parser = .init(null); + p.next('2'); + p.next(';'); + // '—' EM DASH U+2014 (E2 80 94) + p.next(0xE2); + p.next(0x80); + p.next(0x94); + + p.next(' '); + // '‐' HYPHEN U+2010 (E2 80 90) + // Intententionally chosen to conflict with the 0x90 C1 control + p.next(0xE2); + p.next(0x80); + p.next(0x90); + const cmd = p.end(null).?.*; + try testing.expect(cmd == .change_window_title); + try testing.expectEqualStrings("— ‐", cmd.change_window_title); +} + +test "OSC 2: change_window_title empty" { + const testing = std.testing; + + var p: Parser = .init(null); + p.next('2'); + p.next(';'); + const cmd = p.end(null).?.*; + try testing.expect(cmd == .change_window_title); + try testing.expectEqualStrings("", cmd.change_window_title); +} diff --git a/src/terminal/osc/parsers/clipboard_operation.zig b/src/terminal/osc/parsers/clipboard_operation.zig new file mode 100644 index 000000000..59a8831bc --- /dev/null +++ b/src/terminal/osc/parsers/clipboard_operation.zig @@ -0,0 +1,106 @@ +const std = @import("std"); + +const assert = @import("../../../quirks.zig").inlineAssert; + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +/// Parse OSC 52 +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + assert(parser.state == .@"52"); + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + if (data.len == 1) { + parser.state = .invalid; + return null; + } + if (data[0] == ';') { + parser.command = .{ + .clipboard_contents = .{ + .kind = 'c', + .data = data[1 .. data.len - 1 :0], + }, + }; + } else { + if (data.len < 2) { + parser.state = .invalid; + return null; + } + if (data[1] != ';') { + parser.state = .invalid; + return null; + } + parser.command = .{ + .clipboard_contents = .{ + .kind = data[0], + .data = data[2 .. data.len - 1 :0], + }, + }; + } + return &parser.command; +} + +test "OSC 52: get/set clipboard" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "52;s;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .clipboard_contents); + try testing.expect(cmd.clipboard_contents.kind == 's'); + try testing.expectEqualStrings("?", cmd.clipboard_contents.data); +} + +test "OSC 52: get/set clipboard (optional parameter)" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "52;;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .clipboard_contents); + try testing.expect(cmd.clipboard_contents.kind == 'c'); + try testing.expectEqualStrings("?", cmd.clipboard_contents.data); +} + +test "OSC 52: get/set clipboard with allocator" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "52;s;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .clipboard_contents); + try testing.expect(cmd.clipboard_contents.kind == 's'); + try testing.expectEqualStrings("?", cmd.clipboard_contents.data); +} + +test "OSC 52: clear clipboard" { + const testing = std.testing; + + var p: Parser = .init(null); + defer p.deinit(); + + const input = "52;;"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .clipboard_contents); + try testing.expect(cmd.clipboard_contents.kind == 'c'); + try testing.expectEqualStrings("", cmd.clipboard_contents.data); +} diff --git a/src/terminal/osc/color.zig b/src/terminal/osc/parsers/color.zig similarity index 86% rename from src/terminal/osc/color.zig rename to src/terminal/osc/parsers/color.zig index 9fd81ed63..7d3dc68c0 100644 --- a/src/terminal/osc/color.zig +++ b/src/terminal/osc/parsers/color.zig @@ -1,10 +1,15 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const DynamicColor = @import("../color.zig").Dynamic; -const SpecialColor = @import("../color.zig").Special; -const RGB = @import("../color.zig").RGB; -pub const ParseError = Allocator.Error || error{ +const DynamicColor = @import("../../color.zig").Dynamic; +const SpecialColor = @import("../../color.zig").Special; +const RGB = @import("../../color.zig").RGB; +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +const log = std.log.scoped(.osc_color); + +const ParseError = Allocator.Error || error{ MissingOperation, }; @@ -36,6 +41,76 @@ pub const Operation = enum { osc_119, }; +/// Parse OSCs 4, 5, 10-19, 104, 110-119 +pub fn parse(parser: *Parser, terminator_ch: ?u8) ?*Command { + const alloc = parser.alloc orelse { + parser.state = .invalid; + return null; + }; + // If we've collected any extra data parse that, otherwise use an empty + // string. + const data = data: { + const writer = parser.writer orelse break :data ""; + break :data writer.buffered(); + }; + // Check and make sure that we're parsing the correct OSCs + const op: Operation = switch (parser.state) { + .@"4" => .osc_4, + .@"5" => .osc_5, + .@"10" => .osc_10, + .@"11" => .osc_11, + .@"12" => .osc_12, + .@"13" => .osc_13, + .@"14" => .osc_14, + .@"15" => .osc_15, + .@"16" => .osc_16, + .@"17" => .osc_17, + .@"18" => .osc_18, + .@"19" => .osc_19, + .@"104" => .osc_104, + .@"110" => .osc_110, + .@"111" => .osc_111, + .@"112" => .osc_112, + .@"113" => .osc_113, + .@"114" => .osc_114, + .@"115" => .osc_115, + .@"116" => .osc_116, + .@"117" => .osc_117, + .@"118" => .osc_118, + .@"119" => .osc_119, + else => { + parser.state = .invalid; + return null; + }, + }; + parser.command = .{ + .color_operation = .{ + .op = op, + .requests = parseColor(alloc, op, data) catch |err| list: { + log.info( + "failed to parse OSC {t} color request err={} data={s}", + .{ parser.state, err, data }, + ); + break :list .{}; + }, + .terminator = .init(terminator_ch), + }, + }; + return &parser.command; +} + +test "OSC 4: empty param" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "4;;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b'); + try testing.expect(cmd == null); +} + /// Parse any color operation string. This should NOT include the operation /// itself, but only the body of the operation. e.g. for "4;a;b;c" the body /// should be "a;b;c" and the operation should be set accordingly. @@ -46,7 +121,7 @@ pub const Operation = enum { /// request) but grants us an easier to understand and testable implementation. /// /// If color changing ends up being a bottleneck we can optimize this later. -pub fn parse( +fn parseColor( alloc: Allocator, op: Operation, buf: []const u8, @@ -295,7 +370,7 @@ test "OSC 4:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_4, body); + var list = try parseColor(alloc, .osc_4, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -317,7 +392,7 @@ test "OSC 4:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_4, body); + var list = try parseColor(alloc, .osc_4, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -336,7 +411,7 @@ test "OSC 4:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_4, body); + var list = try parseColor(alloc, .osc_4, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -360,7 +435,7 @@ test "OSC 4:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_4, body); + var list = try parseColor(alloc, .osc_4, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -387,7 +462,7 @@ test "OSC 4:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_4, body); + var list = try parseColor(alloc, .osc_4, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -419,7 +494,7 @@ test "OSC 5:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_5, body); + var list = try parseColor(alloc, .osc_5, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -439,7 +514,7 @@ test "OSC 4: multiple requests" { // printf '\e]4;0;red;1;blue\e\\' { - var list = try parse( + var list = try parseColor( alloc, .osc_4, "0;red;1;blue", @@ -465,7 +540,7 @@ test "OSC 4: multiple requests" { // Multiple requests with same index overwrite each other // printf '\e]4;0;red;0;blue\e\\' { - var list = try parse( + var list = try parseColor( alloc, .osc_4, "0;red;0;blue", @@ -505,7 +580,7 @@ test "OSC 104:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_104, body); + var list = try parseColor(alloc, .osc_104, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -529,7 +604,7 @@ test "OSC 104:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_104, body); + var list = try parseColor(alloc, .osc_104, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -544,7 +619,7 @@ test "OSC 104: empty index" { const testing = std.testing; const alloc = testing.allocator; - var list = try parse(alloc, .osc_104, "0;;1"); + var list = try parseColor(alloc, .osc_104, "0;;1"); defer list.deinit(alloc); try testing.expectEqual(2, list.count()); try testing.expectEqual( @@ -561,7 +636,7 @@ test "OSC 104: invalid index" { const testing = std.testing; const alloc = testing.allocator; - var list = try parse(alloc, .osc_104, "ffff;1"); + var list = try parseColor(alloc, .osc_104, "ffff;1"); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -574,7 +649,7 @@ test "OSC 104: reset all" { const testing = std.testing; const alloc = testing.allocator; - var list = try parse(alloc, .osc_104, ""); + var list = try parseColor(alloc, .osc_104, ""); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -587,7 +662,7 @@ test "OSC 105: reset all" { const testing = std.testing; const alloc = testing.allocator; - var list = try parse(alloc, .osc_105, ""); + var list = try parseColor(alloc, .osc_105, ""); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -611,7 +686,7 @@ test "OSC 10: OSC 11: OSC 12: OSC: 13: OSC 14: OSC 15: OSC: 16: OSC 17: OSC 18: // Example script: // printf '\e]10;red\e\\' { - var list = try parse(alloc, op, "red"); + var list = try parseColor(alloc, op, "red"); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -632,7 +707,7 @@ test "OSC 10: OSC 11: OSC 12: OSC: 13: OSC 14: OSC 15: OSC: 16: OSC 17: OSC 18: // Example script: // printf '\e]11;red;blue\e\\' { - var list = try parse( + var list = try parseColor( alloc, .osc_11, "red;blue", @@ -671,7 +746,7 @@ test "OSC 110: OSC 111: OSC 112: OSC: 113: OSC 114: OSC 115: OSC: 116: OSC 117: // Example script: // printf '\e]110\e\\' { - var list = try parse(alloc, op, ""); + var list = try parseColor(alloc, op, ""); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -684,7 +759,7 @@ test "OSC 110: OSC 111: OSC 112: OSC: 113: OSC 114: OSC 115: OSC: 116: OSC 117: // // printf '\e]110;\e\\' { - var list = try parse(alloc, op, ";"); + var list = try parseColor(alloc, op, ";"); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -697,7 +772,7 @@ test "OSC 110: OSC 111: OSC 112: OSC: 113: OSC 114: OSC 115: OSC: 116: OSC 117: // // printf '\e]110 \e\\' { - var list = try parse(alloc, op, " "); + var list = try parseColor(alloc, op, " "); defer list.deinit(alloc); try testing.expectEqual(0, list.count()); } diff --git a/src/terminal/osc/parsers/hyperlink.zig b/src/terminal/osc/parsers/hyperlink.zig new file mode 100644 index 000000000..cf328beb5 --- /dev/null +++ b/src/terminal/osc/parsers/hyperlink.zig @@ -0,0 +1,164 @@ +const std = @import("std"); + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +const log = std.log.scoped(.osc_hyperlink); + +/// Parse OSC 8 hyperlinks +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + const s = std.mem.indexOfScalar(u8, data, ';') orelse { + parser.state = .invalid; + return null; + }; + + parser.command = .{ + .hyperlink_start = .{ + .uri = data[s + 1 .. data.len - 1 :0], + }, + }; + + data[s] = 0; + const kvs = data[0 .. s + 1]; + std.mem.replaceScalar(u8, kvs, ':', 0); + var kv_start: usize = 0; + while (kv_start < kvs.len) { + const kv_end = std.mem.indexOfScalarPos(u8, kvs, kv_start + 1, 0) orelse break; + const kv = data[kv_start .. kv_end + 1]; + const v = std.mem.indexOfScalar(u8, kv, '=') orelse break; + const key = kv[0..v]; + const value = kv[v + 1 .. kv.len - 1 :0]; + if (std.mem.eql(u8, key, "id")) { + if (value.len > 0) parser.command.hyperlink_start.id = value; + } else { + log.warn("unknown hyperlink option: '{s}'", .{key}); + } + kv_start = kv_end + 1; + } + + if (parser.command.hyperlink_start.uri.len == 0) { + if (parser.command.hyperlink_start.id != null) { + parser.state = .invalid; + return null; + } + parser.command = .hyperlink_end; + } + + return &parser.command; +} + +test "OSC 8: hyperlink" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "8;;http://example.com"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .hyperlink_start); + try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); +} + +test "OSC 8: hyperlink with id set" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "8;id=foo;http://example.com"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .hyperlink_start); + try testing.expectEqualStrings(cmd.hyperlink_start.id.?, "foo"); + try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); +} + +test "OSC 8: hyperlink with empty id" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "8;id=;http://example.com"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .hyperlink_start); + try testing.expectEqual(null, cmd.hyperlink_start.id); + try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); +} + +test "OSC 8: hyperlink with incomplete key" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "8;id;http://example.com"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .hyperlink_start); + try testing.expectEqual(null, cmd.hyperlink_start.id); + try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); +} + +test "OSC 8: hyperlink with empty key" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "8;=value;http://example.com"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .hyperlink_start); + try testing.expectEqual(null, cmd.hyperlink_start.id); + try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); +} + +test "OSC 8: hyperlink with empty key and id" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "8;=value:id=foo;http://example.com"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .hyperlink_start); + try testing.expectEqualStrings(cmd.hyperlink_start.id.?, "foo"); + try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); +} + +test "OSC 8: hyperlink with empty uri" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "8;id=foo;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b'); + try testing.expect(cmd == null); +} + +test "OSC 8: hyperlink end" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "8;;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .hyperlink_end); +} diff --git a/src/terminal/osc/parsers/iterm2.zig b/src/terminal/osc/parsers/iterm2.zig new file mode 100644 index 000000000..bd64977cf --- /dev/null +++ b/src/terminal/osc/parsers/iterm2.zig @@ -0,0 +1,435 @@ +const std = @import("std"); + +const assert = @import("../../../quirks.zig").inlineAssert; +const simd = @import("../../../simd/main.zig"); + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +const log = std.log.scoped(.osc_iterm2); + +const Key = enum { + AddAnnotation, + AddHiddenAnnotation, + Block, + Button, + ClearCapturedOutput, + ClearScrollback, + Copy, + CopyToClipboard, + CurrentDir, + CursorShape, + Custom, + Disinter, + EndCopy, + File, + FileEnd, + FilePart, + HighlightCursorLine, + MultipartFile, + OpenURL, + PopKeyLabels, + PushKeyLabels, + RemoteHost, + ReportCellSize, + ReportVariable, + RequestAttention, + RequestUpload, + SetBackgroundImageFile, + SetBadgeFormat, + SetColors, + SetKeyLabel, + SetMark, + SetProfile, + SetUserVar, + ShellIntegrationVersion, + StealFocus, + UnicodeVersion, +}; + +// Instead of using `std.meta.stringToEnum` we set up a StaticStringMap so +// that we can get ASCII case-insensitive lookups. +const Map = std.StaticStringMapWithEql(Key, std.ascii.eqlIgnoreCase); +const map: Map = .initComptime( + map: { + const fields = @typeInfo(Key).@"enum".fields; + var tmp: [fields.len]struct { [:0]const u8, Key } = undefined; + for (fields, 0..) |field, i| { + tmp[i] = .{ field.name, @enumFromInt(field.value) }; + } + break :map tmp; + }, +); + +/// Parse OSC 1337 +/// https://iterm2.com/documentation-escape-codes.html +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + assert(parser.state == .@"1337"); + + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + + const key_str: [:0]u8, const value_: ?[:0]u8 = kv: { + const index = std.mem.indexOfScalar(u8, data, '=') orelse { + break :kv .{ data[0 .. data.len - 1 :0], null }; + }; + data[index] = 0; + break :kv .{ data[0..index :0], data[index + 1 .. data.len - 1 :0] }; + }; + + const key = map.get(key_str) orelse { + parser.command = .invalid; + return null; + }; + + switch (key) { + .Copy => { + var value = value_ orelse { + parser.command = .invalid; + return null; + }; + + // Sending a blank entry to clear the clipboard is an OSC 52-ism, + // make sure that is invalid here. + if (value.len == 0) { + parser.command = .invalid; + return null; + } + + // base64 value must be prefixed by a colon + if (value[0] != ':') { + parser.command = .invalid; + return null; + } + + value = value[1..value.len :0]; + + // Sending a blank entry to clear the clipboard is an OSC 52-ism, + // make sure that is invalid here. + if (value.len == 0) { + parser.command = .invalid; + return null; + } + + // Sending a '?' to query the clipboard is an OSC 52-ism, make sure + // that is invalid here. + if (value.len == 1 and value[0] == '?') { + parser.command = .invalid; + return null; + } + + // It would be better to check for valid base64 data here, but that + // would mean parsing the base64 data twice in the "normal" case. + + parser.command = .{ + .clipboard_contents = .{ + .kind = 'c', + .data = value, + }, + }; + return &parser.command; + }, + + .CurrentDir => { + const value = value_ orelse { + parser.command = .invalid; + return null; + }; + if (value.len == 0) { + parser.command = .invalid; + return null; + } + parser.command = .{ + .report_pwd = .{ + .value = value, + }, + }; + return &parser.command; + }, + + .AddAnnotation, + .AddHiddenAnnotation, + .Block, + .Button, + .ClearCapturedOutput, + .ClearScrollback, + .CopyToClipboard, + .CursorShape, + .Custom, + .Disinter, + .EndCopy, + .File, + .FileEnd, + .FilePart, + .HighlightCursorLine, + .MultipartFile, + .OpenURL, + .PopKeyLabels, + .PushKeyLabels, + .RemoteHost, + .ReportCellSize, + .ReportVariable, + .RequestAttention, + .RequestUpload, + .SetBackgroundImageFile, + .SetBadgeFormat, + .SetColors, + .SetKeyLabel, + .SetMark, + .SetProfile, + .SetUserVar, + .ShellIntegrationVersion, + .StealFocus, + .UnicodeVersion, + => { + log.debug("unimplemented OSC 1337: {t}", .{key}); + parser.command = .invalid; + return null; + }, + } + return &parser.command; +} + +test "OSC: 1337: test valid unimplemented key with no value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;SetBadgeFormat"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test valid unimplemented key with empty value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;SetBadgeFormat="; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test valid unimplemented key with non-empty value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;SetBadgeFormat=abc123"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test valid key with lower case and with no value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;setbadgeformat"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test valid key with lower case and with empty value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;setbadgeformat="; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test valid key with lower case and with non-empty value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;setbadgeformat=abc123"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test invalid key with no value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;BobrKurwa"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test invalid key with empty value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;BobrKurwa="; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test invalid key with non-empty value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;BobrKurwa=abc123"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test Copy with no value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;Copy"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test Copy with empty value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;Copy="; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test Copy with only prefix colon" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;Copy=:"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test Copy with question mark" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;Copy=:?"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test Copy with non-empty value that is invalid base64" { + // For performance reasons, we don't check for valid base64 data + // right now. + return error.SkipZigTest; + + // const testing = std.testing; + + // var p: Parser = .init(testing.allocator); + // defer p.deinit(); + + // const input = "1337;Copy=:abc123"; + // for (input) |ch| p.next(ch); + + // try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test Copy with non-empty value that is valid base64 but not prefixed with a colon" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;Copy=YWJjMTIz"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test Copy with non-empty value that is valid base64" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;Copy=:YWJjMTIz"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .clipboard_contents); + try testing.expectEqual('c', cmd.clipboard_contents.kind); + try testing.expectEqualStrings("YWJjMTIz", cmd.clipboard_contents.data); +} + +test "OSC: 1337: test CurrentDir with no value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;CurrentDir"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test CurrentDir with empty value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;CurrentDir="; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC: 1337: test CurrentDir with non-empty value" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "1337;CurrentDir=abc123"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .report_pwd); + try testing.expectEqualStrings("abc123", cmd.report_pwd.value); +} diff --git a/src/terminal/osc/parsers/kitty_color.zig b/src/terminal/osc/parsers/kitty_color.zig new file mode 100644 index 000000000..30a7fe77f --- /dev/null +++ b/src/terminal/osc/parsers/kitty_color.zig @@ -0,0 +1,212 @@ +const std = @import("std"); + +const assert = @import("../../../quirks.zig").inlineAssert; + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; +const kitty_color = @import("../../kitty/color.zig"); +const RGB = @import("../../color.zig").RGB; + +const log = std.log.scoped(.osc_kitty_color); + +/// Parse OSC 21, the Kitty Color Protocol. +pub fn parse(parser: *Parser, terminator_ch: ?u8) ?*Command { + assert(parser.state == .@"21"); + + const alloc = parser.alloc orelse { + parser.state = .invalid; + return null; + }; + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + parser.command = .{ + .kitty_color_protocol = .{ + .list = .empty, + .terminator = .init(terminator_ch), + }, + }; + const list = &parser.command.kitty_color_protocol.list; + const data = writer.buffered(); + var kv_it = std.mem.splitScalar(u8, data, ';'); + while (kv_it.next()) |kv| { + if (list.items.len >= @as(usize, kitty_color.Kind.max) * 2) { + log.warn("exceeded limit for number of keys in kitty color protocol, ignoring", .{}); + parser.state = .invalid; + return null; + } + var it = std.mem.splitScalar(u8, kv, '='); + const k = it.next() orelse continue; + if (k.len == 0) { + log.warn("zero length key in kitty color protocol", .{}); + continue; + } + const key = kitty_color.Kind.parse(k) orelse { + log.warn("unknown key in kitty color protocol: {s}", .{k}); + continue; + }; + const value = std.mem.trim(u8, it.rest(), " "); + if (value.len == 0) { + list.append(alloc, .{ .reset = key }) catch |err| { + log.warn("unable to append kitty color protocol option: {}", .{err}); + continue; + }; + } else if (std.mem.eql(u8, "?", value)) { + list.append(alloc, .{ .query = key }) catch |err| { + log.warn("unable to append kitty color protocol option: {}", .{err}); + continue; + }; + } else { + list.append(alloc, .{ + .set = .{ + .key = key, + .color = RGB.parse(value) catch |err| switch (err) { + error.InvalidFormat => { + log.warn("invalid color format in kitty color protocol: {s}", .{value}); + continue; + }, + }, + }, + }) catch |err| { + log.warn("unable to append kitty color protocol option: {}", .{err}); + continue; + }; + } + } + return &parser.command; +} + +test "OSC 21: kitty color protocol" { + const testing = std.testing; + const Kind = kitty_color.Kind; + + var p: Parser = .init(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"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_color_protocol); + try testing.expectEqual(@as(usize, 9), cmd.kitty_color_protocol.list.items.len); + { + const item = cmd.kitty_color_protocol.list.items[0]; + try testing.expect(item == .query); + try testing.expectEqual(Kind{ .special = .foreground }, item.query); + } + { + const item = cmd.kitty_color_protocol.list.items[1]; + try testing.expect(item == .set); + try testing.expectEqual(Kind{ .special = .background }, item.set.key); + try testing.expectEqual(@as(u8, 0xf0), item.set.color.r); + try testing.expectEqual(@as(u8, 0xf8), item.set.color.g); + try testing.expectEqual(@as(u8, 0xff), item.set.color.b); + } + { + const item = cmd.kitty_color_protocol.list.items[2]; + try testing.expect(item == .set); + try testing.expectEqual(Kind{ .special = .cursor }, item.set.key); + try testing.expectEqual(@as(u8, 0xf0), item.set.color.r); + try testing.expectEqual(@as(u8, 0xf8), item.set.color.g); + try testing.expectEqual(@as(u8, 0xff), item.set.color.b); + } + { + const item = cmd.kitty_color_protocol.list.items[3]; + try testing.expect(item == .reset); + try testing.expectEqual(Kind{ .special = .cursor_text }, item.reset); + } + { + const item = cmd.kitty_color_protocol.list.items[4]; + try testing.expect(item == .reset); + try testing.expectEqual(Kind{ .special = .visual_bell }, item.reset); + } + { + const item = cmd.kitty_color_protocol.list.items[5]; + try testing.expect(item == .query); + try testing.expectEqual(Kind{ .special = .selection_background }, item.query); + } + { + const item = cmd.kitty_color_protocol.list.items[6]; + try testing.expect(item == .set); + try testing.expectEqual(Kind{ .special = .selection_background }, item.set.key); + try testing.expectEqual(@as(u8, 0xaa), item.set.color.r); + try testing.expectEqual(@as(u8, 0xbb), item.set.color.g); + try testing.expectEqual(@as(u8, 0xcc), item.set.color.b); + } + { + const item = cmd.kitty_color_protocol.list.items[7]; + try testing.expect(item == .query); + try testing.expectEqual(Kind{ .palette = 2 }, item.query); + } + { + const item = cmd.kitty_color_protocol.list.items[8]; + try testing.expect(item == .set); + try testing.expectEqual(Kind{ .palette = 3 }, item.set.key); + try testing.expectEqual(@as(u8, 0xff), item.set.color.r); + try testing.expectEqual(@as(u8, 0xff), item.set.color.g); + try testing.expectEqual(@as(u8, 0xff), item.set.color.b); + } +} + +test "OSC 21: kitty color protocol without allocator" { + const testing = std.testing; + + var p: Parser = .init(null); + defer p.deinit(); + + const input = "21;foreground=?"; + for (input) |ch| p.next(ch); + try testing.expect(p.end('\x1b') == null); +} + +test "OSC 21: kitty color protocol double reset" { + const testing = std.testing; + + var p: Parser = .init(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"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_color_protocol); + + p.reset(); + p.reset(); +} + +test "OSC 21: kitty color protocol reset after invalid" { + const testing = std.testing; + + var p: Parser = .init(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"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_color_protocol); + + p.reset(); + + try testing.expectEqual(Parser.State.start, p.state); + p.next('X'); + try testing.expectEqual(Parser.State.invalid, p.state); + + p.reset(); +} + +test "OSC 21: kitty color protocol no key" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "21;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_color_protocol); + try testing.expectEqual(0, cmd.kitty_color_protocol.list.items.len); +} diff --git a/src/terminal/osc/parsers/kitty_text_sizing.zig b/src/terminal/osc/parsers/kitty_text_sizing.zig new file mode 100644 index 000000000..f0180cc8f --- /dev/null +++ b/src/terminal/osc/parsers/kitty_text_sizing.zig @@ -0,0 +1,257 @@ +//! Kitty's text sizing protocol (OSC 66) +//! Specification: https://sw.kovidgoyal.net/kitty/text-sizing-protocol/ + +const std = @import("std"); +const build_options = @import("terminal_options"); + +const assert = @import("../../../quirks.zig").inlineAssert; + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; +const encoding = @import("../encoding.zig"); +const lib = @import("../../../lib/main.zig"); +const lib_target: lib.Target = if (build_options.c_abi) .c else .zig; + +const log = std.log.scoped(.kitty_text_sizing); + +pub const max_payload_length = 4096; + +pub const VAlign = lib.Enum(lib_target, &.{ + "top", + "bottom", + "center", +}); + +pub const HAlign = lib.Enum(lib_target, &.{ + "left", + "right", + "center", +}); + +pub const OSC = struct { + scale: u3 = 1, // 1 - 7 + width: u3 = 0, // 0 - 7 (0 means default) + numerator: u4 = 0, + denominator: u4 = 0, + valign: VAlign = .top, + halign: HAlign = .left, + text: [:0]const u8, + + /// We don't currently support encoding this to C in any way. + pub const C = void; + + pub fn cval(_: OSC) C { + return {}; + } + + fn update(self: *OSC, key: u8, value: []const u8) error{ + UnknownKey, + InvalidValue, + }!void { + // All values are numeric, so we can do a small hack here + const v = std.fmt.parseInt( + u4, + value, + 10, + ) catch return error.InvalidValue; + + switch (key) { + 's' => { + if (v == 0) return error.InvalidValue; + self.scale = std.math.cast(u3, v) orelse return error.InvalidValue; + }, + 'w' => self.width = std.math.cast(u3, v) orelse return error.InvalidValue, + 'n' => self.numerator = v, + 'd' => self.denominator = v, + 'v' => self.valign = std.enums.fromInt(VAlign, v) orelse return error.InvalidValue, + 'h' => self.halign = std.enums.fromInt(HAlign, v) orelse return error.InvalidValue, + else => return error.UnknownKey, + } + } +}; + +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + assert(parser.state == .@"66"); + + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + + // Write a NUL byte to ensure that `text` is NUL-terminated + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + + const payload_start = std.mem.indexOfScalar(u8, data, ';') orelse { + log.warn("missing semicolon before payload", .{}); + parser.state = .invalid; + return null; + }; + const payload = data[payload_start + 1 .. data.len - 1 :0]; + + // Payload has to be a URL-safe UTF-8 string, + // and be under the size limit. + if (payload.len > max_payload_length) { + log.warn("payload is too long", .{}); + parser.state = .invalid; + return null; + } + if (!encoding.isSafeUtf8(payload)) { + log.warn("payload is not escape code safe UTF-8", .{}); + parser.state = .invalid; + return null; + } + + parser.command = .{ + .kitty_text_sizing = .{ .text = payload }, + }; + const cmd = &parser.command.kitty_text_sizing; + + // Parse any arguments if given + if (payload_start > 0) { + var kv_it = std.mem.splitScalar( + u8, + data[0..payload_start], + ':', + ); + + while (kv_it.next()) |kv| { + var it = std.mem.splitScalar(u8, kv, '='); + const k = it.next() orelse { + log.warn("missing key", .{}); + continue; + }; + if (k.len != 1) { + log.warn("key must be a single character", .{}); + continue; + } + + const value = it.next() orelse { + log.warn("missing value", .{}); + continue; + }; + + cmd.update(k[0], value) catch |err| { + switch (err) { + error.UnknownKey => log.warn("unknown key: '{c}'", .{k[0]}), + error.InvalidValue => log.warn("invalid value for key '{c}': {}", .{ k[0], err }), + } + continue; + }; + } + } + + return &parser.command; +} + +test "OSC 66: empty parameters" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "66;;bobr"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_text_sizing); + try testing.expectEqual(1, cmd.kitty_text_sizing.scale); + try testing.expectEqualStrings("bobr", cmd.kitty_text_sizing.text); +} + +test "OSC 66: single parameter" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "66;s=2;kurwa"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_text_sizing); + try testing.expectEqual(2, cmd.kitty_text_sizing.scale); + try testing.expectEqualStrings("kurwa", cmd.kitty_text_sizing.text); +} + +test "OSC 66: multiple parameters" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "66;s=2:w=7:n=13:d=15:v=1:h=2;long"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_text_sizing); + try testing.expectEqual(2, cmd.kitty_text_sizing.scale); + try testing.expectEqual(7, cmd.kitty_text_sizing.width); + try testing.expectEqual(13, cmd.kitty_text_sizing.numerator); + try testing.expectEqual(15, cmd.kitty_text_sizing.denominator); + try testing.expectEqual(.bottom, cmd.kitty_text_sizing.valign); + try testing.expectEqual(.center, cmd.kitty_text_sizing.halign); + try testing.expectEqualStrings("long", cmd.kitty_text_sizing.text); +} + +test "OSC 66: scale is zero" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "66;s=0;nope"; + for (input) |ch| p.next(ch); + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .kitty_text_sizing); + try testing.expectEqual(1, cmd.kitty_text_sizing.scale); +} + +test "OSC 66: invalid parameters" { + const testing = std.testing; + + var p: Parser = .init(null); + + for ("66;w=8:v=3:n=16;") |ch| p.next(ch); + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .kitty_text_sizing); + try testing.expectEqual(0, cmd.kitty_text_sizing.width); + try testing.expect(cmd.kitty_text_sizing.valign == .top); + try testing.expectEqual(0, cmd.kitty_text_sizing.numerator); +} + +test "OSC 66: UTF-8" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "66;;👻魑魅魍魉ゴースッティ"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_text_sizing); + try testing.expectEqualStrings("👻魑魅魍魉ゴースッティ", cmd.kitty_text_sizing.text); +} + +test "OSC 66: unsafe UTF-8" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "66;;\n"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC 66: overlong UTF-8" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "66;;" ++ "bobr" ** 1025; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} diff --git a/src/terminal/osc/parsers/mouse_shape.zig b/src/terminal/osc/parsers/mouse_shape.zig new file mode 100644 index 000000000..91c5ab270 --- /dev/null +++ b/src/terminal/osc/parsers/mouse_shape.zig @@ -0,0 +1,39 @@ +const std = @import("std"); + +const assert = @import("../../../quirks.zig").inlineAssert; + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +// Parse OSC 22 +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + assert(parser.state == .@"22"); + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + parser.command = .{ + .mouse_shape = .{ + .value = data[0 .. data.len - 1 :0], + }, + }; + return &parser.command; +} + +test "OSC 22: pointer cursor" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "22;pointer"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .mouse_shape); + try testing.expectEqualStrings("pointer", cmd.mouse_shape.value); +} diff --git a/src/terminal/osc/parsers/osc9.zig b/src/terminal/osc/parsers/osc9.zig new file mode 100644 index 000000000..f636813d9 --- /dev/null +++ b/src/terminal/osc/parsers/osc9.zig @@ -0,0 +1,1140 @@ +const std = @import("std"); + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +/// Parse OSC 9, which could be an iTerm2 notification or a ConEmu extension. +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + + // Check first to see if this is a ConEmu OSC + // https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC + conemu: { + var data = writer.buffered(); + if (data.len == 0) break :conemu; + switch (data[0]) { + // Check for OSC 9;1 9;10 9;11 9;12 + '1' => { + if (data.len < 2) break :conemu; + switch (data[1]) { + // OSC 9;1 sleep + ';' => { + parser.command = .{ + .conemu_sleep = .{ + .duration_ms = if (std.fmt.parseUnsigned(u16, data[2..], 10)) |num| @min(num, 10_000) else |_| 100, + }, + }; + return &parser.command; + }, + // OSC 9;10 xterm keyboard and output emulation + '0' => { + if (data.len == 2) { + parser.command = .{ + .conemu_xterm_emulation = .{ + .keyboard = true, + .output = true, + }, + }; + return &parser.command; + } + if (data.len < 4) break :conemu; + if (data[2] != ';') break :conemu; + switch (data[3]) { + '0' => { + parser.command = .{ + .conemu_xterm_emulation = .{ + .keyboard = false, + .output = false, + }, + }; + return &parser.command; + }, + '1' => { + parser.command = .{ + .conemu_xterm_emulation = .{ + .keyboard = true, + .output = true, + }, + }; + return &parser.command; + }, + '2' => { + parser.command = .{ + .conemu_xterm_emulation = .{ + .keyboard = null, + .output = false, + }, + }; + return &parser.command; + }, + '3' => { + parser.command = .{ + .conemu_xterm_emulation = .{ + .keyboard = null, + .output = true, + }, + }; + return &parser.command; + }, + else => break :conemu, + } + }, + // OSC 9;11 comment + '1' => { + if (data.len < 3) break :conemu; + if (data[2] != ';') break :conemu; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + data = writer.buffered(); + parser.command = .{ + .conemu_comment = data[3 .. data.len - 1 :0], + }; + return &parser.command; + }, + // OSC 9;12 mark prompt start + '2' => { + parser.command = .{ .semantic_prompt = .init(.fresh_line_new_prompt) }; + return &parser.command; + }, + else => break :conemu, + } + }, + // OSC 9;2 show message box + '2' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + data = writer.buffered(); + parser.command = .{ + .conemu_show_message_box = data[2 .. data.len - 1 :0], + }; + return &parser.command; + }, + // OSC 9;3 change tab title + '3' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + if (data.len == 2) { + parser.command = .{ + .conemu_change_tab_title = .reset, + }; + return &parser.command; + } + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + data = writer.buffered(); + parser.command = .{ + .conemu_change_tab_title = .{ + .value = data[2 .. data.len - 1 :0], + }, + }; + return &parser.command; + }, + // OSC 9;4 progress report + '4' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + if (data.len < 3) break :conemu; + switch (data[2]) { + '0' => { + parser.command = .{ + .conemu_progress_report = .{ + .state = .remove, + }, + }; + }, + '1' => { + parser.command = .{ + .conemu_progress_report = .{ + .state = .set, + .progress = 0, + }, + }; + }, + '2' => { + parser.command = .{ + .conemu_progress_report = .{ + .state = .@"error", + }, + }; + }, + '3' => { + parser.command = .{ + .conemu_progress_report = .{ + .state = .indeterminate, + }, + }; + }, + '4' => { + parser.command = .{ + .conemu_progress_report = .{ + .state = .pause, + }, + }; + }, + else => break :conemu, + } + switch (parser.command.conemu_progress_report.state) { + .remove, .indeterminate => {}, + .set, .@"error", .pause => progress: { + if (data.len < 4) break :progress; + if (data[3] != ';') break :progress; + // parse the progress value + parser.command.conemu_progress_report.progress = value: { + break :value @intCast(std.math.clamp( + std.fmt.parseUnsigned(usize, data[4..], 10) catch break :value null, + 0, + 100, + )); + }; + }, + } + return &parser.command; + }, + // OSC 9;5 wait for input + '5' => { + parser.command = .conemu_wait_input; + return &parser.command; + }, + // OSC 9;6 guimacro + '6' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + data = writer.buffered(); + parser.command = .{ + .conemu_guimacro = data[2 .. data.len - 1 :0], + }; + return &parser.command; + }, + // OSC 9;7 run process + '7' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + data = writer.buffered(); + parser.command = .{ + .conemu_run_process = data[2 .. data.len - 1 :0], + }; + return &parser.command; + }, + // OSC 9;8 output environment variable + '8' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + data = writer.buffered(); + parser.command = .{ + .conemu_output_environment_variable = data[2 .. data.len - 1 :0], + }; + return &parser.command; + }, + // OSC 9;9 current working directory + '9' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + data = writer.buffered(); + parser.command = .{ + .report_pwd = .{ + .value = data[2 .. data.len - 1 :0], + }, + }; + return &parser.command; + }, + else => break :conemu, + } + } + + // If it's not a ConEmu OSC, it's an iTerm2 notification + + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + parser.command = .{ + .show_desktop_notification = .{ + .title = "", + .body = data[0 .. data.len - 1 :0], + }, + }; + return &parser.command; +} + +test "OSC 9: show desktop notification" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;Hello world"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("", cmd.show_desktop_notification.title); + try testing.expectEqualStrings("Hello world", cmd.show_desktop_notification.body); +} + +test "OSC 9: show single character desktop notification" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;H"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("", cmd.show_desktop_notification.title); + try testing.expectEqualStrings("H", cmd.show_desktop_notification.body); +} + +test "OSC 9;1: ConEmu sleep" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;1;420"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .conemu_sleep); + try testing.expectEqual(420, cmd.conemu_sleep.duration_ms); +} + +test "OSC 9;1: ConEmu sleep with no value default to 100ms" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;1;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .conemu_sleep); + try testing.expectEqual(100, cmd.conemu_sleep.duration_ms); +} + +test "OSC 9;1: conemu sleep cannot exceed 10000ms" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;1;12345"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .conemu_sleep); + try testing.expectEqual(10000, cmd.conemu_sleep.duration_ms); +} + +test "OSC 9;1: conemu sleep invalid input" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;1;foo"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .conemu_sleep); + try testing.expectEqual(100, cmd.conemu_sleep.duration_ms); +} + +test "OSC 9;1: conemu sleep -> desktop notification 1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;1"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("1", cmd.show_desktop_notification.body); +} + +test "OSC 9;1: conemu sleep -> desktop notification 2" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;1a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("1a", cmd.show_desktop_notification.body); +} + +test "OSC 9;2: ConEmu message box" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;2;hello world"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_show_message_box); + try testing.expectEqualStrings("hello world", cmd.conemu_show_message_box); +} + +test "OSC 9;2: ConEmu message box invalid input" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;2"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("2", cmd.show_desktop_notification.body); +} + +test "OSC 9;2: ConEmu message box empty message" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;2;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_show_message_box); + try testing.expectEqualStrings("", cmd.conemu_show_message_box); +} + +test "OSC 9;2: ConEmu message box spaces only message" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;2; "; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_show_message_box); + try testing.expectEqualStrings(" ", cmd.conemu_show_message_box); +} + +test "OSC 9;2: message box -> desktop notification 1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;2"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("2", cmd.show_desktop_notification.body); +} + +test "OSC 9;2: message box -> desktop notification 2" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;2a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("2a", cmd.show_desktop_notification.body); +} + +test "OSC 9;3: ConEmu change tab title" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;3;foo bar"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_change_tab_title); + try testing.expectEqualStrings("foo bar", cmd.conemu_change_tab_title.value); +} + +test "OSC 9;3: ConEmu change tab title reset" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;3;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + const expected_command: Command = .{ .conemu_change_tab_title = .reset }; + try testing.expectEqual(expected_command, cmd); +} + +test "OSC 9;3: ConEmu change tab title spaces only" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;3; "; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .conemu_change_tab_title); + try testing.expectEqualStrings(" ", cmd.conemu_change_tab_title.value); +} + +test "OSC 9;3: change tab title -> desktop notification 1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;3"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("3", cmd.show_desktop_notification.body); +} + +test "OSC 9;3: message box -> desktop notification 2" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;3a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("3a", cmd.show_desktop_notification.body); +} + +test "OSC 9;4: ConEmu progress set" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;4;1;100"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .set); + try testing.expect(cmd.conemu_progress_report.progress == 100); +} + +test "OSC 9;4: ConEmu progress set overflow" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;4;1;900"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .set); + try testing.expectEqual(100, cmd.conemu_progress_report.progress); +} + +test "OSC 9;4: ConEmu progress set single digit" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;4;1;9"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .set); + try testing.expect(cmd.conemu_progress_report.progress == 9); +} + +test "OSC 9;4: ConEmu progress set double digit" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;4;1;94"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .set); + try testing.expectEqual(94, cmd.conemu_progress_report.progress); +} + +test "OSC 9;4: ConEmu progress set extra semicolon ignored" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;4;1;100"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .set); + try testing.expectEqual(100, cmd.conemu_progress_report.progress); +} + +test "OSC 9;4: ConEmu progress remove with no progress" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;4;0;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .remove); + try testing.expect(cmd.conemu_progress_report.progress == null); +} + +test "OSC 9;4: ConEmu progress remove with double semicolon" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;4;0;;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .remove); + try testing.expect(cmd.conemu_progress_report.progress == null); +} + +test "OSC 9;4: ConEmu progress remove ignores progress" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;4;0;100"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .remove); + try testing.expect(cmd.conemu_progress_report.progress == null); +} + +test "OSC 9;4: ConEmu progress remove extra semicolon" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;4;0;100;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .remove); +} + +test "OSC 9;4: ConEmu progress error" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;4;2"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .@"error"); + try testing.expect(cmd.conemu_progress_report.progress == null); +} + +test "OSC 9;4: ConEmu progress error with progress" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;4;2;100"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .@"error"); + try testing.expect(cmd.conemu_progress_report.progress == 100); +} + +test "OSC 9;4: progress pause" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;4;4"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .pause); + try testing.expect(cmd.conemu_progress_report.progress == null); +} + +test "OSC 9;4: ConEmu progress pause with progress" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;4;4;100"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .pause); + try testing.expect(cmd.conemu_progress_report.progress == 100); +} + +test "OSC 9;4: progress -> desktop notification 1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;4"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("4", cmd.show_desktop_notification.body); +} + +test "OSC 9;4: progress -> desktop notification 2" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;4;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("4;", cmd.show_desktop_notification.body); +} + +test "OSC 9;4: progress -> desktop notification 3" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;4;5"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("4;5", cmd.show_desktop_notification.body); +} + +test "OSC 9;4: progress -> desktop notification 4" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;4;5a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("4;5a", cmd.show_desktop_notification.body); +} + +test "OSC 9;5: ConEmu wait input" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;5"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_wait_input); +} + +test "OSC 9;5: ConEmu wait ignores trailing characters" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "9;5;foo"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_wait_input); +} + +test "OSC 9;6: ConEmu guimacro 1" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;6;a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_guimacro); + try testing.expectEqualStrings("a", cmd.conemu_guimacro); +} + +test "OSC: 9;6: ConEmu guimacro 2" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;6;ab"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_guimacro); + try testing.expectEqualStrings("ab", cmd.conemu_guimacro); +} + +test "OSC: 9;6: ConEmu guimacro 3 incomplete -> desktop notification" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;6"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("6", cmd.show_desktop_notification.body); +} + +test "OSC: 9;7: ConEmu run process 1" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;7;ab"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_run_process); + try testing.expectEqualStrings("ab", cmd.conemu_run_process); +} + +test "OSC: 9;7: ConEmu run process 2" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;7;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_run_process); + try testing.expectEqualStrings("", cmd.conemu_run_process); +} + +test "OSC: 9;7: ConEmu run process incomplete -> desktop notification" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;7"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("7", cmd.show_desktop_notification.body); +} + +test "OSC: 9;8: ConEmu output environment variable 1" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;8;ab"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_output_environment_variable); + try testing.expectEqualStrings("ab", cmd.conemu_output_environment_variable); +} + +test "OSC: 9;8: ConEmu output environment variable 2" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;8;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_output_environment_variable); + try testing.expectEqualStrings("", cmd.conemu_output_environment_variable); +} + +test "OSC: 9;8: ConEmu output environment variable incomplete -> desktop notification" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;8"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("8", cmd.show_desktop_notification.body); +} + +test "OSC: 9;9: ConEmu set current working directory" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;9;ab"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .report_pwd); + try testing.expectEqualStrings("ab", cmd.report_pwd.value); +} + +test "OSC: 9;9: ConEmu set current working directory incomplete -> desktop notification" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;9"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("9", cmd.show_desktop_notification.body); +} + +test "OSC: 9;10: ConEmu xterm keyboard and output emulation 1" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;10"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_xterm_emulation); + try testing.expect(cmd.conemu_xterm_emulation.keyboard != null); + try testing.expect(cmd.conemu_xterm_emulation.keyboard.? == true); + try testing.expect(cmd.conemu_xterm_emulation.output != null); + try testing.expect(cmd.conemu_xterm_emulation.output.? == true); +} + +test "OSC: 9;10: ConEmu xterm keyboard and output emulation 2" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;10;0"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_xterm_emulation); + try testing.expect(cmd.conemu_xterm_emulation.keyboard != null); + try testing.expect(cmd.conemu_xterm_emulation.keyboard.? == false); + try testing.expect(cmd.conemu_xterm_emulation.output != null); + try testing.expect(cmd.conemu_xterm_emulation.output.? == false); +} + +test "OSC: 9;10: ConEmu xterm keyboard and output emulation 3" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;10;1"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_xterm_emulation); + try testing.expect(cmd.conemu_xterm_emulation.keyboard != null); + try testing.expect(cmd.conemu_xterm_emulation.keyboard.? == true); + try testing.expect(cmd.conemu_xterm_emulation.output != null); + try testing.expect(cmd.conemu_xterm_emulation.output.? == true); +} + +test "OSC: 9;10: ConEmu xterm keyboard and output emulation 4" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;10;2"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_xterm_emulation); + try testing.expect(cmd.conemu_xterm_emulation.keyboard == null); + try testing.expect(cmd.conemu_xterm_emulation.output != null); + try testing.expect(cmd.conemu_xterm_emulation.output.? == false); +} + +test "OSC: 9;10: ConEmu xterm keyboard and output emulation 5" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;10;3"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_xterm_emulation); + try testing.expect(cmd.conemu_xterm_emulation.keyboard == null); + try testing.expect(cmd.conemu_xterm_emulation.output != null); + try testing.expect(cmd.conemu_xterm_emulation.output.? == true); +} + +test "OSC: 9;10: ConEmu xterm keyboard and output emulation 6" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;10;4"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("10;4", cmd.show_desktop_notification.body); +} + +test "OSC: 9;10: ConEmu xterm keyboard and output emulation 7" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;10;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("10;", cmd.show_desktop_notification.body); +} + +test "OSC: 9;10: ConEmu xterm keyboard and output emulation 8" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;10;abc"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("10;abc", cmd.show_desktop_notification.body); +} + +test "OSC: 9;11: ConEmu comment" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;11;ab"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .conemu_comment); + try testing.expectEqualStrings("ab", cmd.conemu_comment); +} + +test "OSC: 9;11: ConEmu comment incomplete -> desktop notification" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;11"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("11", cmd.show_desktop_notification.body); +} + +test "OSC: 9;12: ConEmu mark prompt start 1" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;12"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .semantic_prompt); +} + +test "OSC: 9;12: ConEmu mark prompt start 2" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "9;12;abc"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .semantic_prompt); +} diff --git a/src/terminal/osc/parsers/report_pwd.zig b/src/terminal/osc/parsers/report_pwd.zig new file mode 100644 index 000000000..080b9cbb0 --- /dev/null +++ b/src/terminal/osc/parsers/report_pwd.zig @@ -0,0 +1,48 @@ +const std = @import("std"); + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +/// Parse OSC 7 +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + parser.command = .{ + .report_pwd = .{ + .value = data[0 .. data.len - 1 :0], + }, + }; + return &parser.command; +} + +test "OSC 7: report pwd" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "7;file:///tmp/example"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .report_pwd); + try testing.expectEqualStrings("file:///tmp/example", cmd.report_pwd.value); +} + +test "OSC 7: report pwd empty" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "7;"; + for (input) |ch| p.next(ch); + const cmd = p.end(null).?.*; + try testing.expect(cmd == .report_pwd); + try testing.expectEqualStrings("", cmd.report_pwd.value); +} diff --git a/src/terminal/osc/parsers/rxvt_extension.zig b/src/terminal/osc/parsers/rxvt_extension.zig new file mode 100644 index 000000000..94a0961d2 --- /dev/null +++ b/src/terminal/osc/parsers/rxvt_extension.zig @@ -0,0 +1,59 @@ +const std = @import("std"); + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +const log = std.log.scoped(.osc_rxvt_extension); + +/// Parse OSC 777 +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + // ensure that we are sentinel terminated + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + const k = std.mem.indexOfScalar(u8, data, ';') orelse { + parser.state = .invalid; + return null; + }; + const ext = data[0..k]; + if (!std.mem.eql(u8, ext, "notify")) { + log.warn("unknown rxvt extension: {s}", .{ext}); + parser.state = .invalid; + return null; + } + const t = std.mem.indexOfScalarPos(u8, data, k + 1, ';') orelse { + log.warn("rxvt notify extension is missing the title", .{}); + parser.state = .invalid; + return null; + }; + data[t] = 0; + const title = data[k + 1 .. t :0]; + const body = data[t + 1 .. data.len - 1 :0]; + parser.command = .{ + .show_desktop_notification = .{ + .title = title, + .body = body, + }, + }; + return &parser.command; +} + +test "OSC: OSC 777 show desktop notification with title" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "777;notify;Title;Body"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings(cmd.show_desktop_notification.title, "Title"); + try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Body"); +} diff --git a/src/terminal/osc/parsers/semantic_prompt.zig b/src/terminal/osc/parsers/semantic_prompt.zig new file mode 100644 index 000000000..f6a0cb593 --- /dev/null +++ b/src/terminal/osc/parsers/semantic_prompt.zig @@ -0,0 +1,901 @@ +//! https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md +const std = @import("std"); +const Parser = @import("../../osc.zig").Parser; +const OSCCommand = @import("../../osc.zig").Command; + +const log = std.log.scoped(.osc_semantic_prompt); + +/// A single semantic prompt command. +/// +/// Technically according to the spec, not all commands have options +/// but it is easier to be "liberal in what we accept" here since +/// all except one do and the spec does also say to ignore unknown +/// options. So, I think this is a fair interpretation. +pub const Command = struct { + action: Action, + options_unvalidated: []const u8, + + pub const Action = enum { + fresh_line, // 'L' + fresh_line_new_prompt, // 'A' + new_command, // 'N' + prompt_start, // 'P' + end_prompt_start_input, // 'B' + end_prompt_start_input_terminate_eol, // 'I' + end_input_start_output, // 'C' + end_command, // 'D' + }; + + pub fn init(action: Action) Command { + return .{ + .action = action, + .options_unvalidated = "", + }; + } + + /// Read an option for this command. Returns null if unset or invalid. + pub fn readOption( + self: Command, + comptime option: Option, + ) ?option.Type() { + return option.read(self.options_unvalidated); + } +}; + +pub const Option = enum { + aid, + cl, + prompt_kind, + err, + // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + // Kitty supports a "redraw" option for prompt_start. I can't find + // this documented anywhere but can see in the code that this is used + // by shell environments to tell the terminal that the shell will NOT + // redraw the prompt so we should attempt to resize it. + redraw, + + // Use a special key instead of arrow keys to move the cursor on + // mouse click. Useful if arrow keys have side-effets like triggering + // auto-complete. The shell integration script should bind the special + // key as needed. + // See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + special_key, + + // If true, the shell is capable of handling mouse click events. + // Ghostty will then send a click event to the shell when the user + // clicks somewhere in the prompt. The shell can then move the cursor + // to that position or perform some other appropriate action. If false, + // Ghostty may generate a number of fake key events to move the cursor + // which is not very robust. + // See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + click_events, + + // Not technically an option that can be set with k=v and only + // present currently with command 'D' but its easier to just + // parse it into our options. + exit_code, + + pub fn Type(comptime self: Option) type { + return switch (self) { + .aid => []const u8, + .cl => Click, + .prompt_kind => PromptKind, + .err => []const u8, + .redraw => bool, + .special_key => bool, + .click_events => bool, + .exit_code => i32, + }; + } + + fn key(comptime self: Option) []const u8 { + return switch (self) { + .aid => "aid", + .cl => "cl", + .prompt_kind => "k", + .err => "err", + .redraw => "redraw", + .special_key => "special_key", + .click_events => "click_events", + + // special case, handled before ever calling key + .exit_code => unreachable, + }; + } + + /// Read the option value from the raw options string. + /// + /// The raw options string is the raw unparsed data after the + /// OSC 133 command. e.g. for `133;A;aid=14;cl=line`, the + /// raw options string would be `aid=14;cl=line`. + /// + /// Any errors in the raw string will return null since the OSC133 + /// specification says to ignore unknown or malformed options. + pub fn read( + comptime self: Option, + raw: []const u8, + ) ?self.Type() { + var remaining = raw; + while (remaining.len > 0) { + // Length of the next value is up to the `;` or the + // end of the string. + const len = std.mem.indexOfScalar( + u8, + remaining, + ';', + ) orelse remaining.len; + + // Grab our full value and move our cursor past the `;` + const full = remaining[0..len]; + + // If we're looking for exit_code we special case it. + // as the first value. + if (comptime self == .exit_code) { + return std.fmt.parseInt( + i32, + full, + 10, + ) catch null; + } + + // Parse our key=value and verify our key matches our + // expectation. + const value = value: { + if (std.mem.indexOfScalar( + u8, + full, + '=', + )) |eql_idx| { + if (std.mem.eql( + u8, + full[0..eql_idx], + self.key(), + )) { + break :value full[eql_idx + 1 ..]; + } + } + + // No match! + if (len < remaining.len) { + remaining = remaining[len + 1 ..]; + continue; + } + + break; + }; + + return switch (self) { + .aid => value, + .cl => std.meta.stringToEnum(Click, value), + .prompt_kind => if (value.len == 1) PromptKind.init(value[0]) else null, + .err => value, + .redraw, .special_key, .click_events => if (value.len == 1) switch (value[0]) { + '0' => false, + '1' => true, + else => null, + } else null, + // Handled above + .exit_code => unreachable, + }; + } + + // Not found + return null; + } +}; + +pub const Click = enum { + line, + multiple, + conservative_vertical, + smart_vertical, +}; + +pub const PromptKind = enum { + initial, + right, + continuation, + secondary, + + pub fn init(c: u8) ?PromptKind { + return switch (c) { + 'i' => .initial, + 'r' => .right, + 'c' => .continuation, + 's' => .secondary, + else => null, + }; + } +}; + +/// Parse OSC 133, semantic prompts +pub fn parse(parser: *Parser, _: ?u8) ?*OSCCommand { + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + if (data.len == 0) { + parser.state = .invalid; + return null; + } + + // All valid cases terminate within this block. Any fallthroughs + // are invalid. This makes some of our parse logic a little less + // repetitive. + valid: { + switch (data[0]) { + 'A' => fresh_line: { + parser.command = .{ .semantic_prompt = .init(.fresh_line_new_prompt) }; + if (data.len == 1) break :fresh_line; + if (data[1] != ';') break :valid; + parser.command.semantic_prompt.options_unvalidated = data[2..]; + }, + + 'B' => end_prompt: { + parser.command = .{ .semantic_prompt = .init(.end_prompt_start_input) }; + if (data.len == 1) break :end_prompt; + if (data[1] != ';') break :valid; + parser.command.semantic_prompt.options_unvalidated = data[2..]; + }, + + 'I' => end_prompt_line: { + parser.command = .{ .semantic_prompt = .init(.end_prompt_start_input_terminate_eol) }; + if (data.len == 1) break :end_prompt_line; + if (data[1] != ';') break :valid; + parser.command.semantic_prompt.options_unvalidated = data[2..]; + }, + + 'C' => end_input: { + parser.command = .{ .semantic_prompt = .init(.end_input_start_output) }; + if (data.len == 1) break :end_input; + if (data[1] != ';') break :valid; + parser.command.semantic_prompt.options_unvalidated = data[2..]; + }, + + 'D' => end_command: { + parser.command = .{ .semantic_prompt = .init(.end_command) }; + if (data.len == 1) break :end_command; + if (data[1] != ';') break :valid; + parser.command.semantic_prompt.options_unvalidated = data[2..]; + }, + + 'L' => { + if (data.len > 1) break :valid; + parser.command = .{ .semantic_prompt = .init(.fresh_line) }; + }, + + 'N' => new_command: { + parser.command = .{ .semantic_prompt = .init(.new_command) }; + if (data.len == 1) break :new_command; + if (data[1] != ';') break :valid; + parser.command.semantic_prompt.options_unvalidated = data[2..]; + }, + + 'P' => prompt_start: { + parser.command = .{ .semantic_prompt = .init(.prompt_start) }; + if (data.len == 1) break :prompt_start; + if (data[1] != ';') break :valid; + parser.command.semantic_prompt.options_unvalidated = data[2..]; + }, + + else => break :valid, + } + + return &parser.command; + } + + // Any fallthroughs are invalid + parser.state = .invalid; + return null; +} + +test "OSC 133: end_input_start_output" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;C"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .end_input_start_output); + try testing.expect(cmd.semantic_prompt.readOption(.aid) == null); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == null); +} + +test "OSC 133: end_input_start_output extra contents" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "133;Cextra"; + for (input) |ch| p.next(ch); + try testing.expect(p.end(null) == null); +} + +test "OSC 133: end_input_start_output with options" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "133;C;aid=foo"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .end_input_start_output); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?); +} + +test "OSC 133: fresh_line" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;L"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .fresh_line); +} + +test "OSC 133: fresh_line extra contents" { + const testing = std.testing; + + // Random + { + var p: Parser = .init(null); + const input = "133;Lol"; + for (input) |ch| p.next(ch); + try testing.expect(p.end(null) == null); + } + + // Options + { + var p: Parser = .init(null); + const input = "133;L;aid=foo"; + for (input) |ch| p.next(ch); + try testing.expect(p.end(null) == null); + } +} + +test "OSC 133: fresh_line_new_prompt" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.readOption(.aid) == null); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == null); +} + +test "OSC 133: fresh_line_new_prompt with aid" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;aid=14"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expectEqualStrings("14", cmd.semantic_prompt.readOption(.aid).?); +} + +test "OSC 133: fresh_line_new_prompt with '=' in aid" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;aid=a=b"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expectEqualStrings("a=b", cmd.semantic_prompt.readOption(.aid).?); +} + +test "OSC 133: fresh_line_new_prompt with cl=line" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;cl=line"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == .line); +} + +test "OSC 133: fresh_line_new_prompt with cl=multiple" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;cl=multiple"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == .multiple); +} + +test "OSC 133: fresh_line_new_prompt with invalid cl" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;cl=invalid"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == null); +} + +test "OSC 133: fresh_line_new_prompt with trailing ;" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); +} + +test "OSC 133: fresh_line_new_prompt with bare key" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;barekey"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.readOption(.aid) == null); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == null); +} + +test "OSC 133: fresh_line_new_prompt with multiple options" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;aid=foo;cl=line"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == .line); +} + +test "OSC 133: fresh_line_new_prompt default redraw" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.readOption(.redraw) == null); +} + +test "OSC 133: fresh_line_new_prompt with redraw=0" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;redraw=0"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.readOption(.redraw).? == false); +} + +test "OSC 133: fresh_line_new_prompt with redraw=1" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;redraw=1"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.readOption(.redraw).? == true); +} + +test "OSC 133: fresh_line_new_prompt with invalid redraw" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;redraw=x"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .fresh_line_new_prompt); + try testing.expect(cmd.semantic_prompt.readOption(.redraw) == null); +} + +test "OSC 133: prompt_start" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;P"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .prompt_start); + try testing.expect(cmd.semantic_prompt.readOption(.prompt_kind) == null); +} + +test "OSC 133: prompt_start with k=i" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;P;k=i"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .prompt_start); + try testing.expect(cmd.semantic_prompt.readOption(.prompt_kind) == .initial); +} + +test "OSC 133: prompt_start with k=r" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;P;k=r"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .prompt_start); + try testing.expect(cmd.semantic_prompt.readOption(.prompt_kind) == .right); +} + +test "OSC 133: prompt_start with k=c" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;P;k=c"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .prompt_start); + try testing.expect(cmd.semantic_prompt.readOption(.prompt_kind) == .continuation); +} + +test "OSC 133: prompt_start with k=s" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;P;k=s"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .prompt_start); + try testing.expect(cmd.semantic_prompt.readOption(.prompt_kind) == .secondary); +} + +test "OSC 133: prompt_start with invalid k" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;P;k=x"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .prompt_start); + try testing.expect(cmd.semantic_prompt.readOption(.prompt_kind) == null); +} + +test "OSC 133: prompt_start extra contents" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "133;Pextra"; + for (input) |ch| p.next(ch); + try testing.expect(p.end(null) == null); +} + +test "OSC 133: new_command" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;N"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .new_command); + try testing.expect(cmd.semantic_prompt.readOption(.aid) == null); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == null); +} + +test "OSC 133: new_command with aid" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;N;aid=foo"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .new_command); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?); +} + +test "OSC 133: new_command with cl=line" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;N;cl=line"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .new_command); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == .line); +} + +test "OSC 133: new_command with multiple options" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;N;aid=foo;cl=line"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .new_command); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?); + try testing.expect(cmd.semantic_prompt.readOption(.cl) == .line); +} + +test "OSC 133: new_command extra contents" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "133;Nextra"; + for (input) |ch| p.next(ch); + try testing.expect(p.end(null) == null); +} + +test "OSC 133: end_prompt_start_input" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;B"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .end_prompt_start_input); +} + +test "OSC 133: end_prompt_start_input extra contents" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "133;Bextra"; + for (input) |ch| p.next(ch); + try testing.expect(p.end(null) == null); +} + +test "OSC 133: end_prompt_start_input with options" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "133;B;aid=foo"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .end_prompt_start_input); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?); +} + +test "OSC 133: end_prompt_start_input_terminate_eol" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;I"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .end_prompt_start_input_terminate_eol); +} + +test "OSC 133: end_prompt_start_input_terminate_eol extra contents" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "133;Iextra"; + for (input) |ch| p.next(ch); + try testing.expect(p.end(null) == null); +} + +test "OSC 133: end_prompt_start_input_terminate_eol with options" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "133;I;aid=foo"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .end_prompt_start_input_terminate_eol); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?); +} + +test "OSC 133: end_command" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;D"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .end_command); + try testing.expect(cmd.semantic_prompt.readOption(.exit_code) == null); + try testing.expect(cmd.semantic_prompt.readOption(.aid) == null); + try testing.expect(cmd.semantic_prompt.readOption(.err) == null); +} + +test "OSC 133: end_command extra contents" { + const testing = std.testing; + + var p: Parser = .init(null); + const input = "133;Dextra"; + for (input) |ch| p.next(ch); + try testing.expect(p.end(null) == null); +} + +test "OSC 133: end_command with exit code 0" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;D;0"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .end_command); + try testing.expect(cmd.semantic_prompt.readOption(.exit_code) == 0); +} + +test "OSC 133: end_command with exit code and aid" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;D;12;aid=foo"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .semantic_prompt); + try testing.expect(cmd.semantic_prompt.action == .end_command); + try testing.expectEqualStrings("foo", cmd.semantic_prompt.readOption(.aid).?); + try testing.expect(cmd.semantic_prompt.readOption(.exit_code) == 12); +} + +test "Option.read aid" { + const testing = std.testing; + try testing.expectEqualStrings("test123", Option.aid.read("aid=test123").?); + try testing.expectEqualStrings("myaid", Option.aid.read("cl=line;aid=myaid;k=i").?); + try testing.expect(Option.aid.read("cl=line;k=i") == null); + try testing.expectEqualStrings("", Option.aid.read("aid=").?); + try testing.expectEqualStrings("last", Option.aid.read("k=i;aid=last").?); + try testing.expectEqualStrings("first", Option.aid.read("aid=first;k=i").?); + try testing.expect(Option.aid.read("") == null); + try testing.expect(Option.aid.read("aid") == null); + try testing.expectEqualStrings("value", Option.aid.read(";;aid=value;;").?); +} + +test "Option.read cl" { + const testing = std.testing; + try testing.expect(Option.cl.read("cl=line").? == .line); + try testing.expect(Option.cl.read("cl=multiple").? == .multiple); + try testing.expect(Option.cl.read("cl=conservative_vertical").? == .conservative_vertical); + try testing.expect(Option.cl.read("cl=smart_vertical").? == .smart_vertical); + try testing.expect(Option.cl.read("cl=invalid") == null); + try testing.expect(Option.cl.read("aid=foo") == null); +} + +test "Option.read prompt_kind" { + const testing = std.testing; + try testing.expect(Option.prompt_kind.read("k=i").? == .initial); + try testing.expect(Option.prompt_kind.read("k=r").? == .right); + try testing.expect(Option.prompt_kind.read("k=c").? == .continuation); + try testing.expect(Option.prompt_kind.read("k=s").? == .secondary); + try testing.expect(Option.prompt_kind.read("k=x") == null); + try testing.expect(Option.prompt_kind.read("k=ii") == null); + try testing.expect(Option.prompt_kind.read("k=") == null); +} + +test "Option.read err" { + const testing = std.testing; + try testing.expectEqualStrings("some_error", Option.err.read("err=some_error").?); + try testing.expect(Option.err.read("aid=foo") == null); +} + +test "Option.read redraw" { + const testing = std.testing; + try testing.expect(Option.redraw.read("redraw=1").? == true); + try testing.expect(Option.redraw.read("redraw=0").? == false); + try testing.expect(Option.redraw.read("redraw=2") == null); + try testing.expect(Option.redraw.read("redraw=10") == null); + try testing.expect(Option.redraw.read("redraw=") == null); +} + +test "Option.read special_key" { + const testing = std.testing; + try testing.expect(Option.special_key.read("special_key=1").? == true); + try testing.expect(Option.special_key.read("special_key=0").? == false); + try testing.expect(Option.special_key.read("special_key=x") == null); +} + +test "Option.read click_events" { + const testing = std.testing; + try testing.expect(Option.click_events.read("click_events=1").? == true); + try testing.expect(Option.click_events.read("click_events=0").? == false); + try testing.expect(Option.click_events.read("click_events=yes") == null); +} + +test "Option.read exit_code" { + const testing = std.testing; + try testing.expect(Option.exit_code.read("42").? == 42); + try testing.expect(Option.exit_code.read("0").? == 0); + try testing.expect(Option.exit_code.read("-1").? == -1); + try testing.expect(Option.exit_code.read("abc") == null); + try testing.expect(Option.exit_code.read("127;aid=foo").? == 127); +} diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 37a419a93..93742774a 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -196,7 +196,8 @@ pub const Page = struct { // We need to go through and initialize all the rows so that // they point to a valid offset into the cells, since the rows // zero-initialized aren't valid. - const cells_ptr = cells.ptr(buf)[0 .. cap.cols * cap.rows]; + const cells_len = @as(usize, cap.cols) * @as(usize, cap.rows); + const cells_ptr = cells.ptr(buf)[0..cells_len]; for (rows.ptr(buf)[0..cap.rows], 0..) |*row, y| { const start = y * cap.cols; row.* = .{ @@ -632,6 +633,114 @@ pub const Page = struct { HyperlinkError || GraphemeError; + /// Compute the exact capacity required to store a range of rows from + /// this page. + /// + /// The returned capacity will have the same number of columns as this + /// page and the number of rows equal to the range given. The returned + /// capacity is by definition strictly less than or equal to this + /// page's capacity, so the layout is guaranteed to succeed. + /// + /// Preconditions: + /// - Range must be at least 1 row + /// - Start and end must be valid for this page + pub fn exactRowCapacity( + self: *const Page, + y_start: usize, + y_end: usize, + ) Capacity { + assert(y_start < y_end); + assert(y_end <= self.size.rows); + + // Track unique IDs using a bitset. Both style IDs and hyperlink IDs + // are CellCountInt (u16), so we reuse this set for both to save + // stack memory (~8KB instead of ~16KB). + const CellCountSet = std.StaticBitSet(std.math.maxInt(size.CellCountInt) + 1); + comptime assert(size.StyleCountInt == size.CellCountInt); + comptime assert(size.HyperlinkCountInt == size.CellCountInt); + + // Accumulators + var id_set: CellCountSet = .initEmpty(); + var grapheme_bytes: usize = 0; + var string_bytes: usize = 0; + + // First pass: count styles and grapheme bytes + const rows = self.rows.ptr(self.memory)[y_start..y_end]; + for (rows) |*row| { + const cells = row.cells.ptr(self.memory)[0..self.size.cols]; + for (cells) |*cell| { + if (cell.style_id != stylepkg.default_id) { + id_set.set(cell.style_id); + } + + if (cell.hasGrapheme()) { + if (self.lookupGrapheme(cell)) |cps| { + grapheme_bytes += GraphemeAlloc.bytesRequired(u21, cps.len); + } + } + } + } + const styles_cap = StyleSet.capacityForCount(id_set.count()); + + // Second pass: count hyperlinks and string bytes + // We count both unique hyperlinks (for hyperlink_set) and total + // hyperlink cells (for hyperlink_map capacity). + id_set = .initEmpty(); + var hyperlink_cells: usize = 0; + for (rows) |*row| { + const cells = row.cells.ptr(self.memory)[0..self.size.cols]; + for (cells) |*cell| { + if (cell.hyperlink) { + hyperlink_cells += 1; + if (self.lookupHyperlink(cell)) |id| { + // Only count each unique hyperlink once for set sizing + if (!id_set.isSet(id)) { + id_set.set(id); + + // Get the hyperlink entry to compute string bytes + const entry = self.hyperlink_set.get(self.memory, id); + string_bytes += StringAlloc.bytesRequired(u8, entry.uri.len); + + switch (entry.id) { + .implicit => {}, + .explicit => |slice| { + string_bytes += StringAlloc.bytesRequired(u8, slice.len); + }, + } + } + } + } + } + } + + // The hyperlink_map capacity in layout() is computed as: + // hyperlink_count * hyperlink_cell_multiplier (rounded to power of 2) + // We need enough hyperlink_bytes so that when layout() computes + // the map capacity, it can accommodate all hyperlink cells. This + // is unit tested. + const hyperlink_cap = cap: { + const hyperlink_count = id_set.count(); + const hyperlink_set_cap = hyperlink.Set.capacityForCount(hyperlink_count); + const hyperlink_map_min = std.math.divCeil( + usize, + hyperlink_cells, + hyperlink_cell_multiplier, + ) catch 0; + break :cap @max(hyperlink_set_cap, hyperlink_map_min); + }; + + // All the intCasts below are safe because we should have a + // capacity strictly less than or equal to this page's capacity. + return .{ + .cols = self.size.cols, + .rows = @intCast(y_end - y_start), + .styles = @intCast(styles_cap), + .grapheme_bytes = @intCast(grapheme_bytes), + .hyperlink_bytes = @intCast(hyperlink_cap * @sizeOf(hyperlink.Set.Item)), + .string_bytes = @intCast(string_bytes), + }; + } + /// Clone the contents of another page into this page. The capacities /// can be different, but the size of the other page must fit into /// this page. @@ -1556,7 +1665,7 @@ pub const Page = struct { const rows_start = 0; const rows_end: usize = rows_start + (rows_count * @sizeOf(Row)); - const cells_count: usize = @intCast(cap.cols * cap.rows); + const cells_count: usize = @as(usize, cap.cols) * @as(usize, cap.rows); const cells_start = alignForward(usize, rows_end, @alignOf(Cell)); const cells_end = cells_start + (cells_count * @sizeOf(Cell)); @@ -1568,7 +1677,13 @@ pub const Page = struct { const grapheme_alloc_start = alignForward(usize, styles_end, GraphemeAlloc.base_align.toByteUnits()); const grapheme_alloc_end = grapheme_alloc_start + grapheme_alloc_layout.total_size; - const grapheme_count = @divFloor(cap.grapheme_bytes, grapheme_chunk); + const grapheme_count: usize = count: { + if (cap.grapheme_bytes == 0) break :count 0; + // Use divCeil to match GraphemeAlloc.layout() which uses alignForward, + // ensuring grapheme_map has capacity when grapheme_alloc has chunks. + const base = std.math.divCeil(usize, cap.grapheme_bytes, grapheme_chunk) catch unreachable; + break :count std.math.ceilPowerOfTwo(usize, base) catch unreachable; + }; const grapheme_map_layout = GraphemeMap.layout(@intCast(grapheme_count)); const grapheme_map_start = alignForward(usize, grapheme_alloc_end, GraphemeMap.base_align.toByteUnits()); const grapheme_map_end = grapheme_map_start + grapheme_map_layout.total_size; @@ -1638,67 +1753,74 @@ pub const Size = struct { }; /// Capacity of this page. +/// +/// This capacity can be maxed out (every field max) and still fit +/// within a 64-bit memory space. If you need more than this, you will +/// need to split data across separate pages. +/// +/// For 32-bit systems, it is possible to overflow the addressable +/// space and this is something we still need to address in the future +/// likely by limiting the maximum capacity on 32-bit systems further. pub const Capacity = struct { /// Number of columns and rows we can know about. cols: size.CellCountInt, rows: size.CellCountInt, /// Number of unique styles that can be used on this page. - styles: usize = 16, + styles: size.StyleCountInt = 16, /// Number of bytes to allocate for hyperlink data. Note that the /// amount of data used for hyperlinks in total is more than this because /// hyperlinks use string data as well as a small amount of lookup metadata. /// This number is a rough approximation. - hyperlink_bytes: usize = hyperlink_bytes_default, + hyperlink_bytes: size.HyperlinkCountInt = hyperlink_bytes_default, /// Number of bytes to allocate for grapheme data. - grapheme_bytes: usize = grapheme_bytes_default, + grapheme_bytes: size.GraphemeBytesInt = grapheme_bytes_default, /// Number of bytes to allocate for strings. - string_bytes: usize = string_bytes_default, + string_bytes: size.StringBytesInt = string_bytes_default, pub const Adjustment = struct { cols: ?size.CellCountInt = null, }; + /// Returns the maximum number of columns that can be used with this + /// capacity while still fitting at least one row. Returns null if even + /// a single column cannot fit (which would indicate an unusable capacity). + /// + /// Note that this is the maximum number of columns that never increases + /// the amount of memory the original capacity will take. If you modify + /// the original capacity to add rows, then you can fit more columns. + pub fn maxCols(self: Capacity) ?size.CellCountInt { + const available_bits = self.availableBitsForGrid(); + + // If we can't even fit the row metadata, return null + if (available_bits <= @bitSizeOf(Row)) return null; + + // We do the math of how many columns we can fit in the remaining + // bits ignoring the metadata of a row. + const remaining_bits = available_bits - @bitSizeOf(Row); + const max_cols = remaining_bits / @bitSizeOf(Cell); + + // Clamp to CellCountInt max + return @min(std.math.maxInt(size.CellCountInt), max_cols); + } + /// Adjust the capacity parameters while retaining the same total size. + /// /// Adjustments always happen by limiting the rows in the page. Everything /// else can grow. If it is impossible to achieve the desired adjustment, /// OutOfMemory is returned. pub fn adjust(self: Capacity, req: Adjustment) Allocator.Error!Capacity { var adjusted = self; if (req.cols) |cols| { - // The math below only works if there is no alignment gap between - // the end of the rows array and the start of the cells array. - // - // To guarantee this, we assert that Row's size is a multiple of - // Cell's alignment, so that any length array of Rows will end on - // a valid alignment for the start of the Cell array. - assert(@sizeOf(Row) % @alignOf(Cell) == 0); - - const layout = Page.layout(self); - - // In order to determine the amount of space in the page available - // for rows & cells (which will allow us to calculate the number of - // rows we can fit at a certain column width) we need to layout the - // "meta" members of the page (i.e. everything else) from the end. - const hyperlink_map_start = alignBackward(usize, layout.total_size - layout.hyperlink_map_layout.total_size, hyperlink.Map.base_align.toByteUnits()); - const hyperlink_set_start = alignBackward(usize, hyperlink_map_start - layout.hyperlink_set_layout.total_size, hyperlink.Set.base_align.toByteUnits()); - const string_alloc_start = alignBackward(usize, hyperlink_set_start - layout.string_alloc_layout.total_size, StringAlloc.base_align.toByteUnits()); - const grapheme_map_start = alignBackward(usize, string_alloc_start - layout.grapheme_map_layout.total_size, GraphemeMap.base_align.toByteUnits()); - const grapheme_alloc_start = alignBackward(usize, grapheme_map_start - layout.grapheme_alloc_layout.total_size, GraphemeAlloc.base_align.toByteUnits()); - const styles_start = alignBackward(usize, grapheme_alloc_start - layout.styles_layout.total_size, StyleSet.base_align.toByteUnits()); + const available_bits = self.availableBitsForGrid(); // The size per row is: // - The row metadata itself // - The cells per row (n=cols) - const bits_per_row: usize = size: { - var bits: usize = @bitSizeOf(Row); // Row metadata - bits += @bitSizeOf(Cell) * @as(usize, @intCast(cols)); // Cells (n=cols) - break :size bits; - }; - const available_bits: usize = styles_start * 8; + const bits_per_row: usize = @bitSizeOf(Row) + @bitSizeOf(Cell) * @as(usize, @intCast(cols)); const new_rows: usize = @divFloor(available_bits, bits_per_row); // If our rows go to zero then we can't fit any row metadata @@ -1711,6 +1833,34 @@ pub const Capacity = struct { return adjusted; } + + /// Computes the number of bits available for rows and cells in the page. + /// + /// This is done by laying out the "meta" members (styles, graphemes, + /// hyperlinks, strings) from the end of the page and finding where they + /// start, which gives us the space available for rows and cells. + fn availableBitsForGrid(self: Capacity) usize { + // The math below only works if there is no alignment gap between + // the end of the rows array and the start of the cells array. + // + // To guarantee this, we assert that Row's size is a multiple of + // Cell's alignment, so that any length array of Rows will end on + // a valid alignment for the start of the Cell array. + assert(@sizeOf(Row) % @alignOf(Cell) == 0); + + const l = Page.layout(self); + + // Layout meta members from the end to find styles_start + const hyperlink_map_start = alignBackward(usize, l.total_size - l.hyperlink_map_layout.total_size, hyperlink.Map.base_align.toByteUnits()); + const hyperlink_set_start = alignBackward(usize, hyperlink_map_start - l.hyperlink_set_layout.total_size, hyperlink.Set.base_align.toByteUnits()); + const string_alloc_start = alignBackward(usize, hyperlink_set_start - l.string_alloc_layout.total_size, StringAlloc.base_align.toByteUnits()); + const grapheme_map_start = alignBackward(usize, string_alloc_start - l.grapheme_map_layout.total_size, GraphemeMap.base_align.toByteUnits()); + const grapheme_alloc_start = alignBackward(usize, grapheme_map_start - l.grapheme_alloc_layout.total_size, GraphemeAlloc.base_align.toByteUnits()); + const styles_start = alignBackward(usize, grapheme_alloc_start - l.styles_layout.total_size, StyleSet.base_align.toByteUnits()); + + // Multiply by 8 to convert bytes to bits + return styles_start * 8; + } }; pub const Row = packed struct(u64) { @@ -1997,6 +2147,21 @@ pub const Cell = packed struct(u64) { // //const pages = total_size / std.heap.page_size_min; // } +test "Page.layout can take a maxed capacity" { + // Our intention is for a maxed-out capacity to always fit + // within a page layout without triggering runtime safety on any + // overflow. This simplifies some of our handling downstream of the + // call (relevant to: https://github.com/ghostty-org/ghostty/issues/10258) + var cap: Capacity = undefined; + inline for (@typeInfo(Capacity).@"struct".fields) |field| { + @field(cap, field.name) = std.math.maxInt(field.type); + } + + // Note that a max capacity will exceed our max_page_size so we + // can't init a page with it, but it should layout. + _ = Page.layout(cap); +} + test "Cell is zero by default" { const cell = Cell.init(0); const cell_int: u64 = @bitCast(cell); @@ -2070,6 +2235,40 @@ test "Page capacity adjust cols too high" { ); } +test "Capacity maxCols basic" { + const cap = std_capacity; + const max = cap.maxCols().?; + + // maxCols should be >= current cols (since current capacity is valid) + try testing.expect(max >= cap.cols); + + // Adjusting to maxCols should succeed with at least 1 row + const adjusted = try cap.adjust(.{ .cols = max }); + try testing.expect(adjusted.rows >= 1); + + // Adjusting to maxCols + 1 should fail + try testing.expectError( + error.OutOfMemory, + cap.adjust(.{ .cols = max + 1 }), + ); +} + +test "Capacity maxCols preserves total size" { + const cap = std_capacity; + const original_size = Page.layout(cap).total_size; + const max = cap.maxCols().?; + const adjusted = try cap.adjust(.{ .cols = max }); + const adjusted_size = Page.layout(adjusted).total_size; + try testing.expectEqual(original_size, adjusted_size); +} + +test "Capacity maxCols with 1 row exactly" { + const cap = std_capacity; + const max = cap.maxCols().?; + const adjusted = try cap.adjust(.{ .cols = max }); + try testing.expectEqual(@as(size.CellCountInt, 1), adjusted.rows); +} + test "Page init" { var page = try Page.init(.{ .cols = 120, @@ -3129,3 +3328,512 @@ test "Page verifyIntegrity zero cols" { page.verifyIntegrity(testing.allocator), ); } + +test "Page exactRowCapacity empty rows" { + var page = try Page.init(.{ + .cols = 10, + .rows = 10, + .styles = 8, + .hyperlink_bytes = 32 * @sizeOf(hyperlink.Set.Item), + .string_bytes = 512, + }); + defer page.deinit(); + + // Empty page: all capacity fields should be 0 (except cols/rows) + const cap = page.exactRowCapacity(0, 5); + try testing.expectEqual(10, cap.cols); + try testing.expectEqual(5, cap.rows); + try testing.expectEqual(0, cap.styles); + try testing.expectEqual(0, cap.grapheme_bytes); + try testing.expectEqual(0, cap.hyperlink_bytes); + try testing.expectEqual(0, cap.string_bytes); +} + +test "Page exactRowCapacity styles" { + var page = try Page.init(.{ + .cols = 10, + .rows = 10, + .styles = 8, + }); + defer page.deinit(); + + // No styles: capacity should be 0 + { + const cap = page.exactRowCapacity(0, 5); + try testing.expectEqual(0, cap.styles); + } + + // Add one style to a cell + const style1_id = try page.styles.add(page.memory, .{ .flags = .{ .bold = true } }); + { + const rac = page.getRowAndCell(0, 0); + rac.row.styled = true; + rac.cell.style_id = style1_id; + } + + // One unique style - capacity accounts for load factor + const cap_one_style = page.exactRowCapacity(0, 5); + { + try testing.expectEqual(StyleSet.capacityForCount(1), cap_one_style.styles); + } + + // Add same style to another cell (duplicate) - capacity unchanged + { + const rac = page.getRowAndCell(1, 0); + rac.cell.style_id = style1_id; + } + { + const cap = page.exactRowCapacity(0, 5); + try testing.expectEqual(cap_one_style.styles, cap.styles); + } + + // Add a different style + const style2_id = try page.styles.add(page.memory, .{ .flags = .{ .italic = true } }); + { + const rac = page.getRowAndCell(2, 0); + rac.cell.style_id = style2_id; + } + + // Two unique styles - capacity accounts for load factor + const cap_two_styles = page.exactRowCapacity(0, 5); + { + try testing.expectEqual(StyleSet.capacityForCount(2), cap_two_styles.styles); + try testing.expect(cap_two_styles.styles > cap_one_style.styles); + } + + // Style outside the row range should not be counted + { + const rac = page.getRowAndCell(0, 7); + rac.row.styled = true; + rac.cell.style_id = try page.styles.add(page.memory, .{ .flags = .{ .underline = .single } }); + } + { + const cap = page.exactRowCapacity(0, 5); + try testing.expectEqual(cap_two_styles.styles, cap.styles); + } + + // Full range includes the new style + { + const cap = page.exactRowCapacity(0, 10); + try testing.expectEqual(StyleSet.capacityForCount(3), cap.styles); + } + + // Verify clone works with exact capacity and produces same result + { + const cap = page.exactRowCapacity(0, 5); + var cloned = try Page.init(cap); + defer cloned.deinit(); + for (0..5) |y| { + const src_row = &page.rows.ptr(page.memory)[y]; + const dst_row = &cloned.rows.ptr(cloned.memory)[y]; + try cloned.cloneRowFrom(&page, dst_row, src_row); + } + const cloned_cap = cloned.exactRowCapacity(0, 5); + try testing.expectEqual(cap, cloned_cap); + } +} + +test "Page exactRowCapacity single style clone" { + // Regression test: verify a single style can be cloned with exact capacity. + // This tests that capacityForCount properly accounts for ID 0 being reserved. + var page = try Page.init(.{ + .cols = 10, + .rows = 2, + .styles = 8, + }); + defer page.deinit(); + + // Add exactly one style to row 0 + const style_id = try page.styles.add(page.memory, .{ .flags = .{ .bold = true } }); + { + const rac = page.getRowAndCell(0, 0); + rac.row.styled = true; + rac.cell.style_id = style_id; + } + + // exactRowCapacity for just row 0 should give capacity for 1 style + const cap = page.exactRowCapacity(0, 1); + try testing.expectEqual(StyleSet.capacityForCount(1), cap.styles); + + // Create a new page with exact capacity and clone + var cloned = try Page.init(cap); + defer cloned.deinit(); + + const src_row = &page.rows.ptr(page.memory)[0]; + const dst_row = &cloned.rows.ptr(cloned.memory)[0]; + + // This must not fail with StyleSetOutOfMemory + try cloned.cloneRowFrom(&page, dst_row, src_row); + + // Verify the style was cloned correctly + const cloned_cell = &cloned.rows.ptr(cloned.memory)[0].cells.ptr(cloned.memory)[0]; + try testing.expect(cloned_cell.style_id != stylepkg.default_id); +} + +test "Page exactRowCapacity styles max single row" { + var page = try Page.init(.{ + .cols = std.math.maxInt(size.CellCountInt), + .rows = 1, + .styles = std.math.maxInt(size.StyleCountInt), + }); + defer page.deinit(); + + // Style our first row + const row = &page.rows.ptr(page.memory)[0]; + row.styled = true; + + // Fill cells with styles until we get OOM, but limit to a reasonable count + // to avoid overflow when computing capacityForCount near maxInt + const cells = row.cells.ptr(page.memory)[0..page.size.cols]; + var count: usize = 0; + const max_count: usize = 1000; // Limit to avoid overflow in capacity calculation + for (cells, 0..) |*cell, i| { + if (count >= max_count) break; + const style_id = page.styles.add(page.memory, .{ + .fg_color = .{ .rgb = .{ + .r = @intCast(i & 0xFF), + .g = @intCast((i >> 8) & 0xFF), + .b = 0, + } }, + }) catch break; + cell.style_id = style_id; + count += 1; + } + + // Verify we added a meaningful number of styles + try testing.expect(count > 0); + + // Capacity should be at least count (adjusted for load factor) + const cap = page.exactRowCapacity(0, 1); + try testing.expectEqual(StyleSet.capacityForCount(count), cap.styles); +} + +test "Page exactRowCapacity grapheme_bytes" { + var page = try Page.init(.{ + .cols = 10, + .rows = 10, + .styles = 8, + }); + defer page.deinit(); + + // No graphemes: capacity should be 0 + { + const cap = page.exactRowCapacity(0, 5); + try testing.expectEqual(0, cap.grapheme_bytes); + } + + // Add one grapheme (1 codepoint) to a cell - rounds up to grapheme_chunk + { + const rac = page.getRowAndCell(0, 0); + rac.cell.* = .init('a'); + try page.appendGrapheme(rac.row, rac.cell, 0x0301); // combining acute accent + } + { + const cap = page.exactRowCapacity(0, 5); + // 1 codepoint = 4 bytes, rounds up to grapheme_chunk (16) + try testing.expectEqual(grapheme_chunk, cap.grapheme_bytes); + } + + // Add another grapheme to a different cell - should sum + { + const rac = page.getRowAndCell(1, 0); + rac.cell.* = .init('e'); + try page.appendGrapheme(rac.row, rac.cell, 0x0300); // combining grave accent + } + { + const cap = page.exactRowCapacity(0, 5); + // 2 graphemes, each 1 codepoint = 2 * grapheme_chunk + try testing.expectEqual(grapheme_chunk * 2, cap.grapheme_bytes); + } + + // Add a larger grapheme (multiple codepoints) that fits in one chunk + { + const rac = page.getRowAndCell(2, 0); + rac.cell.* = .init('o'); + try page.appendGrapheme(rac.row, rac.cell, 0x0301); + try page.appendGrapheme(rac.row, rac.cell, 0x0302); + try page.appendGrapheme(rac.row, rac.cell, 0x0303); + } + { + const cap = page.exactRowCapacity(0, 5); + // First two cells: 2 * grapheme_chunk + // Third cell: 3 codepoints = 12 bytes, rounds up to grapheme_chunk + try testing.expectEqual(grapheme_chunk * 3, cap.grapheme_bytes); + } + + // Grapheme outside the row range should not be counted + { + const rac = page.getRowAndCell(0, 7); + rac.cell.* = .init('x'); + try page.appendGrapheme(rac.row, rac.cell, 0x0304); + } + { + const cap = page.exactRowCapacity(0, 5); + try testing.expectEqual(grapheme_chunk * 3, cap.grapheme_bytes); + } + + // Full range includes the new grapheme + { + const cap = page.exactRowCapacity(0, 10); + try testing.expectEqual(grapheme_chunk * 4, cap.grapheme_bytes); + } + + // Verify clone works with exact capacity and produces same result + { + const cap = page.exactRowCapacity(0, 5); + var cloned = try Page.init(cap); + defer cloned.deinit(); + for (0..5) |y| { + const src_row = &page.rows.ptr(page.memory)[y]; + const dst_row = &cloned.rows.ptr(cloned.memory)[y]; + try cloned.cloneRowFrom(&page, dst_row, src_row); + } + const cloned_cap = cloned.exactRowCapacity(0, 5); + try testing.expectEqual(cap, cloned_cap); + } +} + +test "Page exactRowCapacity grapheme_bytes larger than chunk" { + var page = try Page.init(.{ + .cols = 10, + .rows = 10, + .styles = 8, + }); + defer page.deinit(); + + // Add a grapheme larger than one chunk (grapheme_chunk_len = 4 codepoints) + const rac = page.getRowAndCell(0, 0); + rac.cell.* = .init('a'); + + // Add 6 codepoints - requires 2 chunks (6 * 4 = 24 bytes, rounds up to 32) + for (0..6) |i| { + try page.appendGrapheme(rac.row, rac.cell, @intCast(0x0300 + i)); + } + + const cap = page.exactRowCapacity(0, 1); + // 6 codepoints = 24 bytes, alignForward(24, 16) = 32 + try testing.expectEqual(32, cap.grapheme_bytes); + + // Verify clone works with exact capacity and produces same result + var cloned = try Page.init(cap); + defer cloned.deinit(); + const src_row = &page.rows.ptr(page.memory)[0]; + const dst_row = &cloned.rows.ptr(cloned.memory)[0]; + try cloned.cloneRowFrom(&page, dst_row, src_row); + const cloned_cap = cloned.exactRowCapacity(0, 1); + try testing.expectEqual(cap, cloned_cap); +} + +test "Page exactRowCapacity hyperlinks" { + var page = try Page.init(.{ + .cols = 10, + .rows = 10, + .styles = 8, + .hyperlink_bytes = 32 * @sizeOf(hyperlink.Set.Item), + .string_bytes = 512, + }); + defer page.deinit(); + + // No hyperlinks: capacity should be 0 + { + const cap = page.exactRowCapacity(0, 5); + try testing.expectEqual(0, cap.hyperlink_bytes); + try testing.expectEqual(0, cap.string_bytes); + } + + // Add one hyperlink with implicit ID + const uri1 = "https://example.com"; + const id1 = blk: { + const rac = page.getRowAndCell(0, 0); + + // Create and add hyperlink entry + const id = try page.insertHyperlink(.{ + .id = .{ .implicit = 1 }, + .uri = uri1, + }); + try page.setHyperlink(rac.row, rac.cell, id); + break :blk id; + }; + // 1 hyperlink - capacity accounts for load factor + const cap_one_link = page.exactRowCapacity(0, 5); + { + try testing.expectEqual(hyperlink.Set.capacityForCount(1) * @sizeOf(hyperlink.Set.Item), cap_one_link.hyperlink_bytes); + // URI "https://example.com" = 19 bytes, rounds up to string_chunk (32) + try testing.expectEqual(string_chunk, cap_one_link.string_bytes); + } + + // Add same hyperlink to another cell (duplicate ID) - capacity unchanged + { + const rac = page.getRowAndCell(1, 0); + + // Use the same hyperlink ID for another cell + page.hyperlink_set.use(page.memory, id1); + try page.setHyperlink(rac.row, rac.cell, id1); + } + { + const cap = page.exactRowCapacity(0, 5); + try testing.expectEqual(cap_one_link.hyperlink_bytes, cap.hyperlink_bytes); + try testing.expectEqual(cap_one_link.string_bytes, cap.string_bytes); + } + + // Add a different hyperlink with explicit ID + const uri2 = "https://other.example.org/path"; + const explicit_id = "my-link-id"; + { + const rac = page.getRowAndCell(2, 0); + + const id = try page.insertHyperlink(.{ + .id = .{ .explicit = explicit_id }, + .uri = uri2, + }); + try page.setHyperlink(rac.row, rac.cell, id); + } + // 2 hyperlinks - capacity accounts for load factor + const cap_two_links = page.exactRowCapacity(0, 5); + { + try testing.expectEqual(hyperlink.Set.capacityForCount(2) * @sizeOf(hyperlink.Set.Item), cap_two_links.hyperlink_bytes); + // First URI: 19 bytes -> 32, Second URI: 30 bytes -> 32, Explicit ID: 10 bytes -> 32 + try testing.expectEqual(string_chunk * 3, cap_two_links.string_bytes); + } + + // Hyperlink outside the row range should not be counted + { + const rac = page.getRowAndCell(0, 7); // row 7 is outside range [0, 5) + + const id = try page.insertHyperlink(.{ + .id = .{ .implicit = 99 }, + .uri = "https://outside.example.com", + }); + try page.setHyperlink(rac.row, rac.cell, id); + } + { + const cap = page.exactRowCapacity(0, 5); + try testing.expectEqual(cap_two_links.hyperlink_bytes, cap.hyperlink_bytes); + try testing.expectEqual(cap_two_links.string_bytes, cap.string_bytes); + } + + // Full range includes the new hyperlink + { + const cap = page.exactRowCapacity(0, 10); + try testing.expectEqual(hyperlink.Set.capacityForCount(3) * @sizeOf(hyperlink.Set.Item), cap.hyperlink_bytes); + // Third URI: 27 bytes -> 32 + try testing.expectEqual(string_chunk * 4, cap.string_bytes); + } + + // Verify clone works with exact capacity and produces same result + { + const cap = page.exactRowCapacity(0, 5); + var cloned = try Page.init(cap); + defer cloned.deinit(); + for (0..5) |y| { + const src_row = &page.rows.ptr(page.memory)[y]; + const dst_row = &cloned.rows.ptr(cloned.memory)[y]; + try cloned.cloneRowFrom(&page, dst_row, src_row); + } + const cloned_cap = cloned.exactRowCapacity(0, 5); + try testing.expectEqual(cap, cloned_cap); + } +} + +test "Page exactRowCapacity single hyperlink clone" { + // Regression test: verify a single hyperlink can be cloned with exact capacity. + // This tests that capacityForCount properly accounts for ID 0 being reserved. + var page = try Page.init(.{ + .cols = 10, + .rows = 2, + .styles = 8, + .hyperlink_bytes = 32 * @sizeOf(hyperlink.Set.Item), + .string_bytes = 512, + }); + defer page.deinit(); + + // Add exactly one hyperlink to row 0 + const uri = "https://example.com"; + const id = blk: { + const rac = page.getRowAndCell(0, 0); + const link_id = try page.insertHyperlink(.{ + .id = .{ .implicit = 1 }, + .uri = uri, + }); + try page.setHyperlink(rac.row, rac.cell, link_id); + break :blk link_id; + }; + _ = id; + + // exactRowCapacity for just row 0 should give capacity for 1 hyperlink + const cap = page.exactRowCapacity(0, 1); + try testing.expectEqual(hyperlink.Set.capacityForCount(1) * @sizeOf(hyperlink.Set.Item), cap.hyperlink_bytes); + + // Create a new page with exact capacity and clone + var cloned = try Page.init(cap); + defer cloned.deinit(); + + const src_row = &page.rows.ptr(page.memory)[0]; + const dst_row = &cloned.rows.ptr(cloned.memory)[0]; + + // This must not fail with HyperlinkSetOutOfMemory + try cloned.cloneRowFrom(&page, dst_row, src_row); + + // Verify the hyperlink was cloned correctly + const cloned_cell = &cloned.rows.ptr(cloned.memory)[0].cells.ptr(cloned.memory)[0]; + try testing.expect(cloned_cell.hyperlink); +} + +test "Page exactRowCapacity hyperlink map capacity for many cells" { + // A single hyperlink spanning many cells requires hyperlink_map capacity + // based on cell count, not unique hyperlink count. + const cols = 50; + var page = try Page.init(.{ + .cols = cols, + .rows = 2, + .styles = 8, + .hyperlink_bytes = 32 * @sizeOf(hyperlink.Set.Item), + .string_bytes = 512, + }); + defer page.deinit(); + + // Add one hyperlink spanning all 50 columns in row 0 + const uri = "https://example.com"; + const id = blk: { + const rac = page.getRowAndCell(0, 0); + const link_id = try page.insertHyperlink(.{ + .id = .{ .implicit = 1 }, + .uri = uri, + }); + try page.setHyperlink(rac.row, rac.cell, link_id); + break :blk link_id; + }; + + // Apply same hyperlink to remaining cells in row 0 + for (1..cols) |x| { + const rac = page.getRowAndCell(@intCast(x), 0); + page.hyperlink_set.use(page.memory, id); + try page.setHyperlink(rac.row, rac.cell, id); + } + + // exactRowCapacity must account for 50 hyperlink cells, not just 1 unique hyperlink + const cap = page.exactRowCapacity(0, 1); + + // The hyperlink_bytes must be large enough that layout() computes sufficient + // hyperlink_map capacity. With hyperlink_cell_multiplier=16, we need at least + // ceil(50/16) = 4 hyperlink entries worth of bytes for the map. + const min_for_map = std.math.divCeil(usize, cols, hyperlink_cell_multiplier) catch 0; + const min_hyperlink_bytes = min_for_map * @sizeOf(hyperlink.Set.Item); + try testing.expect(cap.hyperlink_bytes >= min_hyperlink_bytes); + + // Create a new page with exact capacity and clone - must not fail + var cloned = try Page.init(cap); + defer cloned.deinit(); + + const src_row = &page.rows.ptr(page.memory)[0]; + const dst_row = &cloned.rows.ptr(cloned.memory)[0]; + + // This must not fail with HyperlinkMapOutOfMemory + try cloned.cloneRowFrom(&page, dst_row, src_row); + + // Verify all hyperlinks were cloned correctly + for (0..cols) |x| { + const cloned_cell = &cloned.rows.ptr(cloned.memory)[0].cells.ptr(cloned.memory)[x]; + try testing.expect(cloned_cell.hyperlink); + } +} diff --git a/src/terminal/ref_counted_set.zig b/src/terminal/ref_counted_set.zig index e67682ff5..8040039ae 100644 --- a/src/terminal/ref_counted_set.zig +++ b/src/terminal/ref_counted_set.zig @@ -64,6 +64,20 @@ pub fn RefCountedSet( @alignOf(Id), )); + /// This is the max load until the set returns OutOfMemory and + /// requires more capacity. + /// + /// Experimentally, this load factor works quite well. + pub const load_factor = 0.8125; + + /// Returns the minimum capacity needed to store `n` items, + /// accounting for the load factor and the reserved ID 0. + pub fn capacityForCount(n: usize) usize { + if (n == 0) return 0; + // +1 because ID 0 is reserved, so we need at least n+1 slots. + return @intFromFloat(@ceil(@as(f64, @floatFromInt(n + 1)) / load_factor)); + } + /// Set item pub const Item = struct { /// The value this item represents. @@ -154,9 +168,6 @@ pub fn RefCountedSet( /// The returned layout `cap` property will be 1 more than the number /// of items that the set can actually store, since ID 0 is reserved. pub fn init(cap: usize) Layout { - // Experimentally, this load factor works quite well. - const load_factor = 0.8125; - assert(cap <= @as(usize, @intCast(std.math.maxInt(Id))) + 1); // Zero-cap set is valid, return special case @@ -215,7 +226,7 @@ pub fn RefCountedSet( OutOfMemory, /// The set needs to be rehashed, as there are many dead - /// items with lower IDs which are inaccessible for re-use. + /// items with lower IDs which are inaccessible for reuse. NeedsRehash, }; @@ -437,7 +448,7 @@ pub fn RefCountedSet( } /// Delete an item, removing any references from - /// the table, and freeing its ID to be re-used. + /// the table, and freeing its ID to be reused. fn deleteItem(self: *Self, base: anytype, id: Id, ctx: Context) void { const table = self.table.ptr(base); const items = self.items.ptr(base); @@ -585,7 +596,7 @@ pub fn RefCountedSet( const item = &items[id]; // If there's a dead item then we resurrect it - // for our value so that we can re-use its ID, + // for our value so that we can reuse its ID, // unless its ID is greater than the one we're // given (i.e. prefer smaller IDs). if (item.meta.ref == 0) { @@ -645,7 +656,7 @@ pub fn RefCountedSet( } // Our chosen ID may have changed if we decided - // to re-use a dead item's ID, so we make sure + // to reuse a dead item's ID, so we make sure // the chosen bucket contains the correct ID. table[new_item.meta.bucket] = chosen_id; diff --git a/src/terminal/render.zig b/src/terminal/render.zig index b6430ea34..9d75fe4b7 100644 --- a/src/terminal/render.zig +++ b/src/terminal/render.zig @@ -816,12 +816,19 @@ pub const RenderState = struct { const row_pins = row_slice.items(.pin); const row_cells = row_slice.items(.cells); + // Our viewport point is sent in by the caller and can't be trusted. + // If it is outside the valid area then just return empty because + // we can't possibly have a link there. + if (viewport_point.x >= self.cols or + viewport_point.y >= row_pins.len) return result; + // Grab our link ID - const link_page: *page.Page = &row_pins[viewport_point.y].node.data; + const link_pin: PageList.Pin = row_pins[viewport_point.y]; + const link_page: *page.Page = &link_pin.node.data; const link = link: { const rac = link_page.getRowAndCell( viewport_point.x, - viewport_point.y, + link_pin.y, ); // The likely scenario is that our mouse isn't even over a link. @@ -848,7 +855,7 @@ pub const RenderState = struct { const other_page: *page.Page = &pin.node.data; const other = link: { - const rac = other_page.getRowAndCell(x, y); + const rac = other_page.getRowAndCell(x, pin.y); const link_id = other_page.lookupHyperlink(rac.cell) orelse continue; break :link other_page.hyperlink_set.get( other_page.memory, @@ -1317,6 +1324,86 @@ test "string" { try testing.expectEqualStrings(expected, result); } +test "linkCells with scrollback spanning pages" { + const testing = std.testing; + const alloc = testing.allocator; + + const viewport_rows: size.CellCountInt = 10; + const tail_rows: size.CellCountInt = 5; + + var t = try Terminal.init(alloc, .{ + .cols = page.std_capacity.cols, + .rows = viewport_rows, + .max_scrollback = 10_000, + }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + + const pages = &t.screens.active.pages; + const first_page_cap = pages.pages.first.?.data.capacity.rows; + + // Fill first page + for (0..first_page_cap - 1) |_| try s.nextSlice("\r\n"); + + // Create second page with hyperlink + try s.nextSlice("\r\n"); + try s.nextSlice("\x1b]8;;http://example.com\x1b\\LINK\x1b]8;;\x1b\\"); + for (0..(tail_rows - 1)) |_| try s.nextSlice("\r\n"); + + var state: RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + const expected_viewport_y: usize = viewport_rows - tail_rows; + // BUG: This crashes without the fix + var cells = try state.linkCells(alloc, .{ + .x = 0, + .y = expected_viewport_y, + }); + defer cells.deinit(alloc); + try testing.expectEqual(@as(usize, 4), cells.count()); +} + +test "linkCells with invalid viewport point" { + const testing = std.testing; + const alloc = testing.allocator; + + var t = try Terminal.init(alloc, .{ + .cols = 10, + .rows = 5, + }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + + var state: RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + // Row out of bound + { + var cells = try state.linkCells( + alloc, + .{ .x = 0, .y = t.rows + 10 }, + ); + defer cells.deinit(alloc); + try testing.expectEqual(0, cells.count()); + } + + // Col out of bound + { + var cells = try state.linkCells( + alloc, + .{ .x = t.cols + 10, .y = 0 }, + ); + defer cells.deinit(alloc); + try testing.expectEqual(0, cells.count()); + } +} + test "dirty row resets highlights" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/terminal/search/Thread.zig b/src/terminal/search/Thread.zig index 8f2d73f16..3f5377417 100644 --- a/src/terminal/search/Thread.zig +++ b/src/terminal/search/Thread.zig @@ -257,18 +257,46 @@ fn select(self: *Thread, sel: ScreenSearch.Select) !void { self.opts.mutex.lock(); defer self.opts.mutex.unlock(); - // The selection will trigger a selection change notification - // if it did change. - if (try screen_search.select(sel)) scroll: { - if (screen_search.selected) |m| { - // Selection changed, let's scroll the viewport to see it - // since we have the lock anyways. - const screen = self.opts.terminal.screens.get( - s.last_screen.key, - ) orelse break :scroll; - screen.scroll(.{ .pin = m.highlight.start.* }); + // Make the selection. Ignore the result because we don't + // care if the selection didn't change. + _ = try screen_search.select(sel); + + // Grab our match if we have one. If we don't have a selection + // then we do nothing. + const flattened = screen_search.selectedMatch() orelse return; + + // No matter what we reset our selected match cache. This will + // trigger a callback which will trigger the renderer to wake up + // so it can be notified the screen scrolled. + s.last_screen.selected = null; + + // Grab the current screen and see if this match is visible within + // the viewport already. If it is, we do nothing. + const screen = self.opts.terminal.screens.get( + s.last_screen.key, + ) orelse return; + + // Grab the viewport. Viewports and selections are usually small + // so this check isn't very expensive, despite appearing O(N^2), + // both Ns are usually equal to 1. + var it = screen.pages.pageIterator( + .right_down, + .{ .viewport = .{} }, + null, + ); + const hl_chunks = flattened.chunks.slice(); + while (it.next()) |chunk| { + for (0..hl_chunks.len) |i| { + const hl_chunk = hl_chunks.get(i); + if (chunk.overlaps(.{ + .node = hl_chunk.node, + .start = hl_chunk.start, + .end = hl_chunk.end, + })) return; } } + + screen.scroll(.{ .pin = flattened.startPin() }); } /// Change the search term to the given value. diff --git a/src/terminal/search/screen.zig b/src/terminal/search/screen.zig index 0ae7f8a1f..74828d879 100644 --- a/src/terminal/search/screen.zig +++ b/src/terminal/search/screen.zig @@ -1,6 +1,7 @@ const std = @import("std"); const assert = @import("../../quirks.zig").inlineAssert; const testing = std.testing; +const tripwire = @import("../../tripwire.zig"); const Allocator = std.mem.Allocator; const point = @import("../point.zig"); const highlight = @import("../highlight.zig"); @@ -17,6 +18,11 @@ const SlidingWindow = @import("sliding_window.zig").SlidingWindow; const log = std.log.scoped(.search_screen); +const reloadActive_tw = tripwire.module(enum { + history_append_new, + history_append_existing, +}, ScreenSearch.reloadActive); + /// Searches for a needle within a Screen, handling active area updates, /// pages being pruned from the screen (e.g. scrollback limits), and more. /// @@ -386,6 +392,8 @@ pub const ScreenSearch = struct { /// /// The caller must hold the necessary locks to access the screen state. pub fn reloadActive(self: *ScreenSearch) Allocator.Error!void { + const tw = reloadActive_tw; + // If our selection pin became garbage it means we scrolled off // the end. Clear our selection and on exit of this function, // try to select the last match. @@ -412,6 +420,12 @@ pub const ScreenSearch = struct { // pages then we need to re-search the pages and add it to // our history results. + // If our screen has no scrollback then we have no history. + if (self.screen.no_scrollback) { + assert(self.history == null); + break :history; + } + const history_: ?*HistorySearch = if (self.history) |*h| state: { // If our start pin became garbage, it means we pruned all // the way up through it, so we have no history anymore. @@ -479,12 +493,16 @@ pub const ScreenSearch = struct { alloc, self.history_results.items.len, ); - errdefer results.deinit(alloc); + errdefer { + for (results.items) |*hl| hl.deinit(alloc); + results.deinit(alloc); + } while (window.next()) |hl| { if (hl.chunks.items(.node)[0] == history_node) continue; var hl_cloned = try hl.clone(alloc); errdefer hl_cloned.deinit(alloc); + try tw.check(.history_append_new); try results.append(alloc, hl_cloned); } @@ -499,6 +517,7 @@ pub const ScreenSearch = struct { // Matches! Reverse our list then append all the remaining // history items that didn't start on our original node. std.mem.reverse(FlattenedHighlight, results.items); + try tw.check(.history_append_existing); try results.appendSlice(alloc, self.history_results.items); self.history_results.deinit(alloc); self.history_results = results; @@ -575,8 +594,43 @@ pub const ScreenSearch = struct { }, } - // Active area search was successful. Now we have to fixup our - // selection if we had one. + // If we have no scrollback, we need to prune any active results + // that aren't in the actual active area. We only do this for the + // no scrollback scenario because with scrollback we actually + // rely on our active search searching by page to find history + // items as well. This is all related to the fact that PageList + // scrollback limits are discrete by page size except we special + // case zero. + if (self.screen.no_scrollback and + self.active_results.items.len > 0) + active_prune: { + const items = self.active_results.items; + const tl = self.screen.pages.getTopLeft(.active); + for (0.., items) |i, *hl| { + if (!tl.before(hl.endPin())) { + // Deinit because its going to be pruned no matter + // what at some point for not being in the active area. + hl.deinit(alloc); + continue; + } + + // In the active area! Since our results are sorted + // that means everything after this is also in the active + // area, so we prune up to this i. + if (i > 0) self.active_results.replaceRangeAssumeCapacity( + 0, + i, + &.{}, + ); + + break :active_prune; + } + + // None are in the active area... + self.active_results.clearRetainingCapacity(); + } + + // Now we have to fixup our selection if we had one. fixup: { const old_idx = old_selection_idx orelse break :fixup; const m = if (self.selected) |*m| m else break :fixup; @@ -1333,3 +1387,130 @@ test "select prev with history" { } }, t.screens.active.pages.pointFromPin(.active, sel.end).?); } } + +test "screen search no scrollback has no history" { + const alloc = testing.allocator; + var t: Terminal = try .init(alloc, .{ + .cols = 10, + .rows = 2, + .max_scrollback = 0, + }); + defer t.deinit(alloc); + + // Alt screen has no scrollback + _ = try t.switchScreen(.alternate); + + var s = t.vtStream(); + defer s.deinit(); + + // This will probably stop working at some point and we'll have + // no way to test it using public APIs, but at the time of writing + // this test, CSI 22 J (scroll complete) pushes into scrollback + // with alt screen. + try s.nextSlice("Fizz\r\n"); + try s.nextSlice("\x1b[22J"); + try s.nextSlice("hello."); + + var search: ScreenSearch = try .init(alloc, t.screens.active, "Fizz"); + defer search.deinit(); + try search.searchAll(); + try testing.expectEqual(0, search.active_results.items.len); + + // Get all matches + const matches = try search.matches(alloc); + defer alloc.free(matches); + try testing.expectEqual(0, matches.len); +} + +test "reloadActive partial history cleanup on appendSlice error" { + // This test verifies that when reloadActive fails at appendSlice (after + // the loop), all FlattenedHighlight items are properly cleaned up. + const alloc = testing.allocator; + var t: Terminal = try .init(alloc, .{ + .cols = 10, + .rows = 2, + .max_scrollback = std.math.maxInt(usize), + }); + defer t.deinit(alloc); + const list: *PageList = &t.screens.active.pages; + + var s = t.vtStream(); + defer s.deinit(); + + // Write multiple "Fizz" matches that will end up in history. + // We need enough content to push "Fizz" entries into scrollback. + try s.nextSlice("Fizz\r\nFizz\r\n"); + while (list.totalPages() < 3) try s.nextSlice("\r\n"); + for (0..list.rows) |_| try s.nextSlice("\r\n"); + try s.nextSlice("Fizz."); + + // Complete initial search + var search: ScreenSearch = try .init(alloc, t.screens.active, "Fizz"); + defer search.deinit(); + try search.searchAll(); + + // Now trigger reloadActive by adding more content that changes the + // active/history boundary. First add more "Fizz" entries to history. + try s.nextSlice("\r\nFizz\r\nFizz\r\n"); + while (list.totalPages() < 4) try s.nextSlice("\r\n"); + for (0..list.rows) |_| try s.nextSlice("\r\n"); + + // Arm the tripwire to fail at appendSlice (after the loop completes). + // At this point, there are FlattenedHighlight items in the results list + // that need cleanup. + const tw = reloadActive_tw; + defer tw.end(.reset) catch unreachable; + tw.errorAlways(.history_append_existing, error.OutOfMemory); + + // reloadActive is called by select(), which should trigger the error path. + // If the bug exists, testing.allocator will report a memory leak + // because FlattenedHighlight items weren't cleaned up. + try testing.expectError(error.OutOfMemory, search.select(.next)); +} + +test "reloadActive partial history cleanup on loop append error" { + // This test verifies that when reloadActive fails inside the loop + // (after some items have been appended), all FlattenedHighlight items + // are properly cleaned up. + const alloc = testing.allocator; + var t: Terminal = try .init(alloc, .{ + .cols = 10, + .rows = 2, + .max_scrollback = std.math.maxInt(usize), + }); + defer t.deinit(alloc); + const list: *PageList = &t.screens.active.pages; + + var s = t.vtStream(); + defer s.deinit(); + + // Write multiple "Fizz" matches that will end up in history. + // We need enough content to push "Fizz" entries into scrollback. + try s.nextSlice("Fizz\r\nFizz\r\n"); + while (list.totalPages() < 3) try s.nextSlice("\r\n"); + for (0..list.rows) |_| try s.nextSlice("\r\n"); + try s.nextSlice("Fizz."); + + // Complete initial search + var search: ScreenSearch = try .init(alloc, t.screens.active, "Fizz"); + defer search.deinit(); + try search.searchAll(); + + // Now trigger reloadActive by adding more content that changes the + // active/history boundary. First add more "Fizz" entries to history. + try s.nextSlice("\r\nFizz\r\nFizz\r\n"); + while (list.totalPages() < 4) try s.nextSlice("\r\n"); + for (0..list.rows) |_| try s.nextSlice("\r\n"); + + // Arm the tripwire to fail after the first loop append succeeds. + // This leaves at least one FlattenedHighlight in the results list + // that needs cleanup. + const tw = reloadActive_tw; + defer tw.end(.reset) catch unreachable; + tw.errorAfter(.history_append_new, error.OutOfMemory, 1); + + // reloadActive is called by select(), which should trigger the error path. + // If the bug exists, testing.allocator will report a memory leak + // because FlattenedHighlight items weren't cleaned up. + try testing.expectError(error.OutOfMemory, search.select(.next)); +} diff --git a/src/terminal/search/sliding_window.zig b/src/terminal/search/sliding_window.zig index 3d64042ce..c3c29e085 100644 --- a/src/terminal/search/sliding_window.zig +++ b/src/terminal/search/sliding_window.zig @@ -575,9 +575,16 @@ pub const SlidingWindow = struct { ); } + // If our written data is empty, then there is nothing to + // add to our data set. + const written = encoded.written(); + if (written.len == 0) { + self.assertIntegrity(); + return 0; + } + // Get our written data. If we're doing a reverse search then we // need to reverse all our encodings. - const written = encoded.written(); switch (self.direction) { .forward => {}, .reverse => { @@ -1637,3 +1644,33 @@ test "SlidingWindow single append reversed soft wrapped" { try testing.expect(w.next() == null); try testing.expect(w.next() == null); } + +// This tests a real bug that occurred where a whitespace-only page +// that encodes to zero bytes would crash. +test "SlidingWindow append whitespace only node" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .forward, "x"); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ + .cols = 80, + .rows = 24, + .max_scrollback = 0, + }); + defer s.deinit(); + + // By setting the empty page to wrap we get a zero-byte page. + // This is invasive but its otherwise hard to reproduce naturally + // without creating a slow test. + const node: *PageList.List.Node = s.pages.pages.first.?; + const last_row = node.data.getRow(node.data.size.rows - 1); + last_row.wrap = true; + + try testing.expect(s.pages.pages.first == s.pages.pages.last); + _ = try w.append(node); + + // No matches expected + try testing.expect(w.next() == null); +} diff --git a/src/terminal/size.zig b/src/terminal/size.zig index 0dedfcc14..7be09739e 100644 --- a/src/terminal/size.zig +++ b/src/terminal/size.zig @@ -11,9 +11,32 @@ pub const max_page_size = std.math.maxInt(u32); /// derived from the maximum terminal page size. pub const OffsetInt = std.math.IntFittingRange(0, max_page_size - 1); -/// The int type that can contain the maximum number of cells in a page. -pub const CellCountInt = u16; // TODO: derive +/// Int types for maximum values of things. A lot of these sizes are +/// based on "X is enough for any reasonable use case" principles. +// The goal is that a user can have the maxInt amount of all of these +// present at one time and be able to address them in a single Page.zig. + +// Total number of cells that are possible in each dimension (row/col). +// Based on 2^16 being enough for any reasonable terminal size and allowing +// IDs to remain 16-bit. +pub const CellCountInt = u16; + +// Total number of styles and hyperlinks that are possible in a page. +// We match CellCountInt here because each cell in a single row can have at +// most one style, making it simple to split a page by splitting rows. // +// Note due to the way RefCountedSet works, we are short one value, but +// this is a theoretical limit we accept. A page with a single row max +// columns wide would be one short of having every cell have a unique style. +pub const StyleCountInt = CellCountInt; +pub const HyperlinkCountInt = CellCountInt; + +// Total number of bytes that can be taken up by grapheme data and string +// data. Both of these technically unlimited with malicious input, but +// we choose a reasonable limit of 2^32 (4GB) per. +pub const GraphemeBytesInt = u32; +pub const StringBytesInt = u32; + /// The offset from the base address of the page to the start of some data. /// This is typed for ease of use. /// diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index ba6b57d5c..d0d2c1bb3 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -111,8 +111,6 @@ pub const Action = union(Key) { apc_start, apc_end, apc_put: u8, - prompt_end, - end_of_input, end_hyperlink, active_status_display: ansi.StatusDisplay, decaln, @@ -122,14 +120,12 @@ pub const Action = union(Key) { progress_report: osc.Command.ProgressReport, start_hyperlink: StartHyperlink, clipboard_contents: ClipboardContents, - prompt_start: PromptStart, - prompt_continuation: PromptContinuation, - end_of_command: EndOfCommand, mouse_shape: MouseShape, configure_charset: ConfigureCharset, set_attribute: sgr.Attribute, kitty_color_report: kitty.color.OSC, color_operation: ColorOperation, + semantic_prompt: SemanticPrompt, pub const Key = lib.Enum( lib_target, @@ -212,8 +208,6 @@ pub const Action = union(Key) { "apc_start", "apc_end", "apc_put", - "prompt_end", - "end_of_input", "end_hyperlink", "active_status_display", "decaln", @@ -223,14 +217,12 @@ pub const Action = union(Key) { "progress_report", "start_hyperlink", "clipboard_contents", - "prompt_start", - "prompt_continuation", - "end_of_command", "mouse_shape", "configure_charset", "set_attribute", "kitty_color_report", "color_operation", + "semantic_prompt", }, ); @@ -391,47 +383,6 @@ pub const Action = union(Key) { } }; - pub const PromptStart = struct { - aid: ?[]const u8, - redraw: bool, - - pub const C = extern struct { - aid: lib.String, - redraw: bool, - }; - - pub fn cval(self: PromptStart) PromptStart.C { - return .{ - .aid = .init(self.aid orelse ""), - .redraw = self.redraw, - }; - } - }; - - pub const PromptContinuation = struct { - aid: ?[]const u8, - - pub const C = lib.String; - - pub fn cval(self: PromptContinuation) PromptContinuation.C { - return .init(self.aid orelse ""); - } - }; - - pub const EndOfCommand = struct { - exit_code: ?u8, - - pub const C = extern struct { - exit_code: i16, - }; - - pub fn cval(self: EndOfCommand) EndOfCommand.C { - return .{ - .exit_code = if (self.exit_code) |code| @intCast(code) else -1, - }; - } - }; - pub const ConfigureCharset = lib.Struct(lib_target, struct { slot: charsets.Slots, charset: charsets.Charset, @@ -448,6 +399,8 @@ pub const Action = union(Key) { return {}; } }; + + pub const SemanticPrompt = osc.Command.SemanticPrompt; }; /// Returns a type that can process a stream of tty control characters. @@ -1641,7 +1594,7 @@ pub fn Stream(comptime Handler: type) type { }, }, else => { - log.warn("invalid set curor style command: {f}", .{input}); + log.warn("invalid set cursor style command: {f}", .{input}); return; }, }; @@ -1988,10 +1941,9 @@ pub fn Stream(comptime Handler: type) type { // 4. hyperlink_start // 5. report_pwd // 6. color_operation - // 7. prompt_start - // 8. prompt_end + // 7. semantic_prompt // - // Together, these 8 commands make up about 96% of all + // Together, these 7 commands make up about 96% of all // OSC commands encountered in real world scenarios. // // Additionally, within the prongs, unlikely branch @@ -2003,6 +1955,11 @@ pub fn Stream(comptime Handler: type) type { // ref: https://github.com/qwerasd205/asciinema-stats switch (cmd) { + .semantic_prompt => |sp| { + @branchHint(.likely); + try self.handler.vt(.semantic_prompt, sp); + }, + .change_window_title => |title| { @branchHint(.likely); if (!std.unicode.utf8ValidateSlice(title)) { @@ -2026,30 +1983,6 @@ pub fn Stream(comptime Handler: type) type { }); }, - .prompt_start => |v| { - @branchHint(.likely); - switch (v.kind) { - .primary, .right => try self.handler.vt(.prompt_start, .{ - .aid = v.aid, - .redraw = v.redraw, - }), - .continuation, .secondary => try self.handler.vt(.prompt_continuation, .{ - .aid = v.aid, - }), - } - }, - - .prompt_end => { - @branchHint(.likely); - try self.handler.vt(.prompt_end, {}); - }, - - .end_of_input => try self.handler.vt(.end_of_input, {}), - - .end_of_command => |end| { - try self.handler.vt(.end_of_command, .{ .exit_code = end.exit_code }); - }, - .report_pwd => |v| { @branchHint(.likely); try self.handler.vt(.report_pwd, .{ .url = v.value }); @@ -2107,8 +2040,13 @@ pub fn Stream(comptime Handler: type) type { .conemu_change_tab_title, .conemu_wait_input, .conemu_guimacro, + .conemu_comment, + .conemu_xterm_emulation, + .conemu_output_environment_variable, + .conemu_run_process, + .kitty_text_sizing, => { - log.warn("unimplemented OSC callback: {}", .{cmd}); + log.debug("unimplemented OSC callback: {}", .{cmd}); }, .invalid => { diff --git a/src/terminal/stream_readonly.zig b/src/terminal/stream_readonly.zig index 3b088e2b7..18ed0dd42 100644 --- a/src/terminal/stream_readonly.zig +++ b/src/terminal/stream_readonly.zig @@ -4,7 +4,7 @@ const stream = @import("stream.zig"); const Action = stream.Action; const Screen = @import("Screen.zig"); const modes = @import("modes.zig"); -const osc_color = @import("osc/color.zig"); +const osc_color = @import("osc/parsers/color.zig"); const kitty_color = @import("kitty/color.zig"); const Terminal = @import("Terminal.zig"); @@ -100,10 +100,10 @@ pub const Handler = struct { .insert_lines => self.terminal.insertLines(value), .insert_blanks => self.terminal.insertBlanks(value), .delete_lines => self.terminal.deleteLines(value), - .scroll_up => self.terminal.scrollUp(value), + .scroll_up => try self.terminal.scrollUp(value), .scroll_down => self.terminal.scrollDown(value), - .horizontal_tab => try self.horizontalTab(value), - .horizontal_tab_back => try self.horizontalTabBack(value), + .horizontal_tab => self.horizontalTab(value), + .horizontal_tab_back => self.horizontalTabBack(value), .tab_clear_current => self.terminal.tabClear(.current), .tab_clear_all => self.terminal.tabClear(.all), .tab_set => self.terminal.tabSet(), @@ -125,7 +125,7 @@ pub const Handler = struct { } }, .save_cursor => self.terminal.saveCursor(), - .restore_cursor => try self.terminal.restoreCursor(), + .restore_cursor => self.terminal.restoreCursor(), .invoke_charset => self.terminal.invokeCharset(value.bank, value.charset, value.locking), .configure_charset => self.terminal.configureCharset(value.slot, value.charset), .set_attribute => switch (value) { @@ -153,14 +153,7 @@ pub const Handler = struct { .full_reset => self.terminal.fullReset(), .start_hyperlink => try self.terminal.screens.active.startHyperlink(value.uri, value.id), .end_hyperlink => self.terminal.screens.active.endHyperlink(), - .prompt_start => { - self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt; - self.terminal.flags.shell_redraws_prompt = value.redraw; - }, - .prompt_continuation => self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt_continuation, - .prompt_end => self.terminal.markSemanticPrompt(.input), - .end_of_input => self.terminal.markSemanticPrompt(.command), - .end_of_command => self.terminal.screens.active.cursor.page_row.semantic_prompt = .input, + .semantic_prompt => self.semanticPrompt(value), .mouse_shape => self.terminal.mouse_shape = value, .color_operation => try self.colorOperation(value.op, &value.requests), .kitty_color_report => try self.kittyColorOperation(value), @@ -200,22 +193,58 @@ pub const Handler = struct { } } - inline fn horizontalTab(self: *Handler, count: u16) !void { + inline fn horizontalTab(self: *Handler, count: u16) void { for (0..count) |_| { const x = self.terminal.screens.active.cursor.x; - try self.terminal.horizontalTab(); + self.terminal.horizontalTab(); if (x == self.terminal.screens.active.cursor.x) break; } } - inline fn horizontalTabBack(self: *Handler, count: u16) !void { + inline fn horizontalTabBack(self: *Handler, count: u16) void { for (0..count) |_| { const x = self.terminal.screens.active.cursor.x; - try self.terminal.horizontalTabBack(); + self.terminal.horizontalTabBack(); if (x == self.terminal.screens.active.cursor.x) break; } } + fn semanticPrompt( + self: *Handler, + cmd: Action.SemanticPrompt, + ) void { + switch (cmd.action) { + .fresh_line_new_prompt => { + const kind = cmd.readOption(.prompt_kind) orelse .initial; + switch (kind) { + .initial, .right => { + self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt; + if (cmd.readOption(.redraw)) |redraw| { + self.terminal.flags.shell_redraws_prompt = redraw; + } + }, + .continuation, .secondary => { + self.terminal.screens.active.cursor.page_row.semantic_prompt = .prompt_continuation; + }, + } + }, + + .end_prompt_start_input => self.terminal.markSemanticPrompt(.input), + .end_input_start_output => self.terminal.markSemanticPrompt(.command), + .end_command => self.terminal.screens.active.cursor.page_row.semantic_prompt = .input, + + // All of these commands weren't previously handled by our + // semantic prompt code. I am PR-ing the parser separate from the + // handling so we just ignore these like we did before, even + // though we should handle them eventually. + .end_prompt_start_input_terminate_eol, + .fresh_line, + .new_command, + .prompt_start, + => {}, + } + } + fn setMode(self: *Handler, mode: modes.Mode, enabled: bool) !void { // Set the mode on the terminal self.terminal.modes.set(mode, enabled); @@ -240,7 +269,7 @@ pub const Handler = struct { .save_cursor => if (enabled) { self.terminal.saveCursor(); } else { - try self.terminal.restoreCursor(); + self.terminal.restoreCursor(); }, .enable_mode_3 => {}, diff --git a/src/terminal/style.zig b/src/terminal/style.zig index e5c47b9fe..7908beefa 100644 --- a/src/terminal/style.zig +++ b/src/terminal/style.zig @@ -11,7 +11,7 @@ const RefCountedSet = @import("ref_counted_set.zig").RefCountedSet; /// The unique identifier for a style. This is at most the number of cells /// that can fit into a terminal page. -pub const Id = size.CellCountInt; +pub const Id = size.StyleCountInt; /// The Id to use for default styling. pub const default_id: Id = 0; diff --git a/src/terminal/tmux.zig b/src/terminal/tmux.zig index 82ef5036b..c7cda1442 100644 --- a/src/terminal/tmux.zig +++ b/src/terminal/tmux.zig @@ -6,6 +6,7 @@ pub const output = @import("tmux/output.zig"); pub const ControlParser = control.Parser; pub const ControlNotification = control.Notification; pub const Layout = layout.Layout; +pub const Viewer = @import("tmux/viewer.zig").Viewer; test { @import("std").testing.refAllDecls(@This()); diff --git a/src/terminal/tmux/control.zig b/src/terminal/tmux/control.zig index 3624173dd..dbc64b340 100644 --- a/src/terminal/tmux/control.zig +++ b/src/terminal/tmux/control.zig @@ -531,6 +531,30 @@ pub const Notification = union(enum) { session_id: usize, name: []const u8, }, + + pub fn format(self: Notification, writer: *std.Io.Writer) !void { + const T = Notification; + const info = @typeInfo(T).@"union"; + + try writer.writeAll(@typeName(T)); + if (info.tag_type) |TagType| { + try writer.writeAll("{ ."); + try writer.writeAll(@tagName(@as(TagType, self))); + try writer.writeAll(" = "); + + inline for (info.fields) |u_field| { + if (self == @field(TagType, u_field.name)) { + const value = @field(self, u_field.name); + switch (u_field.type) { + []const u8 => try writer.print("\"{s}\"", .{std.mem.trim(u8, value, " \t\r\n")}), + else => try writer.print("{any}", .{value}), + } + } + } + + try writer.writeAll(" }"); + } + } }; test "tmux begin/end empty" { diff --git a/src/terminal/tmux/output.zig b/src/terminal/tmux/output.zig index dcfa89ac3..6b8073e44 100644 --- a/src/terminal/tmux/output.zig +++ b/src/terminal/tmux/output.zig @@ -36,6 +36,36 @@ pub fn parseFormatStruct( return result; } +pub fn comptimeFormat( + comptime vars: []const Variable, + comptime delimiter: u8, +) []const u8 { + comptime { + @setEvalBranchQuota(50000); + var counter: std.Io.Writer.Discarding = .init(&.{}); + try format(&counter.writer, vars, delimiter); + + var buf: [counter.count]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try format(&writer, vars, delimiter); + const final = buf; + return final[0..writer.end]; + } +} + +/// Format a set of variables into the proper format string for tmux +/// that we can handle with `parseFormatStruct`. +pub fn format( + writer: *std.Io.Writer, + vars: []const Variable, + delimiter: u8, +) std.Io.Writer.Error!void { + for (vars, 0..) |variable, i| { + if (i != 0) try writer.writeByte(delimiter); + try writer.print("#{{{t}}}", .{variable}); + } +} + /// Returns a struct type that contains fields for each of the given /// format variables. This can be used with `parseFormatStruct` to /// parse an output string into a format struct. @@ -65,16 +95,109 @@ pub fn FormatStruct(comptime vars: []const Variable) type { /// a subset of them here that are relevant to the use case of implementing /// control mode for terminal emulators. pub const Variable = enum { + /// 1 if pane is in alternate screen. + alternate_on, + /// Saved cursor X in alternate screen. + alternate_saved_x, + /// Saved cursor Y in alternate screen. + alternate_saved_y, + /// 1 if bracketed paste mode is enabled. + bracketed_paste, + /// 1 if the cursor is blinking. + cursor_blinking, + /// Cursor colour in pane. Possible formats: + /// - Named colors: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, + /// `cyan`, `white`, `default`, `terminal`, or bright variants. + /// - 256 colors: `colour` where N is 0-255 (e.g., `colour100`). + /// - RGB hex: `#RRGGBB` (e.g., `#ff0000`). + /// - Empty string if unset. + cursor_colour, + /// Pane cursor flag. + cursor_flag, + /// Cursor shape in pane. Possible values: `block`, `underline`, `bar`, + /// or `default`. + cursor_shape, + /// Cursor X position in pane. + cursor_x, + /// Cursor Y position in pane. + cursor_y, + /// 1 if focus reporting is enabled. + focus_flag, + /// Pane insert flag. + insert_flag, + /// Pane keypad cursor flag. + keypad_cursor_flag, + /// Pane keypad flag. + keypad_flag, + /// Pane mouse all flag. + mouse_all_flag, + /// Pane mouse any flag. + mouse_any_flag, + /// Pane mouse button flag. + mouse_button_flag, + /// Pane mouse SGR flag. + mouse_sgr_flag, + /// Pane mouse standard flag. + mouse_standard_flag, + /// Pane mouse UTF-8 flag. + mouse_utf8_flag, + /// Pane origin flag. + origin_flag, + /// Unique pane ID prefixed with `%` (e.g., `%0`, `%42`). + pane_id, + /// Pane tab positions as a comma-separated list of 0-indexed column + /// numbers (e.g., `8,16,24,32`). Empty string if no tabs are set. + pane_tabs, + /// Bottom of scroll region in pane. + scroll_region_lower, + /// Top of scroll region in pane. + scroll_region_upper, + /// Unique session ID prefixed with `$` (e.g., `$0`, `$42`). session_id, + /// Server version (e.g., `3.5a`). + version, + /// Unique window ID prefixed with `@` (e.g., `@0`, `@42`). window_id, + /// Width of window. window_width, + /// Height of window. window_height, + /// Window layout description, ignoring zoomed window panes. Format is + /// `,` where checksum is a 4-digit hex CRC16 and layout + /// encodes pane dimensions as `WxH,X,Y[,ID]` with `{...}` for horizontal + /// splits and `[...]` for vertical splits. window_layout, + /// Pane wrap flag. + wrap_flag, /// Parse the given string value into the appropriate resulting /// type for this variable. pub fn parse(comptime self: Variable, value: []const u8) !Type(self) { return switch (self) { + .alternate_on, + .bracketed_paste, + .cursor_blinking, + .cursor_flag, + .focus_flag, + .insert_flag, + .keypad_cursor_flag, + .keypad_flag, + .mouse_all_flag, + .mouse_any_flag, + .mouse_button_flag, + .mouse_sgr_flag, + .mouse_standard_flag, + .mouse_utf8_flag, + .origin_flag, + .wrap_flag, + => std.mem.eql(u8, value, "1"), + .alternate_saved_x, + .alternate_saved_y, + .cursor_x, + .cursor_y, + .scroll_region_lower, + .scroll_region_upper, + => try std.fmt.parseInt(usize, value, 10), .session_id => if (value.len >= 2 and value[0] == '$') try std.fmt.parseInt(usize, value[1..], 10) else @@ -83,24 +206,107 @@ pub const Variable = enum { try std.fmt.parseInt(usize, value[1..], 10) else return error.FormatError, + .pane_id => if (value.len >= 2 and value[0] == '%') + try std.fmt.parseInt(usize, value[1..], 10) + else + return error.FormatError, .window_width => try std.fmt.parseInt(usize, value, 10), .window_height => try std.fmt.parseInt(usize, value, 10), - .window_layout => value, + .cursor_colour, + .cursor_shape, + .pane_tabs, + .version, + .window_layout, + => value, }; } /// The type of the parsed value for this variable type. pub fn Type(comptime self: Variable) type { return switch (self) { - .session_id => usize, - .window_id => usize, - .window_width => usize, - .window_height => usize, - .window_layout => []const u8, + .alternate_on, + .bracketed_paste, + .cursor_blinking, + .cursor_flag, + .focus_flag, + .insert_flag, + .keypad_cursor_flag, + .keypad_flag, + .mouse_all_flag, + .mouse_any_flag, + .mouse_button_flag, + .mouse_sgr_flag, + .mouse_standard_flag, + .mouse_utf8_flag, + .origin_flag, + .wrap_flag, + => bool, + .alternate_saved_x, + .alternate_saved_y, + .cursor_x, + .cursor_y, + .scroll_region_lower, + .scroll_region_upper, + .session_id, + .window_id, + .pane_id, + .window_width, + .window_height, + => usize, + .cursor_colour, + .cursor_shape, + .pane_tabs, + .version, + .window_layout, + => []const u8, }; } }; +test "parse alternate_on" { + try testing.expectEqual(true, try Variable.parse(.alternate_on, "1")); + try testing.expectEqual(false, try Variable.parse(.alternate_on, "0")); + try testing.expectEqual(false, try Variable.parse(.alternate_on, "")); + try testing.expectEqual(false, try Variable.parse(.alternate_on, "true")); + try testing.expectEqual(false, try Variable.parse(.alternate_on, "yes")); +} + +test "parse alternate_saved_x" { + try testing.expectEqual(0, try Variable.parse(.alternate_saved_x, "0")); + try testing.expectEqual(42, try Variable.parse(.alternate_saved_x, "42")); + try testing.expectError(error.InvalidCharacter, Variable.parse(.alternate_saved_x, "abc")); +} + +test "parse alternate_saved_y" { + try testing.expectEqual(0, try Variable.parse(.alternate_saved_y, "0")); + try testing.expectEqual(42, try Variable.parse(.alternate_saved_y, "42")); + try testing.expectError(error.InvalidCharacter, Variable.parse(.alternate_saved_y, "abc")); +} + +test "parse cursor_x" { + try testing.expectEqual(0, try Variable.parse(.cursor_x, "0")); + try testing.expectEqual(79, try Variable.parse(.cursor_x, "79")); + try testing.expectError(error.InvalidCharacter, Variable.parse(.cursor_x, "abc")); +} + +test "parse cursor_y" { + try testing.expectEqual(0, try Variable.parse(.cursor_y, "0")); + try testing.expectEqual(23, try Variable.parse(.cursor_y, "23")); + try testing.expectError(error.InvalidCharacter, Variable.parse(.cursor_y, "abc")); +} + +test "parse scroll_region_upper" { + try testing.expectEqual(0, try Variable.parse(.scroll_region_upper, "0")); + try testing.expectEqual(5, try Variable.parse(.scroll_region_upper, "5")); + try testing.expectError(error.InvalidCharacter, Variable.parse(.scroll_region_upper, "abc")); +} + +test "parse scroll_region_lower" { + try testing.expectEqual(0, try Variable.parse(.scroll_region_lower, "0")); + try testing.expectEqual(23, try Variable.parse(.scroll_region_lower, "23")); + try testing.expectError(error.InvalidCharacter, Variable.parse(.scroll_region_lower, "abc")); +} + test "parse session id" { try testing.expectEqual(42, try Variable.parse(.session_id, "$42")); try testing.expectEqual(0, try Variable.parse(.session_id, "$0")); @@ -146,6 +352,147 @@ test "parse window layout" { try testing.expectEqualStrings("a]b,c{d}e(f)", try Variable.parse(.window_layout, "a]b,c{d}e(f)")); } +test "parse cursor_flag" { + try testing.expectEqual(true, try Variable.parse(.cursor_flag, "1")); + try testing.expectEqual(false, try Variable.parse(.cursor_flag, "0")); + try testing.expectEqual(false, try Variable.parse(.cursor_flag, "")); + try testing.expectEqual(false, try Variable.parse(.cursor_flag, "true")); +} + +test "parse insert_flag" { + try testing.expectEqual(true, try Variable.parse(.insert_flag, "1")); + try testing.expectEqual(false, try Variable.parse(.insert_flag, "0")); + try testing.expectEqual(false, try Variable.parse(.insert_flag, "")); + try testing.expectEqual(false, try Variable.parse(.insert_flag, "true")); +} + +test "parse keypad_cursor_flag" { + try testing.expectEqual(true, try Variable.parse(.keypad_cursor_flag, "1")); + try testing.expectEqual(false, try Variable.parse(.keypad_cursor_flag, "0")); + try testing.expectEqual(false, try Variable.parse(.keypad_cursor_flag, "")); + try testing.expectEqual(false, try Variable.parse(.keypad_cursor_flag, "true")); +} + +test "parse keypad_flag" { + try testing.expectEqual(true, try Variable.parse(.keypad_flag, "1")); + try testing.expectEqual(false, try Variable.parse(.keypad_flag, "0")); + try testing.expectEqual(false, try Variable.parse(.keypad_flag, "")); + try testing.expectEqual(false, try Variable.parse(.keypad_flag, "true")); +} + +test "parse mouse_any_flag" { + try testing.expectEqual(true, try Variable.parse(.mouse_any_flag, "1")); + try testing.expectEqual(false, try Variable.parse(.mouse_any_flag, "0")); + try testing.expectEqual(false, try Variable.parse(.mouse_any_flag, "")); + try testing.expectEqual(false, try Variable.parse(.mouse_any_flag, "true")); +} + +test "parse mouse_button_flag" { + try testing.expectEqual(true, try Variable.parse(.mouse_button_flag, "1")); + try testing.expectEqual(false, try Variable.parse(.mouse_button_flag, "0")); + try testing.expectEqual(false, try Variable.parse(.mouse_button_flag, "")); + try testing.expectEqual(false, try Variable.parse(.mouse_button_flag, "true")); +} + +test "parse mouse_sgr_flag" { + try testing.expectEqual(true, try Variable.parse(.mouse_sgr_flag, "1")); + try testing.expectEqual(false, try Variable.parse(.mouse_sgr_flag, "0")); + try testing.expectEqual(false, try Variable.parse(.mouse_sgr_flag, "")); + try testing.expectEqual(false, try Variable.parse(.mouse_sgr_flag, "true")); +} + +test "parse mouse_standard_flag" { + try testing.expectEqual(true, try Variable.parse(.mouse_standard_flag, "1")); + try testing.expectEqual(false, try Variable.parse(.mouse_standard_flag, "0")); + try testing.expectEqual(false, try Variable.parse(.mouse_standard_flag, "")); + try testing.expectEqual(false, try Variable.parse(.mouse_standard_flag, "true")); +} + +test "parse mouse_utf8_flag" { + try testing.expectEqual(true, try Variable.parse(.mouse_utf8_flag, "1")); + try testing.expectEqual(false, try Variable.parse(.mouse_utf8_flag, "0")); + try testing.expectEqual(false, try Variable.parse(.mouse_utf8_flag, "")); + try testing.expectEqual(false, try Variable.parse(.mouse_utf8_flag, "true")); +} + +test "parse wrap_flag" { + try testing.expectEqual(true, try Variable.parse(.wrap_flag, "1")); + try testing.expectEqual(false, try Variable.parse(.wrap_flag, "0")); + try testing.expectEqual(false, try Variable.parse(.wrap_flag, "")); + try testing.expectEqual(false, try Variable.parse(.wrap_flag, "true")); +} + +test "parse bracketed_paste" { + try testing.expectEqual(true, try Variable.parse(.bracketed_paste, "1")); + try testing.expectEqual(false, try Variable.parse(.bracketed_paste, "0")); + try testing.expectEqual(false, try Variable.parse(.bracketed_paste, "")); + try testing.expectEqual(false, try Variable.parse(.bracketed_paste, "true")); +} + +test "parse cursor_blinking" { + try testing.expectEqual(true, try Variable.parse(.cursor_blinking, "1")); + try testing.expectEqual(false, try Variable.parse(.cursor_blinking, "0")); + try testing.expectEqual(false, try Variable.parse(.cursor_blinking, "")); + try testing.expectEqual(false, try Variable.parse(.cursor_blinking, "true")); +} + +test "parse focus_flag" { + try testing.expectEqual(true, try Variable.parse(.focus_flag, "1")); + try testing.expectEqual(false, try Variable.parse(.focus_flag, "0")); + try testing.expectEqual(false, try Variable.parse(.focus_flag, "")); + try testing.expectEqual(false, try Variable.parse(.focus_flag, "true")); +} + +test "parse mouse_all_flag" { + try testing.expectEqual(true, try Variable.parse(.mouse_all_flag, "1")); + try testing.expectEqual(false, try Variable.parse(.mouse_all_flag, "0")); + try testing.expectEqual(false, try Variable.parse(.mouse_all_flag, "")); + try testing.expectEqual(false, try Variable.parse(.mouse_all_flag, "true")); +} + +test "parse origin_flag" { + try testing.expectEqual(true, try Variable.parse(.origin_flag, "1")); + try testing.expectEqual(false, try Variable.parse(.origin_flag, "0")); + try testing.expectEqual(false, try Variable.parse(.origin_flag, "")); + try testing.expectEqual(false, try Variable.parse(.origin_flag, "true")); +} + +test "parse pane_id" { + try testing.expectEqual(42, try Variable.parse(.pane_id, "%42")); + try testing.expectEqual(0, try Variable.parse(.pane_id, "%0")); + try testing.expectError(error.FormatError, Variable.parse(.pane_id, "0")); + try testing.expectError(error.FormatError, Variable.parse(.pane_id, "@0")); + try testing.expectError(error.FormatError, Variable.parse(.pane_id, "%")); + try testing.expectError(error.FormatError, Variable.parse(.pane_id, "")); + try testing.expectError(error.InvalidCharacter, Variable.parse(.pane_id, "%abc")); +} + +test "parse cursor_colour" { + try testing.expectEqualStrings("red", try Variable.parse(.cursor_colour, "red")); + try testing.expectEqualStrings("#ff0000", try Variable.parse(.cursor_colour, "#ff0000")); + try testing.expectEqualStrings("", try Variable.parse(.cursor_colour, "")); +} + +test "parse cursor_shape" { + try testing.expectEqualStrings("block", try Variable.parse(.cursor_shape, "block")); + try testing.expectEqualStrings("underline", try Variable.parse(.cursor_shape, "underline")); + try testing.expectEqualStrings("bar", try Variable.parse(.cursor_shape, "bar")); + try testing.expectEqualStrings("", try Variable.parse(.cursor_shape, "")); +} + +test "parse pane_tabs" { + try testing.expectEqualStrings("0,8,16,24", try Variable.parse(.pane_tabs, "0,8,16,24")); + try testing.expectEqualStrings("", try Variable.parse(.pane_tabs, "")); + try testing.expectEqualStrings("0", try Variable.parse(.pane_tabs, "0")); +} + +test "parse version" { + try testing.expectEqualStrings("3.5a", try Variable.parse(.version, "3.5a")); + try testing.expectEqualStrings("3.5", try Variable.parse(.version, "3.5")); + try testing.expectEqualStrings("next-3.5", try Variable.parse(.version, "next-3.5")); + try testing.expectEqualStrings("", try Variable.parse(.version, "")); +} + test "parseFormatStruct single field" { const T = FormatStruct(&.{.session_id}); const result = try parseFormatStruct(T, "$42", ' '); @@ -203,3 +550,41 @@ test "parseFormatStruct with empty layout field" { try testing.expectEqual(1, result.session_id); try testing.expectEqualStrings("", result.window_layout); } + +fn testFormat( + comptime vars: []const Variable, + comptime delimiter: u8, + comptime expected: []const u8, +) !void { + const comptime_result = comptime comptimeFormat(vars, delimiter); + try testing.expectEqualStrings(expected, comptime_result); + + var buf: [256]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try format(&writer, vars, delimiter); + try testing.expectEqualStrings(expected, buf[0..writer.end]); +} + +test "format single variable" { + try testFormat(&.{.session_id}, ' ', "#{session_id}"); +} + +test "format multiple variables" { + try testFormat(&.{ .session_id, .window_id, .window_width, .window_height }, ' ', "#{session_id} #{window_id} #{window_width} #{window_height}"); +} + +test "format with comma delimiter" { + try testFormat(&.{ .window_id, .window_layout }, ',', "#{window_id},#{window_layout}"); +} + +test "format with tab delimiter" { + try testFormat(&.{ .window_width, .window_height }, '\t', "#{window_width}\t#{window_height}"); +} + +test "format empty variables" { + try testFormat(&.{}, ' ', ""); +} + +test "format all variables" { + try testFormat(&.{ .session_id, .window_id, .window_width, .window_height, .window_layout }, ' ', "#{session_id} #{window_id} #{window_width} #{window_height} #{window_layout}"); +} diff --git a/src/terminal/tmux/viewer.zig b/src/terminal/tmux/viewer.zig new file mode 100644 index 000000000..62a0f1d00 --- /dev/null +++ b/src/terminal/tmux/viewer.zig @@ -0,0 +1,2292 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const testing = std.testing; +const assert = @import("../../quirks.zig").inlineAssert; +const size = @import("../size.zig"); +const CircBuf = @import("../../datastruct/main.zig").CircBuf; +const CursorStyle = @import("../cursor.zig").Style; +const Screen = @import("../Screen.zig"); +const ScreenSet = @import("../ScreenSet.zig"); +const Terminal = @import("../Terminal.zig"); +const Layout = @import("layout.zig").Layout; +const control = @import("control.zig"); +const output = @import("output.zig"); + +const log = std.log.scoped(.terminal_tmux_viewer); + +// TODO: A list of TODOs as I think about them. +// - We need to make startup more robust so session and block can happen +// out of order. +// - We need to ignore `output` for panes that aren't yet initialized +// (until capture-panes are complete). +// - We should note what the active window pane is on the tmux side; +// we can use this at least for initial focus. + +// NOTE: There is some fragility here that can possibly break if tmux +// changes their implementation. In particular, the order of notifications +// and assurances about what is sent when are based on reading the tmux +// source code as of Dec, 2025. These aren't documented as fixed. +// +// I've tried not to depend on anything that seems like it'd change +// in the future. For example, it seems reasonable that command output +// always comes before session attachment. But, I am noting this here +// in case something breaks in the future we can consider it. We should +// be able to easily unit test all variations seen in the real world. + +/// The initial capacity of the command queue. We dynamically resize +/// as necessary so the initial value isn't that important, but if we +/// want to feel good about it we should make it large enough to support +/// our most realistic use cases without resizing. +const COMMAND_QUEUE_INITIAL = 8; + +/// A viewer is a tmux control mode client that attempts to create +/// a remote view of a tmux session, including providing the ability to send +/// new input to the session. +/// +/// This is the primary use case for tmux control mode, but technically +/// tmux control mode clients can do anything a normal tmux client can do, +/// so the `control.zig` and other files in this folder are more general +/// purpose. +/// +/// This struct helps move through a state machine of connecting to a tmux +/// session, negotiating capabilities, listing window state, etc. +/// +/// ## Viewer Lifecycle +/// +/// The viewer progresses through several states from initial connection +/// to steady-state operation. Here is the full flow: +/// +/// ``` +/// ┌─────────────────────────────────────────────┐ +/// │ TMUX CONTROL MODE START │ +/// │ (DCS 1000p received by host) │ +/// └─────────────────┬───────────────────────────┘ +/// │ +/// ▼ +/// ┌─────────────────────────────────────────────┐ +/// │ startup_block │ +/// │ │ +/// │ Wait for initial %begin/%end block from │ +/// │ tmux. This is the response to the initial │ +/// │ command (e.g., "attach -t 0"). │ +/// └─────────────────┬───────────────────────────┘ +/// │ %end / %error +/// ▼ +/// ┌─────────────────────────────────────────────┐ +/// │ startup_session │ +/// │ │ +/// │ Wait for %session-changed notification │ +/// │ to get the initial session ID. │ +/// └─────────────────┬───────────────────────────┘ +/// │ %session-changed +/// ▼ +/// ┌─────────────────────────────────────────────┐ +/// │ command_queue │ +/// │ │ +/// │ Main operating state. Process commands │ +/// │ sequentially and handle notifications. │ +/// └─────────────────────────────────────────────┘ +/// │ +/// ┌───────────────────────────┼───────────────────────────┐ +/// │ │ │ +/// ▼ ▼ ▼ +/// ┌──────────────────────────┐ ┌──────────────────────────┐ ┌────────────────────────┐ +/// │ tmux_version │ │ list_windows │ │ %output / %layout- │ +/// │ │ │ │ │ change / etc. │ +/// │ Query tmux version for │ │ Get all windows in the │ │ │ +/// │ compatibility checks. │ │ current session. │ │ Handle live updates │ +/// └──────────────────────────┘ └────────────┬─────────────┘ │ from tmux server. │ +/// │ └────────────────────────┘ +/// ▼ +/// ┌─────────────────────────────────────────────┐ +/// │ syncLayouts │ +/// │ │ +/// │ For each window, parse layout and sync │ +/// │ panes. New panes trigger capture commands. │ +/// └─────────────────┬───────────────────────────┘ +/// │ +/// ┌───────────────────────────┴───────────────────────────┐ +/// │ For each new pane: │ +/// ▼ ▼ +/// ┌──────────────────────────┐ ┌──────────────────────────┐ +/// │ pane_history │ │ pane_visible │ +/// │ (primary screen) │ │ (primary screen) │ +/// │ │ │ │ +/// │ Capture scrollback │ │ Capture visible area │ +/// │ history into terminal. │ │ into terminal. │ +/// └──────────────────────────┘ └──────────────────────────┘ +/// │ │ +/// ▼ ▼ +/// ┌──────────────────────────┐ ┌──────────────────────────┐ +/// │ pane_history │ │ pane_visible │ +/// │ (alternate screen) │ │ (alternate screen) │ +/// └──────────────────────────┘ └──────────────────────────┘ +/// │ │ +/// └───────────────────────────┬───────────────────────────┘ +/// ▼ +/// ┌─────────────────────────────────────────────┐ +/// │ pane_state │ +/// │ │ +/// │ Query cursor position, cursor style, │ +/// │ and alternate screen mode for all panes. │ +/// └─────────────────────────────────────────────┘ +/// │ +/// ▼ +/// ┌─────────────────────────────────────────────┐ +/// │ READY FOR OPERATION │ +/// │ │ +/// │ Panes are populated with content. The │ +/// │ viewer handles %output for live updates, │ +/// │ %layout-change for pane changes, and │ +/// │ %session-changed for session switches. │ +/// └─────────────────────────────────────────────┘ +/// ``` +/// +/// ## Error Handling +/// +/// At any point, if an unrecoverable error occurs or tmux sends `%exit`, +/// the viewer transitions to the `defunct` state and emits an `.exit` action. +/// +/// ## Session Changes +/// +/// When `%session-changed` is received during `command_queue` state, the +/// viewer resets itself completely: clears all windows/panes, emits an +/// empty windows action, and restarts the `list_windows` flow for the new +/// session. +/// +pub const Viewer = struct { + /// Allocator used for all internal state. + alloc: Allocator, + + /// Current state of the state machine. + state: State, + + /// The current session ID we're attached to. + session_id: usize, + + /// The tmux server version string (e.g., "3.5a"). We capture this + /// on startup because it will allow us to change behavior between + /// versions as necessary. + tmux_version: []const u8, + + /// The list of commands we've sent that we want to send and wait + /// for a response for. We only send one command at a time just + /// to avoid any possible confusion around ordering. + command_queue: CommandQueue, + + /// The windows in the current session. + windows: std.ArrayList(Window), + + /// The panes in the current session, mapped by pane ID. + panes: PanesMap, + + /// The arena used for the prior action allocated state. This contains + /// the contents for the actions as well as the actions slice itself. + action_arena: ArenaAllocator.State, + + /// A single action pre-allocated that we use for single-action + /// returns (common). This ensures that we can never get allocation + /// errors on single-action returns, especially those such as `.exit`. + action_single: [1]Action, + + pub const CommandQueue = CircBuf(Command, undefined); + pub const PanesMap = std.AutoArrayHashMapUnmanaged(usize, Pane); + + pub const Action = union(enum) { + /// Tmux has closed the control mode connection, we should end + /// our viewer session in some way. + exit, + + /// Send a command to tmux, e.g. `list-windows`. The caller + /// should not worry about parsing this or reading what command + /// it is; just send it to tmux as-is. This will include the + /// trailing newline so you can send it directly. + command: []const u8, + + /// Windows changed. This may add, remove or change windows. The + /// caller is responsible for diffing the new window list against + /// the prior one. Remember that for a given Viewer, window IDs + /// are guaranteed to be stable. Additionally, tmux (as of Dec 2025) + /// never reuses window IDs within a server process lifetime. + windows: []const Window, + + pub fn format(self: Action, writer: *std.Io.Writer) !void { + const T = Action; + const info = @typeInfo(T).@"union"; + + try writer.writeAll(@typeName(T)); + if (info.tag_type) |TagType| { + try writer.writeAll("{ ."); + try writer.writeAll(@tagName(@as(TagType, self))); + try writer.writeAll(" = "); + + inline for (info.fields) |u_field| { + if (self == @field(TagType, u_field.name)) { + const value = @field(self, u_field.name); + switch (u_field.type) { + []const u8 => try writer.print("\"{s}\"", .{std.mem.trim(u8, value, " \t\r\n")}), + else => try writer.print("{any}", .{value}), + } + } + } + + try writer.writeAll(" }"); + } + } + }; + + pub const Input = union(enum) { + /// Data from tmux was received that needs to be processed. + tmux: control.Notification, + }; + + pub const Window = struct { + id: usize, + width: usize, + height: usize, + layout_arena: ArenaAllocator.State, + layout: Layout, + + pub fn deinit(self: *Window, alloc: Allocator) void { + self.layout_arena.promote(alloc).deinit(); + } + }; + + pub const Pane = struct { + terminal: Terminal, + + pub fn deinit(self: *Pane, alloc: Allocator) void { + self.terminal.deinit(alloc); + } + }; + + /// Initialize a new viewer. + /// + /// The given allocator is used for all internal state. You must + /// call deinit when you're done with the viewer to free it. + pub fn init(alloc: Allocator) Allocator.Error!Viewer { + // Create our initial command queue + var command_queue: CommandQueue = try .init(alloc, COMMAND_QUEUE_INITIAL); + errdefer command_queue.deinit(alloc); + + return .{ + .alloc = alloc, + .state = .startup_block, + // The default value here is meaningless. We don't get started + // until we receive a session-changed notification which will + // set this to a real value. + .session_id = 0, + .tmux_version = "", + .command_queue = command_queue, + .windows = .empty, + .panes = .empty, + .action_arena = .{}, + .action_single = undefined, + }; + } + + pub fn deinit(self: *Viewer) void { + { + for (self.windows.items) |*window| window.deinit(self.alloc); + self.windows.deinit(self.alloc); + } + { + var it = self.command_queue.iterator(.forward); + while (it.next()) |command| command.deinit(self.alloc); + self.command_queue.deinit(self.alloc); + } + { + var it = self.panes.iterator(); + while (it.next()) |kv| kv.value_ptr.deinit(self.alloc); + self.panes.deinit(self.alloc); + } + if (self.tmux_version.len > 0) { + self.alloc.free(self.tmux_version); + } + self.action_arena.promote(self.alloc).deinit(); + } + + /// Send in an input event (such as a tmux protocol notification, + /// keyboard input for a pane, etc.) and process it. The returned + /// list is a set of actions to take as a result of the input prior + /// to the next input. This list may be empty. + pub fn next(self: *Viewer, input: Input) []const Action { + // Developer note: this function must never return an error. If + // an error occurs we must go into a defunct state or some other + // state to gracefully handle it. + return switch (input) { + .tmux => self.nextTmux(input.tmux), + }; + } + + fn nextTmux( + self: *Viewer, + n: control.Notification, + ) []const Action { + return switch (self.state) { + .defunct => defunct: { + log.info("received notification in defunct state, ignoring", .{}); + break :defunct &.{}; + }, + + .startup_block => self.nextStartupBlock(n), + .startup_session => self.nextStartupSession(n), + .command_queue => self.nextCommand(n), + }; + } + + fn nextStartupBlock( + self: *Viewer, + n: control.Notification, + ) []const Action { + assert(self.state == .startup_block); + + switch (n) { + // This is only sent by the DCS parser when we first get + // DCS 1000p, it should never reach us here. + .enter => unreachable, + + // I don't think this is technically possible (reading the + // tmux source code), but if we see an exit we can semantically + // handle this without issue. + .exit => return self.defunct(), + + // Any begin and end (even error) is fine! Now we wait for + // session-changed to get the initial session ID. session-changed + // is guaranteed to come after the initial command output + // since if the initial command is `attach` tmux will run that, + // queue the notification, then do notificatins. + .block_end, .block_err => { + self.state = .startup_session; + return &.{}; + }, + + // I don't like catch-all else branches but startup is such + // a special case of looking for very specific things that + // are unlikely to expand. + else => return &.{}, + } + } + + fn nextStartupSession( + self: *Viewer, + n: control.Notification, + ) []const Action { + assert(self.state == .startup_session); + + switch (n) { + .enter => unreachable, + + .exit => return self.defunct(), + + .session_changed => |info| { + self.session_id = info.id; + + var arena = self.action_arena.promote(self.alloc); + defer self.action_arena = arena.state; + _ = arena.reset(.free_all); + + return self.enterCommandQueue( + arena.allocator(), + &.{ .tmux_version, .list_windows }, + ) catch { + log.warn("failed to queue command, becoming defunct", .{}); + return self.defunct(); + }; + }, + + else => return &.{}, + } + } + + fn nextIdle( + self: *Viewer, + n: control.Notification, + ) []const Action { + assert(self.state == .idle); + + switch (n) { + .enter => unreachable, + .exit => return self.defunct(), + else => return &.{}, + } + } + + fn nextCommand( + self: *Viewer, + n: control.Notification, + ) []const Action { + // We have to be in a command queue, but the command queue MAY + // be empty. If it is empty, then receivedCommandOutput will + // handle it by ignoring any command output. That's okay! + assert(self.state == .command_queue); + + // Clear our prior arena so it is ready to be used for any + // actions immediately. + { + var arena = self.action_arena.promote(self.alloc); + _ = arena.reset(.free_all); + self.action_arena = arena.state; + } + + // Setup our empty actions list that commands can populate. + var actions: std.ArrayList(Action) = .empty; + + // Track whether the in-flight command slot is available. Starts true + // if queue is empty (no command in flight). Set to true when a command + // completes (block_end/block_err) or the queue is reset (session_changed). + var command_consumed = self.command_queue.empty(); + + switch (n) { + .enter => unreachable, + .exit => return self.defunct(), + + inline .block_end, + .block_err, + => |content, tag| { + self.receivedCommandOutput( + &actions, + content, + tag == .block_err, + ) catch { + log.warn("failed to process command output, becoming defunct", .{}); + return self.defunct(); + }; + + // Command is consumed since a block end/err is the output + // from a command. + command_consumed = true; + }, + + .output => |out| self.receivedOutput( + out.pane_id, + out.data, + ) catch |err| { + log.warn( + "failed to process output for pane id={}: {}", + .{ out.pane_id, err }, + ); + }, + + // Session changed means we switched to a different tmux session. + // We need to reset our state and start fresh with list-windows. + // This completely replaces the viewer, so treat it like a fresh start. + .session_changed => |info| { + self.sessionChanged( + &actions, + info.id, + ) catch { + log.warn("failed to handle session change, becoming defunct", .{}); + return self.defunct(); + }; + + // Command is consumed because sessionChanged resets + // our entire viewer. + command_consumed = true; + }, + + // Layout changed of a single window. + .layout_change => |info| self.layoutChanged( + &actions, + info.window_id, + info.layout, + ) catch { + // Note: in the future, we can probably handle a failure + // here with a fallback to remove this one window, list + // windows again, and try again. + log.warn("failed to handle layout change, becoming defunct", .{}); + return self.defunct(); + }, + + // A window was added to this session. + .window_add => |info| self.windowAdd(info.id) catch { + log.warn("failed to handle window add, becoming defunct", .{}); + return self.defunct(); + }, + + // The active pane changed. We don't care about this because + // we handle our own focus. + .window_pane_changed => {}, + + // We ignore this one. It means a session was created or + // destroyed. If it was our own session we will get an exit + // notification very soon. If it is another session we don't + // care. + .sessions_changed => {}, + + // We don't use window names for anything, currently. + .window_renamed => {}, + + // This is for other clients, which we don't do anything about. + // For us, we'll get `exit` or `session_changed`, respectively. + .client_detached, + .client_session_changed, + => {}, + } + + // After processing commands, we add our next command to + // execute if we have one. We do this last because command + // processing may itself queue more commands. We only emit a + // command if a prior command was consumed (or never existed). + if (self.state == .command_queue and command_consumed) { + if (self.command_queue.first()) |next_command| { + // We should not have any commands, because our nextCommand + // always queues them. + if (comptime std.debug.runtime_safety) { + for (actions.items) |action| { + if (action == .command) assert(false); + } + } + + var arena = self.action_arena.promote(self.alloc); + defer self.action_arena = arena.state; + const arena_alloc = arena.allocator(); + + var builder: std.Io.Writer.Allocating = .init(arena_alloc); + next_command.formatCommand(&builder.writer) catch + return self.defunct(); + actions.append( + arena_alloc, + .{ .command = builder.writer.buffered() }, + ) catch return self.defunct(); + } + } + + return actions.items; + } + + /// When the layout changes for a single window, a pane may be added + /// or removed that we've never seen, in addition to the layout itself + /// physically changing. + /// + /// To handle this, its similar to list-windows except we expect the + /// window to already exist. We update the layout, do the initLayout + /// call for any diffs, setup commands to capture any new panes, + /// prune any removed panes. + fn layoutChanged( + self: *Viewer, + actions: *std.ArrayList(Action), + window_id: usize, + layout_str: []const u8, + ) !void { + // Find the window this layout change is for. + const window: *Window = window: for (self.windows.items) |*w| { + if (w.id == window_id) break :window w; + } else { + log.info("layout change for unknown window id={}", .{window_id}); + return; + }; + + // Clear our prior window arena and setup our layout + window.layout = layout: { + var arena = window.layout_arena.promote(self.alloc); + defer window.layout_arena = arena.state; + _ = arena.reset(.retain_capacity); + break :layout Layout.parseWithChecksum( + arena.allocator(), + layout_str, + ) catch |err| { + log.info( + "failed to parse window layout id={} layout={s}", + .{ window_id, layout_str }, + ); + return err; + }; + }; + + // Reset our arena so we can build up actions. + var arena = self.action_arena.promote(self.alloc); + defer self.action_arena = arena.state; + const arena_alloc = arena.allocator(); + + // Our initial action is to definitely let the caller know that + // some windows changed. + try actions.append(arena_alloc, .{ .windows = self.windows.items }); + + // Sync up our panes + try self.syncLayouts(self.windows.items); + } + + /// When a window is added to the session, we need to refresh our window + /// list to get the new window's information. + fn windowAdd( + self: *Viewer, + window_id: usize, + ) !void { + _ = window_id; // We refresh all windows via list-windows + + // Queue list-windows to get the updated window list + try self.queueCommands(&.{.list_windows}); + } + + fn syncLayouts( + self: *Viewer, + windows: []const Window, + ) !void { + // Go through the window layout and setup all our panes. We move + // this into a new panes map so that we can easily prune our old + // list. + var panes: PanesMap = .empty; + errdefer { + // Clear out all the new panes. + var panes_it = panes.iterator(); + while (panes_it.next()) |kv| { + if (!self.panes.contains(kv.key_ptr.*)) { + kv.value_ptr.deinit(self.alloc); + } + } + panes.deinit(self.alloc); + } + for (windows) |window| try initLayout( + self.alloc, + &self.panes, + &panes, + window.layout, + ); + + // Build up the list of removed panes. + var removed: std.ArrayList(usize) = removed: { + var removed: std.ArrayList(usize) = .empty; + errdefer removed.deinit(self.alloc); + var panes_it = self.panes.iterator(); + while (panes_it.next()) |kv| { + if (panes.contains(kv.key_ptr.*)) continue; + try removed.append(self.alloc, kv.key_ptr.*); + } + + break :removed removed; + }; + defer removed.deinit(self.alloc); + + // Ensure we can add the windows + try self.windows.ensureTotalCapacity(self.alloc, windows.len); + + // Get our list of added panes and setup our command queue + // to populate them. + // TODO: errdefer cleanup + { + var panes_it = panes.iterator(); + var added: bool = false; + while (panes_it.next()) |kv| { + const pane_id: usize = kv.key_ptr.*; + if (self.panes.contains(pane_id)) continue; + added = true; + try self.queueCommands(&.{ + .{ .pane_history = .{ .id = pane_id, .screen_key = .primary } }, + .{ .pane_visible = .{ .id = pane_id, .screen_key = .primary } }, + .{ .pane_history = .{ .id = pane_id, .screen_key = .alternate } }, + .{ .pane_visible = .{ .id = pane_id, .screen_key = .alternate } }, + }); + } + + // If we added any panes, then we also want to resync the pane + // state (terminal modes and cursor positions and so on). + if (added) try self.queueCommands(&.{.pane_state}); + } + + // No more errors after this point. We're about to replace all + // our owned state with our temporary state, and our errdefers + // above will double-free if there is an error. + errdefer comptime unreachable; + + // Replace our window list if it changed. We assume it didn't + // change if our pointer is pointing to the same data. + if (windows.ptr != self.windows.items.ptr) { + for (self.windows.items) |*window| window.deinit(self.alloc); + self.windows.clearRetainingCapacity(); + self.windows.appendSliceAssumeCapacity(windows); + } + + // Replace our panes + { + // First remove our old panes + for (removed.items) |id| if (self.panes.fetchSwapRemove( + id, + )) |entry_const| { + var entry = entry_const; + entry.value.deinit(self.alloc); + }; + // We can now deinit self.panes because the existing + // entries are preserved. + self.panes.deinit(self.alloc); + self.panes = panes; + } + } + + /// When a session changes, we have to basically reset our whole state. + /// To do this, we emit an empty windows event (so callers can clear all + /// windows), reset ourself, and start all over. + fn sessionChanged( + self: *Viewer, + actions: *std.ArrayList(Action), + session_id: usize, + ) (Allocator.Error || std.Io.Writer.Error)!void { + // Build up a new viewer. Its the easiest way to reset ourselves. + var replacement: Viewer = try .init(self.alloc); + errdefer replacement.deinit(); + + // Our actions must start out empty so we don't mix arenas + assert(actions.items.len == 0); + errdefer actions.* = .empty; + + // Build actions: empty windows notification + list-windows command + var arena = replacement.action_arena.promote(replacement.alloc); + const arena_alloc = arena.allocator(); + try actions.append(arena_alloc, .{ .windows = &.{} }); + + // Setup our command queue and put ourselves in the command queue + // state. + try replacement.queueCommands(&.{.list_windows}); + replacement.state = .command_queue; + + // Transfer preserved version to replacement + replacement.tmux_version = try replacement.alloc.dupe(u8, self.tmux_version); + + // Save arena state back before swap + replacement.action_arena = arena.state; + + // Swap our self, no more error handling after this. + errdefer comptime unreachable; + self.deinit(); + self.* = replacement; + + // Set our session ID and jump directly to the list + self.session_id = session_id; + + assert(self.state == .command_queue); + } + + fn receivedCommandOutput( + self: *Viewer, + actions: *std.ArrayList(Action), + content: []const u8, + is_err: bool, + ) !void { + // Get the command we're expecting output for. We need to get the + // non-pointer value because we are deleting it from the circular + // buffer immediately. This shallow copy is all we need since + // all the memory in Command is owned by GPA. + const command: Command = if (self.command_queue.first()) |ptr| switch (ptr.*) { + // I truly can't explain this. A simple `ptr.*` copy will cause + // our memory to become undefined when deleteOldest is called + // below. I logged all the pointers and they don't match so I + // don't know how its being set to undefined. But a copy like + // this does work. + inline else => |v, tag| @unionInit( + Command, + @tagName(tag), + v, + ), + } else { + // If we have no pending commands, this is unexpected. + log.info("unexpected block output err={}", .{is_err}); + return; + }; + self.command_queue.deleteOldest(1); + defer command.deinit(self.alloc); + + // We'll use our arena for the return value here so we can + // easily accumulate actions. + var arena = self.action_arena.promote(self.alloc); + defer self.action_arena = arena.state; + const arena_alloc = arena.allocator(); + + // Process our command + switch (command) { + .user => {}, + + .pane_state => try self.receivedPaneState(content), + + .list_windows => try self.receivedListWindows( + arena_alloc, + actions, + content, + ), + + .pane_history => |cap| try self.receivedPaneHistory( + cap.screen_key, + cap.id, + content, + ), + + .pane_visible => |cap| try self.receivedPaneVisible( + cap.screen_key, + cap.id, + content, + ), + + .tmux_version => try self.receivedTmuxVersion(content), + } + } + + fn receivedTmuxVersion( + self: *Viewer, + content: []const u8, + ) !void { + const line = std.mem.trim(u8, content, " \t\r\n"); + if (line.len == 0) return; + + const data = output.parseFormatStruct( + Format.tmux_version.Struct(), + line, + Format.tmux_version.delim, + ) catch |err| { + log.info("failed to parse tmux version: {s}", .{line}); + return err; + }; + + if (self.tmux_version.len > 0) { + self.alloc.free(self.tmux_version); + } + self.tmux_version = try self.alloc.dupe(u8, data.version); + } + + fn receivedListWindows( + self: *Viewer, + arena_alloc: Allocator, + actions: *std.ArrayList(Action), + content: []const u8, + ) !void { + // If there is an error, reset our actions to what it was before. + errdefer actions.shrinkRetainingCapacity(actions.items.len); + + // This stores our new window state from this list-windows output. + var windows: std.ArrayList(Window) = .empty; + defer windows.deinit(self.alloc); + + // Parse all our windows + var it = std.mem.splitScalar(u8, content, '\n'); + while (it.next()) |line_raw| { + const line = std.mem.trim(u8, line_raw, " \t\r"); + if (line.len == 0) continue; + const data = output.parseFormatStruct( + Format.list_windows.Struct(), + line, + Format.list_windows.delim, + ) catch |err| { + log.info("failed to parse list-windows line: {s}", .{line}); + return err; + }; + + // Parse the layout + var arena: ArenaAllocator = .init(self.alloc); + errdefer arena.deinit(); + const window_alloc = arena.allocator(); + const layout: Layout = Layout.parseWithChecksum( + window_alloc, + data.window_layout, + ) catch |err| { + log.info( + "failed to parse window layout id={} layout={s}", + .{ data.window_id, data.window_layout }, + ); + return err; + }; + + try windows.append(self.alloc, .{ + .id = data.window_id, + .width = data.window_width, + .height = data.window_height, + .layout_arena = arena.state, + .layout = layout, + }); + } + + // Setup our windows action so the caller can process GUI + // window changes. + try actions.append(arena_alloc, .{ .windows = windows.items }); + + // Sync up our layouts. This will populate unknown panes, prune, etc. + try self.syncLayouts(windows.items); + } + + fn receivedPaneState( + self: *Viewer, + content: []const u8, + ) !void { + var it = std.mem.splitScalar(u8, content, '\n'); + while (it.next()) |line_raw| { + const line = std.mem.trim(u8, line_raw, " \t\r"); + if (line.len == 0) continue; + + const data = output.parseFormatStruct( + Format.list_panes.Struct(), + line, + Format.list_panes.delim, + ) catch |err| { + log.info("failed to parse list-panes line: {s}", .{line}); + return err; + }; + + // Get the pane for this ID + const entry = self.panes.getEntry(data.pane_id) orelse { + log.info("received pane state for untracked pane id={}", .{data.pane_id}); + continue; + }; + const pane: *Pane = entry.value_ptr; + const t: *Terminal = &pane.terminal; + + // Determine which screen to use based on alternate_on + const screen_key: ScreenSet.Key = if (data.alternate_on) .alternate else .primary; + + // Set cursor position on the appropriate screen (tmux uses 0-based) + if (t.screens.get(screen_key)) |screen| { + cursor: { + const cursor_x = std.math.cast( + size.CellCountInt, + data.cursor_x, + ) orelse break :cursor; + const cursor_y = std.math.cast( + size.CellCountInt, + data.cursor_y, + ) orelse break :cursor; + if (cursor_x >= screen.pages.cols or + cursor_y >= screen.pages.rows) break :cursor; + screen.cursorAbsolute(cursor_x, cursor_y); + } + + // Set cursor shape on this screen + if (data.cursor_shape.len > 0) { + if (std.mem.eql(u8, data.cursor_shape, "block")) { + screen.cursor.cursor_style = .block; + } else if (std.mem.eql(u8, data.cursor_shape, "underline")) { + screen.cursor.cursor_style = .underline; + } else if (std.mem.eql(u8, data.cursor_shape, "bar")) { + screen.cursor.cursor_style = .bar; + } + } + // "default" or unknown: leave as-is + } + + // Set alternate screen saved cursor position + if (t.screens.get(.alternate)) |alt_screen| cursor: { + const alt_x = std.math.cast( + size.CellCountInt, + data.alternate_saved_x, + ) orelse break :cursor; + const alt_y = std.math.cast( + size.CellCountInt, + data.alternate_saved_y, + ) orelse break :cursor; + + // If our coordinates are outside our screen we ignore it. + // tmux actually sends MAX_INT for when there isn't a set + // cursor position, so this isn't theoretical. + if (alt_x >= alt_screen.pages.cols or + alt_y >= alt_screen.pages.rows) break :cursor; + + alt_screen.cursorAbsolute(alt_x, alt_y); + } + + // Set cursor visibility + t.modes.set(.cursor_visible, data.cursor_flag); + + // Set cursor blinking + t.modes.set(.cursor_blinking, data.cursor_blinking); + + // Terminal modes + t.modes.set(.insert, data.insert_flag); + t.modes.set(.wraparound, data.wrap_flag); + t.modes.set(.keypad_keys, data.keypad_flag); + t.modes.set(.cursor_keys, data.keypad_cursor_flag); + t.modes.set(.origin, data.origin_flag); + + // Mouse modes + t.modes.set(.mouse_event_any, data.mouse_all_flag); + t.modes.set(.mouse_event_button, data.mouse_any_flag); + t.modes.set(.mouse_event_normal, data.mouse_button_flag); + t.modes.set(.mouse_event_x10, data.mouse_standard_flag); + t.modes.set(.mouse_format_utf8, data.mouse_utf8_flag); + t.modes.set(.mouse_format_sgr, data.mouse_sgr_flag); + + // Focus and bracketed paste + t.modes.set(.focus_event, data.focus_flag); + t.modes.set(.bracketed_paste, data.bracketed_paste); + + // Scroll region (tmux uses 0-based values) + scroll: { + const scroll_top = std.math.cast( + size.CellCountInt, + data.scroll_region_upper, + ) orelse break :scroll; + const scroll_bottom = std.math.cast( + size.CellCountInt, + data.scroll_region_lower, + ) orelse break :scroll; + t.scrolling_region.top = scroll_top; + t.scrolling_region.bottom = scroll_bottom; + } + + // Tab stops - parse comma-separated list and set + t.tabstops.reset(0); // Clear all tabstops first + if (data.pane_tabs.len > 0) { + var tabs_it = std.mem.splitScalar(u8, data.pane_tabs, ','); + while (tabs_it.next()) |tab_str| { + const col = std.fmt.parseInt(usize, tab_str, 10) catch continue; + const col_cell = std.math.cast(size.CellCountInt, col) orelse continue; + if (col_cell >= t.cols) continue; + t.tabstops.set(col_cell); + } + } + } + } + + fn receivedPaneHistory( + self: *Viewer, + screen_key: ScreenSet.Key, + id: usize, + content: []const u8, + ) !void { + // Get our pane + const entry = self.panes.getEntry(id) orelse { + log.info("received pane history for untracked pane id={}", .{id}); + return; + }; + const pane: *Pane = entry.value_ptr; + const t: *Terminal = &pane.terminal; + _ = try t.switchScreen(screen_key); + const screen: *Screen = t.screens.active; + + // Get a VT stream from the terminal so we can send data as-is into + // it. This will populate the active area too so it won't be exactly + // correct but we'll get the active contents soon. + var stream = t.vtStream(); + defer stream.deinit(); + stream.nextSlice(content) catch |err| { + log.info("failed to process pane history for pane id={}: {}", .{ id, err }); + return err; + }; + + // Populate the active area to be empty since this is only history. + // We'll fill it with blanks and move the cursor to the top-left. + t.carriageReturn(); + for (0..t.rows) |_| try t.index(); + t.setCursorPos(1, 1); + + // Our active area should be empty + if (comptime std.debug.runtime_safety) { + var discarding: std.Io.Writer.Discarding = .init(&.{}); + screen.dumpString(&discarding.writer, .{ + .tl = screen.pages.getTopLeft(.active), + .unwrap = false, + }) catch unreachable; + assert(discarding.count == 0); + } + } + + fn receivedPaneVisible( + self: *Viewer, + screen_key: ScreenSet.Key, + id: usize, + content: []const u8, + ) !void { + // Get our pane + const entry = self.panes.getEntry(id) orelse { + log.info("received pane visible for untracked pane id={}", .{id}); + return; + }; + const pane: *Pane = entry.value_ptr; + const t: *Terminal = &pane.terminal; + _ = try t.switchScreen(screen_key); + + // Erase the active area and reset the cursor to the top-left + // before writing the visible content. + t.eraseDisplay(.complete, false); + t.setCursorPos(1, 1); + + var stream = t.vtStream(); + defer stream.deinit(); + stream.nextSlice(content) catch |err| { + log.info("failed to process pane visible for pane id={}: {}", .{ id, err }); + return err; + }; + } + + fn receivedOutput( + self: *Viewer, + id: usize, + data: []const u8, + ) !void { + const entry = self.panes.getEntry(id) orelse { + log.info("received output for untracked pane id={}", .{id}); + return; + }; + const pane: *Pane = entry.value_ptr; + const t: *Terminal = &pane.terminal; + + var stream = t.vtStream(); + defer stream.deinit(); + stream.nextSlice(data) catch |err| { + log.info("failed to process output for pane id={}: {}", .{ id, err }); + return err; + }; + } + + fn initLayout( + gpa_alloc: Allocator, + panes_old: *const PanesMap, + panes_new: *PanesMap, + layout: Layout, + ) !void { + switch (layout.content) { + // Nested layouts, continue going. + .horizontal, .vertical => |layouts| { + for (layouts) |l| { + try initLayout( + gpa_alloc, + panes_old, + panes_new, + l, + ); + } + }, + + // A leaf! Initialize. + .pane => |id| pane: { + const gop = try panes_new.getOrPut(gpa_alloc, id); + if (gop.found_existing) break :pane; + errdefer _ = panes_new.swapRemove(gop.key_ptr.*); + + // If we already have this pane, it is already initialized + // so just copy it over. + if (panes_old.getEntry(id)) |entry| { + gop.value_ptr.* = entry.value_ptr.*; + break :pane; + } + + // TODO: We need to gracefully handle overflow of our + // max cols/width here. In practice we shouldn't hit this + // so we cast but its not safe. + var t: Terminal = try .init(gpa_alloc, .{ + .cols = @intCast(layout.width), + .rows = @intCast(layout.height), + }); + errdefer t.deinit(gpa_alloc); + + gop.value_ptr.* = .{ + .terminal = t, + }; + }, + } + } + + /// Enters the command queue state from any other state, queueing + /// the commands and returning an action to execute the first command. + fn enterCommandQueue( + self: *Viewer, + arena_alloc: Allocator, + commands: []const Command, + ) Allocator.Error![]const Action { + assert(self.state != .command_queue); + assert(commands.len > 0); + + // Build our command string to send for the action. + var builder: std.Io.Writer.Allocating = .init(arena_alloc); + commands[0].formatCommand(&builder.writer) catch return error.OutOfMemory; + const action: Action = .{ .command = builder.writer.buffered() }; + + // Add our commands + try self.command_queue.ensureUnusedCapacity(self.alloc, commands.len); + for (commands) |cmd| self.command_queue.appendAssumeCapacity(cmd); + + // Move into the command queue state + self.state = .command_queue; + + return self.singleAction(action); + } + + /// Queue multiple commands to execute. This doesn't add anything + /// to the actions queue or return actions or anything because the + /// command_queue state will automatically send the next command when + /// it receives output. + fn queueCommands( + self: *Viewer, + commands: []const Command, + ) Allocator.Error!void { + try self.command_queue.ensureUnusedCapacity( + self.alloc, + commands.len, + ); + for (commands) |command| { + self.command_queue.appendAssumeCapacity(command); + } + } + + /// Helper to return a single action. The input action may use the arena + /// for allocated memory; this will not touch the arena. + fn singleAction(self: *Viewer, action: Action) []const Action { + // Make our single action slice. + self.action_single[0] = action; + return &self.action_single; + } + + fn defunct(self: *Viewer) []const Action { + self.state = .defunct; + return self.singleAction(.exit); + } +}; + +const State = enum { + /// We start in this state just after receiving the initial + /// DCS 1000p opening sequence. We wait for an initial + /// begin/end block that is guaranteed to be sent by tmux for + /// the initial control mode command. (See tmux server-client.c + /// where control mode starts). + startup_block, + + /// After receiving the initial block, we wait for a session-changed + /// notification to record the initial session ID. + startup_session, + + /// Tmux has closed the control mode connection + defunct, + + /// We're sitting on the command queue waiting for command output + /// in the order provided in the `command_queue` field. This field + /// isn't part of the state because it can be queued at any state. + /// + /// Precondition: if self.command_queue.len > 0, then the first + /// command in the queue has already been sent to tmux (via a + /// `command` Action). The next output is assumed to be the result + /// of this command. + /// + /// To satisfy the above, any transitions INTO this state should + /// send a command Action for the first command in the queue. + command_queue, +}; + +const Command = union(enum) { + /// List all windows so we can sync our window state. + list_windows, + + /// Capture history for the given pane ID. + pane_history: CapturePane, + + /// Capture visible area for the given pane ID. + pane_visible: CapturePane, + + /// Capture the pane terminal state as best we can. The pane ID(s) + /// are part of the output so we can map it back to our panes. + pane_state, + + /// Get the tmux server version. + tmux_version, + + /// User command. This is a command provided by the user. Since + /// this is user provided, we can't be sure what it is. + user: []const u8, + + const CapturePane = struct { + id: usize, + screen_key: ScreenSet.Key, + }; + + pub fn deinit(self: Command, alloc: Allocator) void { + return switch (self) { + .list_windows, + .pane_history, + .pane_visible, + .pane_state, + .tmux_version, + => {}, + .user => |v| alloc.free(v), + }; + } + + /// Format the command into the command that should be executed + /// by tmux. Trailing newlines are appended so this can be sent as-is + /// to tmux. + pub fn formatCommand( + self: Command, + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { + switch (self) { + .list_windows => try writer.writeAll(std.fmt.comptimePrint( + "list-windows -F '{s}'\n", + .{comptime Format.list_windows.comptimeFormat()}, + )), + + .pane_history => |cap| try writer.print( + // -p = output to stdout instead of buffer + // -e = output escape sequences for SGR + // -a = capture alternate screen (only valid for alternate) + // -q = quiet, don't error if alternate screen doesn't exist + // -S - = start at the top of history ("-") + // -E -1 = end at the last line of history (1 before the + // visible area is -1). + // -t %{d} = target a specific pane ID + "capture-pane -p -e -q {s}-S - -E -1 -t %{d}\n", + .{ + if (cap.screen_key == .alternate) "-a " else "", + cap.id, + }, + ), + + .pane_visible => |cap| try writer.print( + // -p = output to stdout instead of buffer + // -e = output escape sequences for SGR + // -a = capture alternate screen (only valid for alternate) + // -q = quiet, don't error if alternate screen doesn't exist + // -t %{d} = target a specific pane ID + // (no -S/-E = capture visible area only) + "capture-pane -p -e -q {s}-t %{d}\n", + .{ + if (cap.screen_key == .alternate) "-a " else "", + cap.id, + }, + ), + + .pane_state => try writer.writeAll(std.fmt.comptimePrint( + "list-panes -F '{s}'\n", + .{comptime Format.list_panes.comptimeFormat()}, + )), + + .tmux_version => try writer.writeAll(std.fmt.comptimePrint( + "display-message -p '{s}'\n", + .{comptime Format.tmux_version.comptimeFormat()}, + )), + + .user => |v| try writer.writeAll(v), + } + } +}; + +/// Format strings used for commands in our viewer. +const Format = struct { + /// The variables included in this format, in order. + vars: []const output.Variable, + + /// The delimiter to use between variables. This must be a character + /// guaranteed to not appear in any of the variable outputs. + delim: u8, + + const list_panes: Format = .{ + .delim = ';', + .vars = &.{ + .pane_id, + // Cursor position & appearance + .cursor_x, + .cursor_y, + .cursor_flag, + .cursor_shape, + .cursor_colour, + .cursor_blinking, + // Alternate screen + .alternate_on, + .alternate_saved_x, + .alternate_saved_y, + // Terminal modes + .insert_flag, + .wrap_flag, + .keypad_flag, + .keypad_cursor_flag, + .origin_flag, + // Mouse modes + .mouse_all_flag, + .mouse_any_flag, + .mouse_button_flag, + .mouse_standard_flag, + .mouse_utf8_flag, + .mouse_sgr_flag, + // Focus & special features + .focus_flag, + .bracketed_paste, + // Scroll region + .scroll_region_upper, + .scroll_region_lower, + // Tab stops + .pane_tabs, + }, + }; + + const list_windows: Format = .{ + .delim = ' ', + .vars = &.{ + .session_id, + .window_id, + .window_width, + .window_height, + .window_layout, + }, + }; + + const tmux_version: Format = .{ + .delim = ' ', + .vars = &.{.version}, + }; + + /// The format string, available at comptime. + pub fn comptimeFormat(comptime self: Format) []const u8 { + return output.comptimeFormat(self.vars, self.delim); + } + + /// The struct that can contain the parsed output. + pub fn Struct(comptime self: Format) type { + return output.FormatStruct(self.vars); + } +}; + +const TestStep = struct { + input: Viewer.Input, + contains_tags: []const std.meta.Tag(Viewer.Action) = &.{}, + contains_command: []const u8 = "", + check: ?*const fn (viewer: *Viewer, []const Viewer.Action) anyerror!void = null, + check_command: ?*const fn (viewer: *Viewer, []const u8) anyerror!void = null, + + fn run(self: TestStep, viewer: *Viewer) !void { + const actions = viewer.next(self.input); + + // Common mistake, forgetting the newline on a command. + for (actions) |action| { + if (action == .command) { + try testing.expect(std.mem.endsWith(u8, action.command, "\n")); + } + } + + for (self.contains_tags) |tag| { + var found = false; + for (actions) |action| { + if (action == tag) { + found = true; + break; + } + } + try testing.expect(found); + } + + if (self.contains_command.len > 0) { + var found = false; + for (actions) |action| { + if (action == .command and + std.mem.startsWith(u8, action.command, self.contains_command)) + { + found = true; + break; + } + } + try testing.expect(found); + } + + if (self.check) |check_fn| { + try check_fn(viewer, actions); + } + + if (self.check_command) |check_fn| { + var found = false; + for (actions) |action| { + if (action == .command) { + found = true; + try check_fn(viewer, action.command); + } + } + try testing.expect(found); + } + } +}; + +/// A helper to run a series of test steps against a viewer and assert +/// that the expected actions are produced. +/// +/// I'm generally not a fan of these types of abstracted tests because +/// it makes diagnosing failures harder, but being able to construct +/// simulated tmux inputs and verify outputs is going to be extremely +/// important since the tmux control mode protocol is very complex and +/// fragile. +fn testViewer(viewer: *Viewer, steps: []const TestStep) !void { + for (steps, 0..) |step, i| { + step.run(viewer) catch |err| { + log.warn("testViewer step failed i={} step={}", .{ i, step }); + return err; + }; + } +} + +test "immediate exit" { + var viewer = try Viewer.init(testing.allocator); + defer viewer.deinit(); + + try testViewer(&viewer, &.{ + .{ + .input = .{ .tmux = .exit }, + .contains_tags = &.{.exit}, + }, + .{ + .input = .{ .tmux = .exit }, + .check = (struct { + fn check(_: *Viewer, actions: []const Viewer.Action) anyerror!void { + try testing.expectEqual(0, actions.len); + } + }).check, + }, + }); +} + +test "session changed resets state" { + var viewer = try Viewer.init(testing.allocator); + defer viewer.deinit(); + + try testViewer(&viewer, &.{ + // Initial startup + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + .{ + .input = .{ .tmux = .{ .session_changed = .{ + .id = 1, + .name = "first", + } } }, + .contains_command = "display-message", + }, + // Receive version response, which triggers list-windows + .{ + .input = .{ .tmux = .{ .block_end = "3.5a" } }, + .contains_command = "list-windows", + }, + // Receive window layout with two panes (same format as "initial flow" test) + .{ + .input = .{ .tmux = .{ + .block_end = + \\$1 @0 83 44 027b,83x44,0,0[83x20,0,0,0,83x23,0,21,1] + , + } }, + .contains_tags = &.{ .windows, .command }, + .check = (struct { + fn check(v: *Viewer, _: []const Viewer.Action) anyerror!void { + try testing.expectEqual(1, v.session_id); + try testing.expectEqual(1, v.windows.items.len); + try testing.expectEqual(2, v.panes.count()); + try testing.expectEqualStrings("3.5a", v.tmux_version); + } + }).check, + }, + // Now session changes - should reset everything but keep version + .{ + .input = .{ .tmux = .{ .session_changed = .{ + .id = 2, + .name = "second", + } } }, + .contains_tags = &.{ .windows, .command }, + .contains_command = "list-windows", + .check = (struct { + fn check(v: *Viewer, actions: []const Viewer.Action) anyerror!void { + // Session ID should be updated + try testing.expectEqual(2, v.session_id); + // Windows should be cleared (empty windows action sent) + var found_empty_windows = false; + for (actions) |action| { + if (action == .windows and action.windows.len == 0) { + found_empty_windows = true; + } + } + try testing.expect(found_empty_windows); + // Old windows should be cleared + try testing.expectEqual(0, v.windows.items.len); + // Old panes should be cleared + try testing.expectEqual(0, v.panes.count()); + // Version should still be preserved + try testing.expectEqualStrings("3.5a", v.tmux_version); + } + }).check, + }, + // Receive new window layout for new session (same layout, different session/window) + // Uses same pane IDs 0,1 - they should be re-created since old panes were cleared + .{ + .input = .{ .tmux = .{ + .block_end = + \\$2 @1 83 44 027b,83x44,0,0[83x20,0,0,0,83x23,0,21,1] + , + } }, + .contains_tags = &.{ .windows, .command }, + .check = (struct { + fn check(v: *Viewer, _: []const Viewer.Action) anyerror!void { + try testing.expectEqual(2, v.session_id); + try testing.expectEqual(1, v.windows.items.len); + try testing.expectEqual(1, v.windows.items[0].id); + // Panes 0 and 1 should be created (fresh, since old ones were cleared) + try testing.expectEqual(2, v.panes.count()); + } + }).check, + }, + .{ + .input = .{ .tmux = .exit }, + .contains_tags = &.{.exit}, + }, + }); +} + +test "initial flow" { + var viewer = try Viewer.init(testing.allocator); + defer viewer.deinit(); + + try testViewer(&viewer, &.{ + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + .{ + .input = .{ .tmux = .{ .session_changed = .{ + .id = 42, + .name = "main", + } } }, + .contains_command = "display-message", + .check = (struct { + fn check(v: *Viewer, _: []const Viewer.Action) anyerror!void { + try testing.expectEqual(42, v.session_id); + } + }).check, + }, + // Receive version response, which triggers list-windows + .{ + .input = .{ .tmux = .{ .block_end = "3.5a" } }, + .contains_command = "list-windows", + .check = (struct { + fn check(v: *Viewer, _: []const Viewer.Action) anyerror!void { + try testing.expectEqualStrings("3.5a", v.tmux_version); + } + }).check, + }, + .{ + .input = .{ .tmux = .{ + .block_end = + \\$0 @0 83 44 027b,83x44,0,0[83x20,0,0,0,83x23,0,21,1] + , + } }, + .contains_tags = &.{ .windows, .command }, + .contains_command = "capture-pane", + // pane_history for pane 0 (primary) + .check_command = (struct { + fn check(_: *Viewer, command: []const u8) anyerror!void { + try testing.expect(std.mem.containsAtLeast(u8, command, 1, "-t %0")); + try testing.expect(!std.mem.containsAtLeast(u8, command, 1, "-a")); + } + }).check, + }, + .{ + .input = .{ .tmux = .{ + .block_end = + \\Hello, world! + , + } }, + // Moves on to pane_visible for pane 0 (primary) + .contains_command = "capture-pane", + .check_command = (struct { + fn check(_: *Viewer, command: []const u8) anyerror!void { + try testing.expect(std.mem.containsAtLeast(u8, command, 1, "-t %0")); + try testing.expect(!std.mem.containsAtLeast(u8, command, 1, "-a")); + } + }).check, + .check = (struct { + fn check(v: *Viewer, _: []const Viewer.Action) anyerror!void { + const pane: *Viewer.Pane = v.panes.getEntry(0).?.value_ptr; + const screen: *Screen = pane.terminal.screens.active; + { + const str = try screen.dumpStringAlloc( + testing.allocator, + .{ .history = .{} }, + ); + defer testing.allocator.free(str); + try testing.expectEqualStrings("Hello, world!", str); + } + { + const str = try screen.dumpStringAlloc( + testing.allocator, + .{ .active = .{} }, + ); + defer testing.allocator.free(str); + try testing.expectEqualStrings("", str); + } + } + }).check, + }, + .{ + .input = .{ .tmux = .{ .block_end = "" } }, + // Moves on to pane_history for pane 0 (alternate) + .contains_command = "capture-pane", + .check_command = (struct { + fn check(_: *Viewer, command: []const u8) anyerror!void { + try testing.expect(std.mem.containsAtLeast(u8, command, 1, "-t %0")); + try testing.expect(std.mem.containsAtLeast(u8, command, 1, "-a")); + } + }).check, + }, + .{ + .input = .{ .tmux = .{ .block_end = "" } }, + // Moves on to pane_visible for pane 0 (alternate) + .contains_command = "capture-pane", + .check_command = (struct { + fn check(_: *Viewer, command: []const u8) anyerror!void { + try testing.expect(std.mem.containsAtLeast(u8, command, 1, "-t %0")); + try testing.expect(std.mem.containsAtLeast(u8, command, 1, "-a")); + } + }).check, + }, + .{ + .input = .{ .tmux = .{ .block_end = "" } }, + // Moves on to pane_history for pane 1 (primary) + .contains_command = "capture-pane", + .check_command = (struct { + fn check(_: *Viewer, command: []const u8) anyerror!void { + try testing.expect(std.mem.containsAtLeast(u8, command, 1, "-t %1")); + try testing.expect(!std.mem.containsAtLeast(u8, command, 1, "-a")); + } + }).check, + }, + .{ + .input = .{ .tmux = .{ .block_end = "" } }, + // Moves on to pane_visible for pane 1 (primary) + .contains_command = "capture-pane", + .check_command = (struct { + fn check(_: *Viewer, command: []const u8) anyerror!void { + try testing.expect(std.mem.containsAtLeast(u8, command, 1, "-t %1")); + try testing.expect(!std.mem.containsAtLeast(u8, command, 1, "-a")); + } + }).check, + }, + .{ + .input = .{ .tmux = .{ .block_end = "" } }, + // Moves on to pane_history for pane 1 (alternate) + .contains_command = "capture-pane", + .check_command = (struct { + fn check(_: *Viewer, command: []const u8) anyerror!void { + try testing.expect(std.mem.containsAtLeast(u8, command, 1, "-t %1")); + try testing.expect(std.mem.containsAtLeast(u8, command, 1, "-a")); + } + }).check, + }, + .{ + .input = .{ .tmux = .{ .block_end = "" } }, + // Moves on to pane_visible for pane 1 (alternate) + .contains_command = "capture-pane", + .check_command = (struct { + fn check(_: *Viewer, command: []const u8) anyerror!void { + try testing.expect(std.mem.containsAtLeast(u8, command, 1, "-t %1")); + try testing.expect(std.mem.containsAtLeast(u8, command, 1, "-a")); + } + }).check, + }, + .{ + .input = .{ .tmux = .{ .block_end = "" } }, + }, + .{ + .input = .{ .tmux = .{ .output = .{ .pane_id = 0, .data = "new output" } } }, + .check = (struct { + fn check(v: *Viewer, actions: []const Viewer.Action) anyerror!void { + try testing.expectEqual(0, actions.len); + const pane: *Viewer.Pane = v.panes.getEntry(0).?.value_ptr; + const screen: *Screen = pane.terminal.screens.active; + const str = try screen.dumpStringAlloc( + testing.allocator, + .{ .active = .{} }, + ); + defer testing.allocator.free(str); + try testing.expect(std.mem.containsAtLeast(u8, str, 1, "new output")); + } + }).check, + }, + .{ + .input = .{ .tmux = .{ .output = .{ .pane_id = 999, .data = "ignored" } } }, + .check = (struct { + fn check(_: *Viewer, actions: []const Viewer.Action) anyerror!void { + try testing.expectEqual(0, actions.len); + } + }).check, + }, + .{ + .input = .{ .tmux = .exit }, + .contains_tags = &.{.exit}, + }, + }); +} + +test "layout change" { + var viewer = try Viewer.init(testing.allocator); + defer viewer.deinit(); + + try testViewer(&viewer, &.{ + // Initial startup + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + .{ + .input = .{ .tmux = .{ .session_changed = .{ + .id = 1, + .name = "test", + } } }, + .contains_command = "display-message", + }, + // Receive version response, which triggers list-windows + .{ + .input = .{ .tmux = .{ .block_end = "3.5a" } }, + .contains_command = "list-windows", + }, + // Receive initial window layout with one pane + .{ + .input = .{ .tmux = .{ + .block_end = + \\$0 @0 83 44 b7dd,83x44,0,0,0 + , + } }, + .contains_tags = &.{ .windows, .command }, + .check = (struct { + fn check(v: *Viewer, _: []const Viewer.Action) anyerror!void { + try testing.expectEqual(1, v.windows.items.len); + try testing.expectEqual(1, v.panes.count()); + try testing.expect(v.panes.contains(0)); + } + }).check, + }, + // Complete all capture-pane commands for pane 0 (primary and alternate) + // plus pane_state + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + // Now send a layout_change that splits into two panes + .{ + .input = .{ .tmux = .{ .layout_change = .{ + .window_id = 0, + .layout = "e07b,83x44,0,0[83x22,0,0,0,83x21,0,23,2]", + .visible_layout = "e07b,83x44,0,0[83x22,0,0,0,83x21,0,23,2]", + .raw_flags = "*", + } } }, + .contains_tags = &.{.windows}, + .check = (struct { + fn check(v: *Viewer, _: []const Viewer.Action) anyerror!void { + // Should still have 1 window + try testing.expectEqual(1, v.windows.items.len); + // Should now have 2 panes (0 and 2) + try testing.expectEqual(2, v.panes.count()); + try testing.expect(v.panes.contains(0)); + try testing.expect(v.panes.contains(2)); + // Commands should be queued for the new pane (4 capture-pane + 1 pane_state) + try testing.expectEqual(5, v.command_queue.len()); + } + }).check, + }, + .{ + .input = .{ .tmux = .exit }, + .contains_tags = &.{.exit}, + }, + }); +} + +test "layout_change does not return command when queue not empty" { + var viewer = try Viewer.init(testing.allocator); + defer viewer.deinit(); + + try testViewer(&viewer, &.{ + // Initial startup + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + .{ + .input = .{ .tmux = .{ .session_changed = .{ + .id = 1, + .name = "test", + } } }, + .contains_command = "display-message", + }, + // Receive version response, which triggers list-windows + .{ + .input = .{ .tmux = .{ .block_end = "3.5a" } }, + .contains_command = "list-windows", + }, + // Receive initial window layout with one pane + .{ + .input = .{ .tmux = .{ + .block_end = + \\$0 @0 83 44 b7dd,83x44,0,0,0 + , + } }, + .contains_tags = &.{ .windows, .command }, + .check = (struct { + fn check(v: *Viewer, _: []const Viewer.Action) anyerror!void { + try testing.expect(!v.command_queue.empty()); + } + }).check, + }, + // Do NOT complete capture-pane commands - queue still has commands. + // Send a layout_change that splits into two panes. + // This should NOT return a command action since queue was not empty. + .{ + .input = .{ .tmux = .{ .layout_change = .{ + .window_id = 0, + .layout = "e07b,83x44,0,0[83x22,0,0,0,83x21,0,23,2]", + .visible_layout = "e07b,83x44,0,0[83x22,0,0,0,83x21,0,23,2]", + .raw_flags = "*", + } } }, + .contains_tags = &.{.windows}, + .check = (struct { + fn check(v: *Viewer, actions: []const Viewer.Action) anyerror!void { + try testing.expectEqual(2, v.panes.count()); + // Should not contain a command action + for (actions) |action| { + try testing.expect(action != .command); + } + } + }).check, + }, + .{ + .input = .{ .tmux = .exit }, + .contains_tags = &.{.exit}, + }, + }); +} + +test "layout_change returns command when queue was empty" { + var viewer = try Viewer.init(testing.allocator); + defer viewer.deinit(); + + try testViewer(&viewer, &.{ + // Initial startup + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + .{ + .input = .{ .tmux = .{ .session_changed = .{ + .id = 1, + .name = "test", + } } }, + .contains_command = "display-message", + }, + // Receive version response, which triggers list-windows + .{ + .input = .{ .tmux = .{ .block_end = "3.5a" } }, + .contains_command = "list-windows", + }, + // Receive initial window layout with one pane + .{ + .input = .{ .tmux = .{ + .block_end = + \\$0 @0 83 44 b7dd,83x44,0,0,0 + , + } }, + .contains_tags = &.{ .windows, .command }, + }, + // Complete all capture-pane commands for pane 0 + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + // Queue should now be empty + .{ + .input = .{ .tmux = .{ .block_end = "" } }, + .check = (struct { + fn check(v: *Viewer, _: []const Viewer.Action) anyerror!void { + try testing.expect(v.command_queue.empty()); + } + }).check, + }, + // Now send a layout_change that splits into two panes. + // This should return a command action since we're queuing commands + // for the new pane and the queue was empty. + .{ + .input = .{ .tmux = .{ .layout_change = .{ + .window_id = 0, + .layout = "e07b,83x44,0,0[83x22,0,0,0,83x21,0,23,2]", + .visible_layout = "e07b,83x44,0,0[83x22,0,0,0,83x21,0,23,2]", + .raw_flags = "*", + } } }, + .contains_tags = &.{ .windows, .command }, + .check = (struct { + fn check(v: *Viewer, _: []const Viewer.Action) anyerror!void { + try testing.expectEqual(2, v.panes.count()); + try testing.expect(!v.command_queue.empty()); + } + }).check, + }, + .{ + .input = .{ .tmux = .exit }, + .contains_tags = &.{.exit}, + }, + }); +} + +test "window_add queues list_windows when queue empty" { + var viewer = try Viewer.init(testing.allocator); + defer viewer.deinit(); + + try testViewer(&viewer, &.{ + // Initial startup + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + .{ + .input = .{ .tmux = .{ .session_changed = .{ + .id = 1, + .name = "test", + } } }, + .contains_command = "display-message", + }, + // Receive version response, which triggers list-windows + .{ + .input = .{ .tmux = .{ .block_end = "3.5a" } }, + .contains_command = "list-windows", + }, + // Receive initial window layout with one pane + .{ + .input = .{ .tmux = .{ + .block_end = + \\$0 @0 83 44 b7dd,83x44,0,0,0 + , + } }, + .contains_tags = &.{ .windows, .command }, + }, + // Complete all capture-pane commands for pane 0 + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + // Queue should now be empty + .{ + .input = .{ .tmux = .{ .block_end = "" } }, + .check = (struct { + fn check(v: *Viewer, _: []const Viewer.Action) anyerror!void { + try testing.expect(v.command_queue.empty()); + } + }).check, + }, + // Now send window_add - should trigger list-windows command + .{ + .input = .{ .tmux = .{ .window_add = .{ .id = 1 } } }, + .contains_command = "list-windows", + .check = (struct { + fn check(v: *Viewer, _: []const Viewer.Action) anyerror!void { + // Command queue should have list_windows + try testing.expect(!v.command_queue.empty()); + try testing.expectEqual(1, v.command_queue.len()); + } + }).check, + }, + .{ + .input = .{ .tmux = .exit }, + .contains_tags = &.{.exit}, + }, + }); +} + +test "window_add queues list_windows when queue not empty" { + var viewer = try Viewer.init(testing.allocator); + defer viewer.deinit(); + + try testViewer(&viewer, &.{ + // Initial startup + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + .{ + .input = .{ .tmux = .{ .session_changed = .{ + .id = 1, + .name = "test", + } } }, + .contains_command = "display-message", + }, + // Receive version response, which triggers list-windows + .{ + .input = .{ .tmux = .{ .block_end = "3.5a" } }, + .contains_command = "list-windows", + }, + // Receive initial window layout with one pane + .{ + .input = .{ .tmux = .{ + .block_end = + \\$0 @0 83 44 b7dd,83x44,0,0,0 + , + } }, + .contains_tags = &.{ .windows, .command }, + .check = (struct { + fn check(v: *Viewer, _: []const Viewer.Action) anyerror!void { + // Queue should have capture-pane commands + try testing.expect(!v.command_queue.empty()); + } + }).check, + }, + // Do NOT complete capture-pane commands - queue still has commands. + // Send window_add - should queue list-windows but NOT return command action + .{ + .input = .{ .tmux = .{ .window_add = .{ .id = 1 } } }, + .check = (struct { + fn check(v: *Viewer, actions: []const Viewer.Action) anyerror!void { + // Should not contain a command action since queue was not empty + for (actions) |action| { + try testing.expect(action != .command); + } + // But list_windows should be in the queue + try testing.expect(!v.command_queue.empty()); + } + }).check, + }, + .{ + .input = .{ .tmux = .exit }, + .contains_tags = &.{.exit}, + }, + }); +} + +test "two pane flow with pane state" { + var viewer = try Viewer.init(testing.allocator); + defer viewer.deinit(); + + try testViewer(&viewer, &.{ + // Initial block_end from attach + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + // Session changed notification + .{ + .input = .{ .tmux = .{ .session_changed = .{ + .id = 0, + .name = "0", + } } }, + .contains_command = "display-message", + .check = (struct { + fn check(v: *Viewer, _: []const Viewer.Action) anyerror!void { + try testing.expectEqual(0, v.session_id); + } + }).check, + }, + // Receive version response, which triggers list-windows + .{ + .input = .{ .tmux = .{ .block_end = "3.5a" } }, + .contains_command = "list-windows", + }, + // list-windows output with 2 panes in a vertical split + .{ + .input = .{ .tmux = .{ + .block_end = + \\$0 @0 165 79 ca97,165x79,0,0[165x40,0,0,0,165x38,0,41,4] + , + } }, + .contains_tags = &.{ .windows, .command }, + .check = (struct { + fn check(v: *Viewer, _: []const Viewer.Action) anyerror!void { + try testing.expectEqual(1, v.windows.items.len); + const window = v.windows.items[0]; + try testing.expectEqual(0, window.id); + try testing.expectEqual(165, window.width); + try testing.expectEqual(79, window.height); + try testing.expectEqual(2, v.panes.count()); + try testing.expect(v.panes.contains(0)); + try testing.expect(v.panes.contains(4)); + } + }).check, + }, + // capture-pane pane 0 primary history + .{ + .input = .{ .tmux = .{ + .block_end = + \\prompt % + \\prompt % + , + } }, + }, + // capture-pane pane 0 primary visible + .{ + .input = .{ .tmux = .{ + .block_end = + \\prompt % + , + } }, + .check = (struct { + fn check(v: *Viewer, _: []const Viewer.Action) anyerror!void { + const pane: *Viewer.Pane = v.panes.getEntry(0).?.value_ptr; + const screen: *Screen = pane.terminal.screens.active; + { + const str = try screen.dumpStringAlloc( + testing.allocator, + .{ .history = .{} }, + ); + defer testing.allocator.free(str); + // History has 2 lines with "prompt %" (padded to screen width) + try testing.expect(std.mem.containsAtLeast(u8, str, 2, "prompt %")); + } + { + const str = try screen.dumpStringAlloc( + testing.allocator, + .{ .active = .{} }, + ); + defer testing.allocator.free(str); + try testing.expectEqualStrings("prompt %", str); + } + } + }).check, + }, + // capture-pane pane 0 alternate history (empty) + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + // capture-pane pane 0 alternate visible (empty) + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + // capture-pane pane 4 primary history + .{ + .input = .{ .tmux = .{ + .block_end = + \\prompt % + , + } }, + }, + // capture-pane pane 4 primary visible + .{ + .input = .{ .tmux = .{ + .block_end = + \\prompt % + , + } }, + .check = (struct { + fn check(v: *Viewer, _: []const Viewer.Action) anyerror!void { + const pane: *Viewer.Pane = v.panes.getEntry(4).?.value_ptr; + const screen: *Screen = pane.terminal.screens.active; + { + const str = try screen.dumpStringAlloc( + testing.allocator, + .{ .history = .{} }, + ); + defer testing.allocator.free(str); + try testing.expectEqualStrings("prompt %", str); + } + { + const str = try screen.dumpStringAlloc( + testing.allocator, + .{ .active = .{} }, + ); + defer testing.allocator.free(str); + // Active screen starts with "prompt %" at beginning + try testing.expect(std.mem.startsWith(u8, str, "prompt %")); + } + } + }).check, + }, + // capture-pane pane 4 alternate history (empty) + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + // capture-pane pane 4 alternate visible (empty) + .{ .input = .{ .tmux = .{ .block_end = "" } } }, + // list-panes output with terminal state + .{ + .input = .{ .tmux = .{ + .block_end = + \\%0;42;0;1;;;;0;4294967295;4294967295;0;1;0;0;0;0;0;0;0;0;0;;;0;39;8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128,136,144,152,160 + \\%4;10;5;1;;;;0;4294967295;4294967295;0;1;0;0;0;0;0;0;0;0;0;;;0;37;8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128,136,144,152,160 + , + } }, + .check = (struct { + fn check(v: *Viewer, _: []const Viewer.Action) anyerror!void { + // Pane 0: cursor at (42, 0), cursor visible, wraparound on + { + const pane: *Viewer.Pane = v.panes.getEntry(0).?.value_ptr; + const t: *Terminal = &pane.terminal; + const screen: *Screen = t.screens.get(.primary).?; + try testing.expectEqual(42, screen.cursor.x); + try testing.expectEqual(0, screen.cursor.y); + try testing.expect(t.modes.get(.cursor_visible)); + try testing.expect(t.modes.get(.wraparound)); + try testing.expect(!t.modes.get(.insert)); + try testing.expect(!t.modes.get(.origin)); + try testing.expect(!t.modes.get(.keypad_keys)); + try testing.expect(!t.modes.get(.cursor_keys)); + } + // Pane 4: cursor at (10, 5), cursor visible, wraparound on + { + const pane: *Viewer.Pane = v.panes.getEntry(4).?.value_ptr; + const t: *Terminal = &pane.terminal; + const screen: *Screen = t.screens.get(.primary).?; + try testing.expectEqual(10, screen.cursor.x); + try testing.expectEqual(5, screen.cursor.y); + try testing.expect(t.modes.get(.cursor_visible)); + try testing.expect(t.modes.get(.wraparound)); + try testing.expect(!t.modes.get(.insert)); + try testing.expect(!t.modes.get(.origin)); + try testing.expect(!t.modes.get(.keypad_keys)); + try testing.expect(!t.modes.get(.cursor_keys)); + } + } + }).check, + }, + .{ + .input = .{ .tmux = .exit }, + .contains_tags = &.{.exit}, + }, + }); +} diff --git a/src/terminal/x11_color.zig b/src/terminal/x11_color.zig index 477218d6f..95dcb76db 100644 --- a/src/terminal/x11_color.zig +++ b/src/terminal/x11_color.zig @@ -3,11 +3,14 @@ const assert = @import("../quirks.zig").inlineAssert; const RGB = @import("color.zig").RGB; /// The map of all available X11 colors. -pub const map = colorMap() catch @compileError("failed to parse rgb.txt"); +pub const map = colorMap(); -pub const ColorMap = std.StaticStringMapWithEql(RGB, std.static_string_map.eqlAsciiIgnoreCase); +pub const ColorMap = std.StaticStringMapWithEql( + RGB, + std.static_string_map.eqlAsciiIgnoreCase, +); -fn colorMap() !ColorMap { +fn colorMap() ColorMap { @setEvalBranchQuota(500_000); const KV = struct { []const u8, RGB }; diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 7c7b711fd..0e7cdc172 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -750,15 +750,15 @@ const Subprocess = struct { else => "sh", } }; + // Always set up shell features (GHOSTTY_SHELL_FEATURES). These are + // used by both automatic and manual shell integrations. + try shell_integration.setupFeatures( + &env, + cfg.shell_integration_features, + ); + const force: ?shell_integration.Shell = switch (cfg.shell_integration) { .none => { - // Even if shell integration is none, we still want to - // set up the feature env vars - try shell_integration.setupFeatures( - &env, - cfg.shell_integration_features, - ); - // This is a source of confusion for users despite being // opt-in since it results in some Ghostty features not // working. We always want to log it. @@ -770,6 +770,7 @@ const Subprocess = struct { .bash => .bash, .elvish => .elvish, .fish => .fish, + .nushell => .nushell, .zsh => .zsh, }; @@ -784,7 +785,6 @@ const Subprocess = struct { default_shell_command, &env, force, - cfg.shell_integration_features, ) orelse { log.warn("shell could not be detected, no automatic shell integration will be injected", .{}); break :shell default_shell_command; diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index 53df00433..a1bcea6d3 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -22,6 +22,9 @@ const configpkg = @import("../config.zig"); const log = std.log.scoped(.io_exec); +/// Mutex state argument for queueMessage. +pub const MutexState = enum { locked, unlocked }; + /// Allocator alloc: Allocator, @@ -162,6 +165,7 @@ pub const DerivedConfig = struct { osc_color_report_format: configpkg.Config.OSCColorReportFormat, clipboard_write: configpkg.ClipboardAccess, enquiry_response: []const u8, + conditional_state: configpkg.ConditionalState, pub fn init( alloc_gpa: Allocator, @@ -182,6 +186,7 @@ pub const DerivedConfig = struct { .osc_color_report_format = config.@"osc-color-report-format", .clipboard_write = config.@"clipboard-write", .enquiry_response = try alloc.dupe(u8, config.@"enquiry-response"), + .conditional_state = config._conditional_state, // This has to be last so that we copy AFTER the arena allocations // above happen (Zig assigns in order). @@ -380,7 +385,7 @@ pub fn threadExit(self: *Termio, data: *ThreadData) void { pub fn queueMessage( self: *Termio, msg: termio.Message, - mutex: enum { locked, unlocked }, + mutex: MutexState, ) void { self.mailbox.send(msg, switch (mutex) { .locked => self.renderer_state.mutex, @@ -709,6 +714,25 @@ fn processOutputLocked(self: *Termio, buf: []const u8) void { } } +/// Sends a DSR response for the current color scheme to the pty. +pub fn colorSchemeReport(self: *Termio, td: *ThreadData, force: bool) !void { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + try self.colorSchemeReportLocked(td, force); +} + +pub fn colorSchemeReportLocked(self: *Termio, td: *ThreadData, force: bool) !void { + if (!force and !self.renderer_state.terminal.modes.get(.report_color_scheme)) { + return; + } + const output = switch (self.config.conditional_state.theme) { + .light => "\x1B[?997;2n", + .dark => "\x1B[?997;1n", + }; + try self.queueWrite(td, output, false); +} + /// ThreadData is the data created and stored in the termio thread /// when the thread is started and destroyed when the thread is /// stopped. diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index b111d5a52..6aa5e1c26 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -311,6 +311,7 @@ fn drainMailbox( log.debug("mailbox message={s}", .{@tagName(message)}); switch (message) { + .color_scheme_report => |v| try io.colorSchemeReport(data, v.force), .crash => @panic("crash request, crashing intentionally"), .change_config => |config| { defer config.alloc.destroy(config.ptr); diff --git a/src/termio/message.zig b/src/termio/message.zig index f78da2058..d7a59bf5e 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -16,6 +16,12 @@ pub const Message = union(enum) { /// in the future. pub const WriteReq = MessageData(u8, 38); + /// Request a color scheme report is sent to the pty. + color_scheme_report: struct { + /// Force write the current color scheme + force: bool, + }, + /// Purposely crash the renderer. This is used for testing and debugging. /// See the "crash" binding action. crash: void, diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index c2a637b80..ab6dcd6ff 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -14,6 +14,7 @@ pub const Shell = enum { bash, elvish, fish, + nushell, zsh, }; @@ -44,100 +45,43 @@ pub fn setup( command: config.Command, env: *EnvMap, force_shell: ?Shell, - features: config.ShellIntegrationFeatures, ) !?ShellIntegration { - const exe = if (force_shell) |shell| switch (shell) { - .bash => "bash", - .elvish => "elvish", - .fish => "fish", - .zsh => "zsh", - } else switch (command) { - .direct => |v| std.fs.path.basename(v[0]), - .shell => |v| exe: { - // Shell strings can include spaces so we want to only - // look up to the space if it exists. No shell that we integrate - // has spaces. - const idx = std.mem.indexOfScalar(u8, v, ' ') orelse v.len; - break :exe std.fs.path.basename(v[0..idx]); - }, - }; + const shell: Shell = force_shell orelse + try detectShell(alloc_arena, command) orelse + return null; - const result = try setupShell( - alloc_arena, - resource_dir, - command, - env, - exe, - ); - - // Setup our feature env vars - try setupFeatures(env, features); - - return result; -} - -fn setupShell( - alloc_arena: Allocator, - resource_dir: []const u8, - command: config.Command, - env: *EnvMap, - exe: []const u8, -) !?ShellIntegration { - if (std.mem.eql(u8, "bash", exe)) { - // Apple distributes their own patched version of Bash 3.2 - // on macOS that disables the ENV-based POSIX startup path. - // This means we're unable to perform our automatic shell - // integration sequence in this specific environment. - // - // If we're running "/bin/bash" on Darwin, we can assume - // we're using Apple's Bash because /bin is non-writable - // on modern macOS due to System Integrity Protection. - if (comptime builtin.target.os.tag.isDarwin()) { - if (std.mem.eql(u8, "/bin/bash", switch (command) { - .direct => |v| v[0], - .shell => |v| v, - })) { - return null; - } - } - - const new_command = try setupBash( + const new_command: config.Command = switch (shell) { + .bash => try setupBash( alloc_arena, command, resource_dir, env, - ) orelse return null; - return .{ - .shell = .bash, - .command = new_command, - }; - } + ), - if (std.mem.eql(u8, "elvish", exe)) { - try setupXdgDataDirs(alloc_arena, resource_dir, env); - return .{ - .shell = .elvish, - .command = try command.clone(alloc_arena), - }; - } + .nushell => try setupNushell( + alloc_arena, + command, + resource_dir, + env, + ), - if (std.mem.eql(u8, "fish", exe)) { - try setupXdgDataDirs(alloc_arena, resource_dir, env); - return .{ - .shell = .fish, - .command = try command.clone(alloc_arena), - }; - } + .zsh => try setupZsh( + alloc_arena, + command, + resource_dir, + env, + ), - if (std.mem.eql(u8, "zsh", exe)) { - try setupZsh(resource_dir, env); - return .{ - .shell = .zsh, - .command = try command.clone(alloc_arena), - }; - } + .elvish, .fish => xdg: { + if (!try setupXdgDataDirs(alloc_arena, resource_dir, env)) return null; + break :xdg try command.clone(alloc_arena); + }, + } orelse return null; - return null; + return .{ + .shell = shell, + .command = new_command, + }; } test "force shell" { @@ -152,18 +96,94 @@ test "force shell" { inline for (@typeInfo(Shell).@"enum".fields) |field| { const shell = @field(Shell, field.name); + + var res: TmpResourcesDir = try .init(alloc, shell); + defer res.deinit(); + const result = try setup( alloc, - ".", + res.path, .{ .shell = "sh" }, &env, shell, - .{}, ); try testing.expectEqual(shell, result.?.shell); } } +test "shell integration failure" { + const testing = std.testing; + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var env = EnvMap.init(alloc); + defer env.deinit(); + + const result = try setup( + alloc, + "/nonexistent", + .{ .shell = "sh" }, + &env, + null, + ); + + try testing.expect(result == null); + try testing.expectEqual(0, env.count()); +} + +fn detectShell(alloc: Allocator, command: config.Command) !?Shell { + var arg_iter = try command.argIterator(alloc); + defer arg_iter.deinit(); + + const arg0 = arg_iter.next() orelse return null; + const exe = std.fs.path.basename(arg0); + + if (std.mem.eql(u8, "bash", exe)) { + // Apple distributes their own patched version of Bash 3.2 + // on macOS that disables the ENV-based POSIX startup path. + // This means we're unable to perform our automatic shell + // integration sequence in this specific environment. + // + // If we're running "/bin/bash" on Darwin, we can assume + // we're using Apple's Bash because /bin is non-writable + // on modern macOS due to System Integrity Protection. + if (comptime builtin.target.os.tag.isDarwin()) { + if (std.mem.eql(u8, "/bin/bash", arg0)) { + return null; + } + } + return .bash; + } + + if (std.mem.eql(u8, "elvish", exe)) return .elvish; + if (std.mem.eql(u8, "fish", exe)) return .fish; + if (std.mem.eql(u8, "nu", exe)) return .nushell; + if (std.mem.eql(u8, "zsh", exe)) return .zsh; + + return null; +} + +test detectShell { + const testing = std.testing; + const alloc = testing.allocator; + + try testing.expect(try detectShell(alloc, .{ .shell = "sh" }) == null); + try testing.expectEqual(.bash, try detectShell(alloc, .{ .shell = "bash" })); + try testing.expectEqual(.elvish, try detectShell(alloc, .{ .shell = "elvish" })); + try testing.expectEqual(.fish, try detectShell(alloc, .{ .shell = "fish" })); + try testing.expectEqual(.nushell, try detectShell(alloc, .{ .shell = "nu" })); + try testing.expectEqual(.zsh, try detectShell(alloc, .{ .shell = "zsh" })); + + if (comptime builtin.target.os.tag.isDarwin()) { + try testing.expect(try detectShell(alloc, .{ .shell = "/bin/bash" }) == null); + } + + try testing.expectEqual(.bash, try detectShell(alloc, .{ .shell = "bash -c 'command'" })); + try testing.expectEqual(.bash, try detectShell(alloc, .{ .shell = "\"/a b/bash\"" })); +} + /// Set up the shell integration features environment variable. pub fn setupFeatures( env: *EnvMap, @@ -230,7 +250,7 @@ test "setup features" { var env = EnvMap.init(alloc); defer env.deinit(); - try setupFeatures(&env, .{ .cursor = false, .sudo = false, .title = false, .@"ssh-env" = false, .@"ssh-terminfo" = false, .path = false }); + try setupFeatures(&env, std.mem.zeroes(config.ShellIntegrationFeatures)); try testing.expect(env.get("GHOSTTY_SHELL_FEATURES") == null); } @@ -259,8 +279,9 @@ fn setupBash( resource_dir: []const u8, env: *EnvMap, ) !?config.Command { - var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 3); - defer args.deinit(alloc); + var stack_fallback = std.heap.stackFallback(4096, alloc); + var cmd = internal_os.shell.ShellCommandBuilder.init(stack_fallback.get()); + defer cmd.deinit(); // Iterator that yields each argument in the original command line. // This will allocate once proportionate to the command line length. @@ -269,14 +290,9 @@ fn setupBash( // Start accumulating arguments with the executable and initial flags. if (iter.next()) |exe| { - try args.append(alloc, try alloc.dupeZ(u8, exe)); + try cmd.appendArg(exe); } else return null; - try args.append(alloc, "--posix"); - - // On macOS, we request a login shell to match that platform's norms. - if (comptime builtin.target.os.tag.isDarwin()) { - try args.append(alloc, "--login"); - } + try cmd.appendArg("--posix"); // Stores the list of intercepted command line flags that will be passed // to our shell integration script: --norc --noprofile @@ -309,19 +325,41 @@ fn setupBash( if (std.mem.indexOfScalar(u8, arg, 'c') != null) { return null; } - try args.append(alloc, try alloc.dupeZ(u8, arg)); + try cmd.appendArg(arg); } else if (std.mem.eql(u8, arg, "-") or std.mem.eql(u8, arg, "--")) { // All remaining arguments should be passed directly to the shell // command. We shouldn't perform any further option processing. - try args.append(alloc, try alloc.dupeZ(u8, arg)); + try cmd.appendArg(arg); while (iter.next()) |remaining_arg| { - try args.append(alloc, try alloc.dupeZ(u8, remaining_arg)); + try cmd.appendArg(remaining_arg); } break; } else { - try args.append(alloc, try alloc.dupeZ(u8, arg)); + try cmd.appendArg(arg); } } + + // Preserve an existing ENV value. We're about to overwrite it. + if (env.get("ENV")) |v| { + try env.put("GHOSTTY_BASH_ENV", v); + } + + // Set our new ENV to point to our integration script. + var script_path_buf: [std.fs.max_path_bytes]u8 = undefined; + const script_path = try std.fmt.bufPrint( + &script_path_buf, + "{s}/shell-integration/bash/ghostty.bash", + .{resource_dir}, + ); + if (std.fs.openFileAbsolute(script_path, .{})) |file| { + file.close(); + try env.put("ENV", script_path); + } else |err| { + log.warn("unable to open {s}: {}", .{ script_path, err }); + env.remove("GHOSTTY_BASH_ENV"); + return null; + } + try env.put("GHOSTTY_BASH_INJECT", buf[0..inject.end]); if (rcfile) |v| { try env.put("GHOSTTY_BASH_RCFILE", v); @@ -343,23 +381,8 @@ fn setupBash( } } - // Preserve an existing ENV value. We're about to overwrite it. - if (env.get("ENV")) |v| { - try env.put("GHOSTTY_BASH_ENV", v); - } - - // Set our new ENV to point to our integration script. - var path_buf: [std.fs.max_path_bytes]u8 = undefined; - const integ_dir = try std.fmt.bufPrint( - &path_buf, - "{s}/shell-integration/bash/ghostty.bash", - .{resource_dir}, - ); - try env.put("ENV", integ_dir); - - // Since we built up a command line, we don't need to wrap it in - // ANOTHER shell anymore and can do a direct command. - return .{ .direct = try args.toOwnedSlice(alloc) }; + // Return a copy of our modified command line to use as the shell command. + return .{ .shell = try alloc.dupeZ(u8, try cmd.toOwnedSlice()) }; } test "bash" { @@ -368,19 +391,21 @@ test "bash" { defer arena.deinit(); const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(alloc, .bash); + defer res.deinit(); + var env = EnvMap.init(alloc); defer env.deinit(); - const command = try setupBash(alloc, .{ .shell = "bash" }, ".", &env); - - try testing.expect(command.?.direct.len >= 2); - try testing.expectEqualStrings("bash", command.?.direct[0]); - try testing.expectEqualStrings("--posix", command.?.direct[1]); - if (comptime builtin.target.os.tag.isDarwin()) { - try testing.expectEqualStrings("--login", command.?.direct[2]); - } - try testing.expectEqualStrings("./shell-integration/bash/ghostty.bash", env.get("ENV").?); + const command = try setupBash(alloc, .{ .shell = "bash" }, res.path, &env); + try testing.expectEqualStrings("bash --posix", command.?.shell); try testing.expectEqualStrings("1", env.get("GHOSTTY_BASH_INJECT").?); + + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + try testing.expectEqualStrings( + try std.fmt.bufPrint(&path_buf, "{s}/ghostty.bash", .{res.shell_path}), + env.get("ENV").?, + ); } test "bash: unsupported options" { @@ -389,6 +414,9 @@ test "bash: unsupported options" { defer arena.deinit(); const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(alloc, .bash); + defer res.deinit(); + const cmdlines = [_][:0]const u8{ "bash --posix", "bash --rcfile script.sh --posix", @@ -401,10 +429,8 @@ test "bash: unsupported options" { var env = EnvMap.init(alloc); defer env.deinit(); - try testing.expect(try setupBash(alloc, .{ .shell = cmdline }, ".", &env) == null); - try testing.expect(env.get("GHOSTTY_BASH_INJECT") == null); - try testing.expect(env.get("GHOSTTY_BASH_RCFILE") == null); - try testing.expect(env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE") == null); + try testing.expect(try setupBash(alloc, .{ .shell = cmdline }, res.path, &env) == null); + try testing.expectEqual(0, env.count()); } } @@ -414,19 +440,16 @@ test "bash: inject flags" { defer arena.deinit(); const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(alloc, .bash); + defer res.deinit(); + // bash --norc { var env = EnvMap.init(alloc); defer env.deinit(); - const command = try setupBash(alloc, .{ .shell = "bash --norc" }, ".", &env); - - try testing.expect(command.?.direct.len >= 2); - try testing.expectEqualStrings("bash", command.?.direct[0]); - try testing.expectEqualStrings("--posix", command.?.direct[1]); - if (comptime builtin.target.os.tag.isDarwin()) { - try testing.expectEqualStrings("--login", command.?.direct[2]); - } + const command = try setupBash(alloc, .{ .shell = "bash --norc" }, res.path, &env); + try testing.expectEqualStrings("bash --posix", command.?.shell); try testing.expectEqualStrings("1 --norc", env.get("GHOSTTY_BASH_INJECT").?); } @@ -435,14 +458,8 @@ test "bash: inject flags" { var env = EnvMap.init(alloc); defer env.deinit(); - const command = try setupBash(alloc, .{ .shell = "bash --noprofile" }, ".", &env); - - try testing.expect(command.?.direct.len >= 2); - try testing.expectEqualStrings("bash", command.?.direct[0]); - try testing.expectEqualStrings("--posix", command.?.direct[1]); - if (comptime builtin.target.os.tag.isDarwin()) { - try testing.expectEqualStrings("--login", command.?.direct[2]); - } + const command = try setupBash(alloc, .{ .shell = "bash --noprofile" }, res.path, &env); + try testing.expectEqualStrings("bash --posix", command.?.shell); try testing.expectEqualStrings("1 --noprofile", env.get("GHOSTTY_BASH_INJECT").?); } } @@ -453,30 +470,23 @@ test "bash: rcfile" { defer arena.deinit(); const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(alloc, .bash); + defer res.deinit(); + var env = EnvMap.init(alloc); defer env.deinit(); // bash --rcfile { - const command = try setupBash(alloc, .{ .shell = "bash --rcfile profile.sh" }, ".", &env); - try testing.expect(command.?.direct.len >= 2); - try testing.expectEqualStrings("bash", command.?.direct[0]); - try testing.expectEqualStrings("--posix", command.?.direct[1]); - if (comptime builtin.target.os.tag.isDarwin()) { - try testing.expectEqualStrings("--login", command.?.direct[2]); - } + const command = try setupBash(alloc, .{ .shell = "bash --rcfile profile.sh" }, res.path, &env); + try testing.expectEqualStrings("bash --posix", command.?.shell); try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?); } // bash --init-file { - const command = try setupBash(alloc, .{ .shell = "bash --init-file profile.sh" }, ".", &env); - try testing.expect(command.?.direct.len >= 2); - try testing.expectEqualStrings("bash", command.?.direct[0]); - try testing.expectEqualStrings("--posix", command.?.direct[1]); - if (comptime builtin.target.os.tag.isDarwin()) { - try testing.expectEqualStrings("--login", command.?.direct[2]); - } + const command = try setupBash(alloc, .{ .shell = "bash --init-file profile.sh" }, res.path, &env); + try testing.expectEqualStrings("bash --posix", command.?.shell); try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?); } } @@ -487,12 +497,15 @@ test "bash: HISTFILE" { defer arena.deinit(); const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(alloc, .bash); + defer res.deinit(); + // HISTFILE unset { var env = EnvMap.init(alloc); defer env.deinit(); - _ = try setupBash(alloc, .{ .shell = "bash" }, ".", &env); + _ = try setupBash(alloc, .{ .shell = "bash" }, res.path, &env); try testing.expect(std.mem.endsWith(u8, env.get("HISTFILE").?, ".bash_history")); try testing.expectEqualStrings("1", env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE").?); } @@ -504,7 +517,7 @@ test "bash: HISTFILE" { try env.put("HISTFILE", "my_history"); - _ = try setupBash(alloc, .{ .shell = "bash" }, ".", &env); + _ = try setupBash(alloc, .{ .shell = "bash" }, res.path, &env); try testing.expectEqualStrings("my_history", env.get("HISTFILE").?); try testing.expect(env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE") == null); } @@ -516,14 +529,22 @@ test "bash: ENV" { defer arena.deinit(); const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(alloc, .bash); + defer res.deinit(); + var env = EnvMap.init(alloc); defer env.deinit(); try env.put("ENV", "env.sh"); - _ = try setupBash(alloc, .{ .shell = "bash" }, ".", &env); - try testing.expectEqualStrings("./shell-integration/bash/ghostty.bash", env.get("ENV").?); + _ = try setupBash(alloc, .{ .shell = "bash" }, res.path, &env); try testing.expectEqualStrings("env.sh", env.get("GHOSTTY_BASH_ENV").?); + + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + try testing.expectEqualStrings( + try std.fmt.bufPrint(&path_buf, "{s}/ghostty.bash", .{res.shell_path}), + env.get("ENV").?, + ); } test "bash: additional arguments" { @@ -532,44 +553,44 @@ test "bash: additional arguments" { defer arena.deinit(); const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(alloc, .bash); + defer res.deinit(); + var env = EnvMap.init(alloc); defer env.deinit(); // "-" argument separator { - const command = try setupBash(alloc, .{ .shell = "bash - --arg file1 file2" }, ".", &env); - try testing.expect(command.?.direct.len >= 6); - try testing.expectEqualStrings("bash", command.?.direct[0]); - try testing.expectEqualStrings("--posix", command.?.direct[1]); - if (comptime builtin.target.os.tag.isDarwin()) { - try testing.expectEqualStrings("--login", command.?.direct[2]); - } - - const offset = if (comptime builtin.target.os.tag.isDarwin()) 3 else 2; - try testing.expectEqualStrings("-", command.?.direct[offset + 0]); - try testing.expectEqualStrings("--arg", command.?.direct[offset + 1]); - try testing.expectEqualStrings("file1", command.?.direct[offset + 2]); - try testing.expectEqualStrings("file2", command.?.direct[offset + 3]); + const command = try setupBash(alloc, .{ .shell = "bash - --arg file1 file2" }, res.path, &env); + try testing.expectEqualStrings("bash --posix - --arg file1 file2", command.?.shell); } // "--" argument separator { - const command = try setupBash(alloc, .{ .shell = "bash -- --arg file1 file2" }, ".", &env); - try testing.expect(command.?.direct.len >= 6); - try testing.expectEqualStrings("bash", command.?.direct[0]); - try testing.expectEqualStrings("--posix", command.?.direct[1]); - if (comptime builtin.target.os.tag.isDarwin()) { - try testing.expectEqualStrings("--login", command.?.direct[2]); - } - - const offset = if (comptime builtin.target.os.tag.isDarwin()) 3 else 2; - try testing.expectEqualStrings("--", command.?.direct[offset + 0]); - try testing.expectEqualStrings("--arg", command.?.direct[offset + 1]); - try testing.expectEqualStrings("file1", command.?.direct[offset + 2]); - try testing.expectEqualStrings("file2", command.?.direct[offset + 3]); + const command = try setupBash(alloc, .{ .shell = "bash -- --arg file1 file2" }, res.path, &env); + try testing.expectEqualStrings("bash --posix -- --arg file1 file2", command.?.shell); } } +test "bash: missing resources" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const resources_dir = try tmp_dir.dir.realpathAlloc(alloc, "."); + defer alloc.free(resources_dir); + + var env = EnvMap.init(alloc); + defer env.deinit(); + + try testing.expect(try setupBash(alloc, .{ .shell = "bash" }, resources_dir, &env) == null); + try testing.expectEqual(0, env.count()); +} + /// Setup automatic shell integration for shells that include /// their modules from paths in `XDG_DATA_DIRS` env variable. /// @@ -578,30 +599,35 @@ test "bash: additional arguments" { /// so that the shell can refer to it and safely remove this directory /// from `XDG_DATA_DIRS` when integration is complete. fn setupXdgDataDirs( - alloc_arena: Allocator, + alloc: Allocator, resource_dir: []const u8, env: *EnvMap, -) !void { +) !bool { var path_buf: [std.fs.max_path_bytes]u8 = undefined; // Get our path to the shell integration directory. - const integ_dir = try std.fmt.bufPrint( + const integ_path = try std.fmt.bufPrint( &path_buf, "{s}/shell-integration", .{resource_dir}, ); + var integ_dir = std.fs.openDirAbsolute(integ_path, .{}) catch |err| { + log.warn("unable to open {s}: {}", .{ integ_path, err }); + return false; + }; + integ_dir.close(); // Set an env var so we can remove this from XDG_DATA_DIRS later. // This happens in the shell integration config itself. We do this // so that our modifications don't interfere with other commands. - try env.put("GHOSTTY_SHELL_INTEGRATION_XDG_DIR", integ_dir); + try env.put("GHOSTTY_SHELL_INTEGRATION_XDG_DIR", integ_path); // We attempt to avoid allocating by using the stack up to 4K. // Max stack size is considerably larger on mac // 4K is a reasonable size for this for most cases. However, env // vars can be significantly larger so if we have to we fall // back to a heap allocated value. - var stack_alloc_state = std.heap.stackFallback(4096, alloc_arena); + var stack_alloc_state = std.heap.stackFallback(4096, alloc); const stack_alloc = stack_alloc_state.get(); // If no XDG_DATA_DIRS set use the default value as specified. @@ -614,9 +640,11 @@ fn setupXdgDataDirs( try internal_os.prependEnv( stack_alloc, env.get(xdg_data_dirs_key) orelse "/usr/local/share:/usr/share", - integ_dir, + integ_path, ), ); + + return true; } test "xdg: empty XDG_DATA_DIRS" { @@ -626,13 +654,23 @@ test "xdg: empty XDG_DATA_DIRS" { defer arena.deinit(); const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(alloc, .fish); + defer res.deinit(); + var env = EnvMap.init(alloc); defer env.deinit(); - try setupXdgDataDirs(alloc, ".", &env); + try testing.expect(try setupXdgDataDirs(alloc, res.path, &env)); - try testing.expectEqualStrings("./shell-integration", env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR").?); - try testing.expectEqualStrings("./shell-integration:/usr/local/share:/usr/share", env.get("XDG_DATA_DIRS").?); + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + try testing.expectEqualStrings( + try std.fmt.bufPrint(&path_buf, "{s}/shell-integration", .{res.path}), + env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR").?, + ); + try testing.expectEqualStrings( + try std.fmt.bufPrint(&path_buf, "{s}/shell-integration:/usr/local/share:/usr/share", .{res.path}), + env.get("XDG_DATA_DIRS").?, + ); } test "xdg: existing XDG_DATA_DIRS" { @@ -642,23 +680,198 @@ test "xdg: existing XDG_DATA_DIRS" { defer arena.deinit(); const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(alloc, .fish); + defer res.deinit(); + var env = EnvMap.init(alloc); defer env.deinit(); try env.put("XDG_DATA_DIRS", "/opt/share"); - try setupXdgDataDirs(alloc, ".", &env); - try testing.expectEqualStrings("./shell-integration", env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR").?); - try testing.expectEqualStrings("./shell-integration:/opt/share", env.get("XDG_DATA_DIRS").?); + try testing.expect(try setupXdgDataDirs(alloc, res.path, &env)); + + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + try testing.expectEqualStrings( + try std.fmt.bufPrint(&path_buf, "{s}/shell-integration", .{res.path}), + env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR").?, + ); + try testing.expectEqualStrings( + try std.fmt.bufPrint(&path_buf, "{s}/shell-integration:/opt/share", .{res.path}), + env.get("XDG_DATA_DIRS").?, + ); +} + +test "xdg: missing resources" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const resources_dir = try tmp_dir.dir.realpathAlloc(alloc, "."); + defer alloc.free(resources_dir); + + var env = EnvMap.init(alloc); + defer env.deinit(); + + try testing.expect(!try setupXdgDataDirs(alloc, resources_dir, &env)); + try testing.expectEqual(0, env.count()); +} + +/// Set up automatic Nushell shell integration. This works by adding our +/// shell resource directory to the `XDG_DATA_DIRS` environment variable, +/// which Nushell will use to load `nushell/vendor/autoload/ghostty.nu`. +/// +/// We then add `--execute 'use ghostty ...'` to the nu command line to +/// automatically enable our shelll features. +fn setupNushell( + alloc: Allocator, + command: config.Command, + resource_dir: []const u8, + env: *EnvMap, +) !?config.Command { + // Add our XDG_DATA_DIRS entry (for nushell/vendor/autoload/). This + // makes our 'ghostty' module automatically available, even if any + // of the later checks abort the rest of our automatic integration. + if (!try setupXdgDataDirs(alloc, resource_dir, env)) return null; + + var stack_fallback = std.heap.stackFallback(4096, alloc); + var cmd = internal_os.shell.ShellCommandBuilder.init(stack_fallback.get()); + defer cmd.deinit(); + + // Iterator that yields each argument in the original command line. + // This will allocate once proportionate to the command line length. + var iter = try command.argIterator(alloc); + defer iter.deinit(); + + // Start accumulating arguments with the executable and initial flags. + if (iter.next()) |exe| { + try cmd.appendArg(exe); + } else return null; + + // Tell nu to immediately "use" all of the exported functions in our + // 'ghostty' module. + // + // We can consider making this more specific based on the set of + // enabled shell features (e.g. `use ghostty sudo`). At the moment, + // shell features are all runtime-guarded in the nushell script. + try cmd.appendArg("--execute 'use ghostty *'"); + + // Walk through the rest of the given arguments. If we see an option that + // would require complex or unsupported integration behavior, we bail out + // and skip loading our shell integration. Users can still manually source + // the shell integration module. + // + // Unsupported options: + // -c / --command -c is always non-interactive + // --lsp --lsp starts the language server + while (iter.next()) |arg| { + if (std.mem.eql(u8, arg, "--command") or std.mem.eql(u8, arg, "--lsp")) { + return null; + } else if (arg.len > 1 and arg[0] == '-' and arg[1] != '-') { + if (std.mem.indexOfScalar(u8, arg, 'c') != null) { + return null; + } + try cmd.appendArg(arg); + } else if (std.mem.eql(u8, arg, "-") or std.mem.eql(u8, arg, "--")) { + // All remaining arguments should be passed directly to the shell + // command. We shouldn't perform any further option processing. + try cmd.appendArg(arg); + while (iter.next()) |remaining_arg| { + try cmd.appendArg(remaining_arg); + } + break; + } else { + try cmd.appendArg(arg); + } + } + + // Return a copy of our modified command line to use as the shell command. + return .{ .shell = try alloc.dupeZ(u8, try cmd.toOwnedSlice()) }; +} + +test "nushell" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var res: TmpResourcesDir = try .init(alloc, .nushell); + defer res.deinit(); + + var env = EnvMap.init(alloc); + defer env.deinit(); + + const command = try setupNushell(alloc, .{ .shell = "nu" }, res.path, &env); + try testing.expectEqualStrings("nu --execute 'use ghostty *'", command.?.shell); + + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + try testing.expectEqualStrings( + try std.fmt.bufPrint(&path_buf, "{s}/shell-integration", .{res.path}), + env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR").?, + ); + try testing.expectStringStartsWith( + env.get("XDG_DATA_DIRS").?, + try std.fmt.bufPrint(&path_buf, "{s}/shell-integration", .{res.path}), + ); +} + +test "nushell: unsupported options" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var res: TmpResourcesDir = try .init(alloc, .nushell); + defer res.deinit(); + + const cmdlines = [_][:0]const u8{ + "nu --command exit", + "nu --lsp", + "nu -c script.sh", + "nu -ic script.sh", + }; + + for (cmdlines) |cmdline| { + var env = EnvMap.init(alloc); + defer env.deinit(); + + try testing.expect(try setupNushell(alloc, .{ .shell = cmdline }, res.path, &env) == null); + try testing.expect(env.get("XDG_DATA_DIRS") != null); + try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR") != null); + } +} + +test "nushell: missing resources" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const resources_dir = try tmp_dir.dir.realpathAlloc(alloc, "."); + defer alloc.free(resources_dir); + + var env = EnvMap.init(alloc); + defer env.deinit(); + + try testing.expect(try setupNushell(alloc, .{ .shell = "nu" }, resources_dir, &env) == null); + try testing.expectEqual(0, env.count()); } /// Setup the zsh automatic shell integration. This works by setting /// ZDOTDIR to our resources dir so that zsh will load our config. This /// config then loads the true user config. fn setupZsh( + alloc: Allocator, + command: config.Command, resource_dir: []const u8, env: *EnvMap, -) !void { +) !?config.Command { // Preserve an existing ZDOTDIR value. We're about to overwrite it. if (env.get("ZDOTDIR")) |old| { try env.put("GHOSTTY_ZSH_ZDOTDIR", old); @@ -666,34 +879,128 @@ fn setupZsh( // Set our new ZDOTDIR to point to our shell resource directory. var path_buf: [std.fs.max_path_bytes]u8 = undefined; - const integ_dir = try std.fmt.bufPrint( + const integ_path = try std.fmt.bufPrint( &path_buf, "{s}/shell-integration/zsh", .{resource_dir}, ); - try env.put("ZDOTDIR", integ_dir); + var integ_dir = std.fs.openDirAbsolute(integ_path, .{}) catch |err| { + log.warn("unable to open {s}: {}", .{ integ_path, err }); + return null; + }; + integ_dir.close(); + try env.put("ZDOTDIR", integ_path); + + return try command.clone(alloc); } test "zsh" { const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var res: TmpResourcesDir = try .init(testing.allocator, .zsh); + defer res.deinit(); + var env = EnvMap.init(testing.allocator); defer env.deinit(); - try setupZsh(".", &env); - try testing.expectEqualStrings("./shell-integration/zsh", env.get("ZDOTDIR").?); + const command = try setupZsh(alloc, .{ .shell = "zsh" }, res.path, &env); + try testing.expectEqualStrings("zsh", command.?.shell); + try testing.expectEqualStrings(res.shell_path, env.get("ZDOTDIR").?); try testing.expect(env.get("GHOSTTY_ZSH_ZDOTDIR") == null); } test "zsh: ZDOTDIR" { const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var res: TmpResourcesDir = try .init(testing.allocator, .zsh); + defer res.deinit(); + 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").?); + const command = try setupZsh(alloc, .{ .shell = "zsh" }, res.path, &env); + try testing.expectEqualStrings("zsh", command.?.shell); + try testing.expectEqualStrings(res.shell_path, env.get("ZDOTDIR").?); try testing.expectEqualStrings("$HOME/.config/zsh", env.get("GHOSTTY_ZSH_ZDOTDIR").?); } + +test "zsh: missing resources" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const resources_dir = try tmp_dir.dir.realpathAlloc(alloc, "."); + defer alloc.free(resources_dir); + + var env = EnvMap.init(alloc); + defer env.deinit(); + + try testing.expect(try setupZsh(alloc, .{ .shell = "zsh" }, resources_dir, &env) == null); + try testing.expectEqual(0, env.count()); +} + +/// Test helper that creates a temporary resources directory with shell integration paths. +const TmpResourcesDir = struct { + allocator: Allocator, + tmp_dir: std.testing.TmpDir, + path: []const u8, + shell_path: []const u8, + + fn init(allocator: Allocator, shell: Shell) !TmpResourcesDir { + var tmp_dir = std.testing.tmpDir(.{}); + errdefer tmp_dir.cleanup(); + + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const relative_shell_path = try std.fmt.bufPrint( + &path_buf, + "shell-integration/{s}", + .{@tagName(shell)}, + ); + try tmp_dir.dir.makePath(relative_shell_path); + + const path = try tmp_dir.dir.realpathAlloc(allocator, "."); + errdefer allocator.free(path); + + const shell_path = try std.fmt.allocPrint( + allocator, + "{s}/{s}", + .{ path, relative_shell_path }, + ); + errdefer allocator.free(shell_path); + + switch (shell) { + .bash => try tmp_dir.dir.writeFile(.{ + .sub_path = "shell-integration/bash/ghostty.bash", + .data = "", + }), + else => {}, + } + + return .{ + .allocator = allocator, + .tmp_dir = tmp_dir, + .path = path, + .shell_path = shell_path, + }; + } + + fn deinit(self: *TmpResourcesDir) void { + self.allocator.free(self.shell_path); + self.allocator.free(self.path); + self.tmp_dir.cleanup(); + } +}; diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 6e125e100..63094b106 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -70,6 +70,9 @@ pub const StreamHandler = struct { /// such as XTGETTCAP. dcs: terminal.dcs.Handler = .{}, + /// The tmux control mode viewer state. + tmux_viewer: if (tmux_enabled) ?*terminal.tmux.Viewer else void = if (tmux_enabled) null else {}, + /// This is set to true when a message was written to the termio /// mailbox. This can be used by callers to determine if they need /// to wake up the termio thread. @@ -81,9 +84,18 @@ pub const StreamHandler = struct { pub const Stream = terminal.Stream(StreamHandler); + /// True if we have tmux control mode built in. + pub const tmux_enabled = terminal.options.tmux_control_mode; + pub fn deinit(self: *StreamHandler) void { self.apc.deinit(); self.dcs.deinit(); + if (comptime tmux_enabled) tmux: { + const viewer = self.tmux_viewer orelse break :tmux; + viewer.deinit(); + self.alloc.destroy(viewer); + self.tmux_viewer = null; + } } /// This queues a render operation with the renderer thread. The render @@ -107,7 +119,7 @@ pub const StreamHandler = struct { }; // The config could have changed any of our colors so update mode 2031 - self.surfaceMessageWriter(.{ .report_color_scheme = false }); + self.messageWriter(.{ .color_scheme_report = .{ .force = false } }); } inline fn surfaceMessageWriter( @@ -186,8 +198,8 @@ pub const StreamHandler = struct { .print_repeat => try self.terminal.printRepeat(value), .bell => self.bell(), .backspace => self.terminal.backspace(), - .horizontal_tab => try self.horizontalTab(value), - .horizontal_tab_back => try self.horizontalTabBack(value), + .horizontal_tab => self.horizontalTab(value), + .horizontal_tab_back => self.horizontalTabBack(value), .linefeed => { @branchHint(.likely); try self.linefeed(); @@ -234,7 +246,7 @@ pub const StreamHandler = struct { .insert_lines => self.terminal.insertLines(value), .insert_blanks => self.terminal.insertBlanks(value), .delete_lines => self.terminal.deleteLines(value), - .scroll_up => self.terminal.scrollUp(value), + .scroll_up => try self.terminal.scrollUp(value), .scroll_down => self.terminal.scrollDown(value), .tab_clear_current => self.terminal.tabClear(.current), .tab_clear_all => self.terminal.tabClear(.all), @@ -299,8 +311,6 @@ pub const StreamHandler = struct { }, .kitty_color_report => try self.kittyColorReport(value), .color_operation => try self.colorOperation(value.op, &value.requests, value.terminator), - .prompt_end => try self.promptEnd(), - .end_of_input => try self.endOfInput(), .end_hyperlink => try self.endHyperlink(), .active_status_display => self.terminal.status_display = value, .decaln => try self.decaln(), @@ -310,9 +320,7 @@ pub const StreamHandler = struct { .progress_report => self.progressReport(value), .start_hyperlink => try self.startHyperlink(value.uri, value.id), .clipboard_contents => try self.clipboardContents(value.kind, value.data), - .prompt_start => self.promptStart(value.aid, value.redraw), - .prompt_continuation => self.promptContinuation(value.aid), - .end_of_command => self.endOfCommand(value.exit_code), + .semantic_prompt => self.semanticPrompt(value), .mouse_shape => try self.setMouseShape(value), .configure_charset => self.configureCharset(value.slot, value.charset), .set_attribute => { @@ -368,9 +376,78 @@ pub const StreamHandler = struct { fn dcsCommand(self: *StreamHandler, cmd: *terminal.dcs.Command) !void { // log.warn("DCS command: {}", .{cmd}); switch (cmd.*) { - .tmux => |tmux| { - // TODO: process it - log.warn("tmux control mode event unimplemented cmd={}", .{tmux}); + .tmux => |tmux| tmux: { + // If tmux control mode is disabled at the build level, + // then this whole block shouldn't be analyzed. + if (comptime !tmux_enabled) break :tmux; + log.info("tmux control mode event cmd={f}", .{tmux}); + + switch (tmux) { + .enter => { + // Setup our viewer state + assert(self.tmux_viewer == null); + const viewer = try self.alloc.create(terminal.tmux.Viewer); + errdefer self.alloc.destroy(viewer); + viewer.* = try .init(self.alloc); + errdefer viewer.deinit(); + self.tmux_viewer = viewer; + break :tmux; + }, + + .exit => { + // Free our viewer state if we have one + if (self.tmux_viewer) |viewer| { + viewer.deinit(); + self.alloc.destroy(viewer); + self.tmux_viewer = null; + } + + // And always break since we assert below + // that we're not handling an exit command. + break :tmux; + }, + + else => {}, + } + + assert(tmux != .enter); + assert(tmux != .exit); + + const viewer = self.tmux_viewer orelse { + // This can only really happen if we failed to + // initialize the viewer on enter. + log.info( + "received tmux control mode command without viewer: {f}", + .{tmux}, + ); + + break :tmux; + }; + + for (viewer.next(.{ .tmux = tmux })) |action| { + log.info("tmux viewer action={f}", .{action}); + switch (action) { + .exit => { + // We ignore this because we will fully exit when + // our DCS connection ends. We may want to handle + // this in the future to notify our GUI we're + // disconnected though. + }, + + .command => |command| { + assert(command.len > 0); + assert(command[command.len - 1] == '\n'); + self.messageWriter(try termio.Message.writeReq( + self.alloc, + command, + )); + }, + + .windows => { + // TODO + }, + } + } }, .xtgettcap => |*gettcap| { @@ -479,18 +556,18 @@ pub const StreamHandler = struct { self.surfaceMessageWriter(.ring_bell); } - inline fn horizontalTab(self: *StreamHandler, count: u16) !void { + inline fn horizontalTab(self: *StreamHandler, count: u16) void { for (0..count) |_| { const x = self.terminal.screens.active.cursor.x; - try self.terminal.horizontalTab(); + self.terminal.horizontalTab(); if (x == self.terminal.screens.active.cursor.x) break; } } - inline fn horizontalTabBack(self: *StreamHandler, count: u16) !void { + inline fn horizontalTabBack(self: *StreamHandler, count: u16) void { for (0..count) |_| { const x = self.terminal.screens.active.cursor.x; - try self.terminal.horizontalTabBack(); + self.terminal.horizontalTabBack(); if (x == self.terminal.screens.active.cursor.x) break; } } @@ -640,7 +717,7 @@ pub const StreamHandler = struct { if (enabled) { self.terminal.saveCursor(); } else { - try self.terminal.restoreCursor(); + self.terminal.restoreCursor(); } }, @@ -790,7 +867,7 @@ pub const StreamHandler = struct { self.messageWriter(msg); }, - .color_scheme => self.surfaceMessageWriter(.{ .report_color_scheme = true }), + .color_scheme => self.messageWriter(.{ .color_scheme_report = .{ .force = true } }), } } @@ -852,7 +929,7 @@ pub const StreamHandler = struct { } pub inline fn restoreCursor(self: *StreamHandler) !void { - try self.terminal.restoreCursor(); + self.terminal.restoreCursor(); } pub fn enquiry(self: *StreamHandler) !void { @@ -875,7 +952,10 @@ pub const StreamHandler = struct { try self.setMouseShape(.text); // Reset resets our palette so we report it for mode 2031. - self.surfaceMessageWriter(.{ .report_color_scheme = false }); + self.messageWriter(.{ .color_scheme_report = .{ .force = false } }); + + // Clear the progress bar + self.progressReport(.{ .state = .remove }); } pub fn queryKittyKeyboard(self: *StreamHandler) !void { @@ -986,28 +1066,53 @@ pub const StreamHandler = struct { }); } - inline fn promptStart(self: *StreamHandler, aid: ?[]const u8, redraw: bool) void { - _ = aid; - self.terminal.markSemanticPrompt(.prompt); - self.terminal.flags.shell_redraws_prompt = redraw; - } + fn semanticPrompt( + self: *StreamHandler, + cmd: Stream.Action.SemanticPrompt, + ) void { + switch (cmd.action) { + .fresh_line_new_prompt => { + const kind = cmd.readOption(.prompt_kind) orelse .initial; + switch (kind) { + .initial, .right => { + self.terminal.markSemanticPrompt(.prompt); + if (cmd.readOption(.redraw)) |redraw| { + self.terminal.flags.shell_redraws_prompt = redraw; + } + }, + .continuation, .secondary => { + self.terminal.markSemanticPrompt(.prompt_continuation); + }, + } + }, - inline fn promptContinuation(self: *StreamHandler, aid: ?[]const u8) void { - _ = aid; - self.terminal.markSemanticPrompt(.prompt_continuation); - } + .end_prompt_start_input => self.terminal.markSemanticPrompt(.input), + .end_input_start_output => { + self.terminal.markSemanticPrompt(.command); + self.surfaceMessageWriter(.start_command); + }, + .end_command => { + // The specification seems to not specify the type but + // other terminals accept 32-bits, but exit codes are really + // bytes, so we just do our best here. + const code: u8 = code: { + const raw: i32 = cmd.readOption(.exit_code) orelse 0; + break :code std.math.cast(u8, raw) orelse 1; + }; - pub inline fn promptEnd(self: *StreamHandler) !void { - self.terminal.markSemanticPrompt(.input); - } + self.surfaceMessageWriter(.{ .stop_command = code }); + }, - pub inline fn endOfInput(self: *StreamHandler) !void { - self.terminal.markSemanticPrompt(.command); - self.surfaceMessageWriter(.start_command); - } - - inline fn endOfCommand(self: *StreamHandler, exit_code: ?u8) void { - self.surfaceMessageWriter(.{ .stop_command = exit_code }); + // All of these commands weren't previously handled by our + // semantic prompt code. I am PR-ing the parser separate from the + // handling so we just ignore these like we did before, even + // though we should handle them eventually. + .end_prompt_start_input_terminate_eol, + .fresh_line, + .new_command, + .prompt_start, + => {}, + } } fn reportPwd(self: *StreamHandler, url: []const u8) !void { diff --git a/src/tripwire.zig b/src/tripwire.zig new file mode 100644 index 000000000..225674b33 --- /dev/null +++ b/src/tripwire.zig @@ -0,0 +1,288 @@ +//! A library for injecting failures into Zig code for the express +//! purpose of testing error handling paths. +//! +//! Improper `errdefer` is one of the highest sources of bugs in Zig code. +//! Many `errdefer` points are hard to exercise in unit tests and rare +//! to encounter in production, so they often hide bugs. Worse, error +//! scenarios are most likely to put your code in an unexpected state +//! that can result in future assertion failures or memory safety issues. +//! +//! This module aims to solve this problem by providing a way to inject +//! errors at specific points in your code during unit tests, allowing you +//! to test every possible error path. +//! +//! # Usage +//! +//! To use this package, create a `tripwire.module` for each failable +//! function you want to test. The enum must be hand-curated to be the +//! set of fail points, and the error set comes directly from the function +//! itself. +//! +//! Pepper your function with `try tw.check` calls wherever you want to +//! have a testable failure point. You don't need every "try" to have +//! an associated tripwire check, only the ones you care about testing. +//! Usually, this is going to be the points where you want to test +//! errdefer logic above it. +//! +//! In unit tests, add `try tw.errorAlways` or related calls to +//! configure expected failures. Then, call your function. Finally, always +//! call `try tw.end(.reset)` to verify your expectations were met and +//! to reset the tripwire module for future tests. +//! +//! ``` +//! const tw = tripwire.module(enum { alloc_buf, open_file }, myFunction); +//! +//! fn myFunction() tw.Error!void { +//! try tw.check(.alloc_buf); +//! const buf = try allocator.alloc(u8, 1024); +//! errdefer allocator.free(buf); +//! +//! try tw.check(.open_file); +//! const file = try std.fs.cwd().openFile("foo.txt", .{}); +//! // ... +//! } +//! +//! test "myFunction fails on alloc" { +//! tw.errorAlways(.alloc_buf, error.OutOfMemory); +//! try std.testing.expectError(error.OutOfMemory, myFunction()); +//! try tw.end(.reset); +//! } +//! ``` +//! +//! ## Transitive Function Calls +//! +//! To test transitive calls, there are two schools of thought: +//! +//! 1. Put a failure point above the transitive call in the caller +//! and assume the child function error handling works correctly. +//! +//! 2. Create another tripwire module for the child function and +//! trigger failures there. This is recommended if the child function +//! can't really be called in isolation (e.g. its an auxiliary function +//! to a public API). +//! +//! Either works, its situationally dependent which is better. + +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const testing = std.testing; + +const log = std.log.scoped(.tripwire); + +// Future ideas: +// +// - Assert that the errors are actually tripped. e.g. you set a +// errorAlways on a point, and want to verify it was tripped. +// - Assert that every point is covered by at least one test. We +// can probably use global state for this. +// - Error only after a certain number of calls to a point. +// - Error only on a range of calls (first 5 times, 2-7th time, etc.) +// +// I don't want to implement these until they're actually needed by +// some part of our codebase, but I want to list them here in case they +// do become useful. + +/// A tripwire module that can be used to inject failures at specific points. +/// +/// Outside of unit tests, this module is free and completely optimized away. +/// It takes up zero binary or runtime space and all function calls are +/// optimized out. +/// +/// To use this module, add `check` (or related) calls prior to every +/// `try` operation that you want to be able to fail arbitrarily. Then, +/// in your unit tests, call the `error` family of functions to configure +/// when errors should be injected. +/// +/// P is an enum type representing the failure points in the module. +/// E is the error set of possible errors that can be returned from the +/// failure points. You can use `anyerror` here but note you may have to +/// use `checkConstrained` to narrow down the error type when you call +/// it in your function (so your function can compile). +/// +/// E may also be an error union type, in which case the error set of that +/// union is used as the error set for the tripwire module. +/// If E is a function, then the error set of the return value of that +/// function is used as the error set for the tripwire module. +pub fn module( + comptime P: type, + comptime E: anytype, +) type { + return struct { + /// The points this module can fail at. + pub const FailPoint = P; + + /// The error set used for failures at the failure points. + pub const Error = err: { + const T = if (@TypeOf(E) == type) E else @TypeOf(E); + break :err switch (@typeInfo(T)) { + .error_set => E, + .error_union => |info| info.error_set, + .@"fn" => |info| @typeInfo(info.return_type.?).error_union.error_set, + else => @compileError("E must be an error set or function type"), + }; + }; + + /// Whether our module is enabled or not. In the future we may + /// want to make this a comptime parameter to the module. + pub const enabled = builtin.is_test; + + comptime { + assert(@typeInfo(FailPoint) == .@"enum"); + assert(@typeInfo(Error) == .error_set); + } + + /// The configured tripwires for this module. + var tripwires: TripwireMap = .{}; + const TripwireMap = std.EnumMap(FailPoint, Tripwire); + const Tripwire = struct { + /// Error to return when tripped + err: Error, + + /// The amount of times this tripwire has been reached. This + /// is NOT the number of times it has tripped, since we may + /// have mins for that. + reached: usize = 0, + + /// The minimum number of times this must be reached before + /// tripping. After this point, it trips every time. This is + /// a "before" check so if this is "1" then it'll trip the + /// second time it's reached. + min: usize = 0, + + /// True if this has been tripped at least once. + tripped: bool = false, + }; + + /// Check for a failure at the given failure point. These should + /// be placed directly before the `try` operation that may fail. + pub fn check(point: FailPoint) callconv(callingConvention()) Error!void { + if (comptime !enabled) return; + return checkConstrained(point, Error); + } + + /// Same as check but allows specifying a custom error type for the + /// return value. This must be a subset of the module's Error type + /// and will produce a runtime error if the configured tripwire + /// error can't be cast to the ConstrainedError type. + pub fn checkConstrained( + point: FailPoint, + comptime ConstrainedError: type, + ) callconv(callingConvention()) ConstrainedError!void { + if (comptime !enabled) return; + const tripwire = tripwires.getPtr(point) orelse return; + tripwire.reached += 1; + if (tripwire.reached <= tripwire.min) return; + tripwire.tripped = true; + return tripwire.err; + } + + /// Mark a failure point to always trip with the given error. + pub fn errorAlways(point: FailPoint, err: Error) void { + errorAfter(point, err, 0); + } + + /// Mark a failure point to trip with the given error after + /// the failure point is reached at least `min` times. A value of + /// zero is equivalent to `errorAlways`. + pub fn errorAfter(point: FailPoint, err: Error, min: usize) void { + tripwires.put(point, .{ .err = err, .min = min }); + } + + /// Ends the tripwire session. This will raise an error if there + /// were untripped error expectations. The reset mode specifies + /// whether expectations are reset too. Expectations are always reset, + /// even if this returns an error. + pub fn end(reset_mode: enum { reset, retain }) error{UntrippedError}!void { + var untripped: bool = false; + var iter = tripwires.iterator(); + while (iter.next()) |entry| { + if (!entry.value.tripped) { + log.warn("untripped point={s}", .{@tagName(entry.key)}); + untripped = true; + } + } + + switch (reset_mode) { + .reset => reset(), + .retain => {}, + } + + if (untripped) return error.UntrippedError; + } + + /// Unset all the tripwires. You should usually call `end` instead. + pub fn reset() void { + tripwires = .{}; + } + + /// Our calling convention is inline if our tripwire module is + /// NOT enabled, so that all calls to `check` are optimized away. + fn callingConvention() std.builtin.CallingConvention { + return if (!enabled) .@"inline" else .auto; + } + }; +} + +test { + const io = module(enum { + read, + write, + }, anyerror); + + // Reset should work + try io.end(.reset); + + // By default, its pass-through + try io.check(.read); + + // Always trip + io.errorAlways(.read, error.OutOfMemory); + try testing.expectError( + error.OutOfMemory, + io.check(.read), + ); + // Happens again + try testing.expectError( + error.OutOfMemory, + io.check(.read), + ); + try io.end(.reset); +} + +test "module as error set" { + const io = module(enum { read, write }, @TypeOf((struct { + fn func() error{ Foo, Bar }!void { + return error.Foo; + } + }).func)); + try io.end(.reset); +} + +test "errorAfter" { + const io = module(enum { read, write }, anyerror); + // Trip after 2 calls (on the 3rd call) + io.errorAfter(.read, error.OutOfMemory, 2); + + // First two calls succeed + try io.check(.read); + try io.check(.read); + + // Third call and on trips + try testing.expectError(error.OutOfMemory, io.check(.read)); + try testing.expectError(error.OutOfMemory, io.check(.read)); + + try io.end(.reset); +} + +test "errorAfter untripped error if min not reached" { + const io = module(enum { read }, anyerror); + io.errorAfter(.read, error.OutOfMemory, 2); + // Only call once, not enough to trip + try io.check(.read); + // end should fail because tripwire was set but never tripped + try testing.expectError( + error.UntrippedError, + io.end(.reset), + ); +} diff --git a/src/unicode/props_uucode.zig b/src/unicode/props_uucode.zig index cade78f8c..527a757ed 100644 --- a/src/unicode/props_uucode.zig +++ b/src/unicode/props_uucode.zig @@ -50,7 +50,10 @@ pub fn main() !void { var buf: [4096]u8 = undefined; var stdout = std.fs.File.stdout().writer(&buf); try t.writeZig(&stdout.interface); - try stdout.end(); + // Use flush instead of end because stdout is a pipe when captured by + // the build system, and pipes cannot be truncated (Windows returns + // INVALID_PARAMETER, Linux returns EINVAL). + try stdout.interface.flush(); // Uncomment when manually debugging to see our table sizes. // std.log.warn("stage1={} stage2={} stage3={}", .{ diff --git a/src/unicode/symbols_uucode.zig b/src/unicode/symbols_uucode.zig index 8cbd59211..794ca5bab 100644 --- a/src/unicode/symbols_uucode.zig +++ b/src/unicode/symbols_uucode.zig @@ -34,7 +34,10 @@ pub fn main() !void { var buf: [4096]u8 = undefined; var stdout = std.fs.File.stdout().writer(&buf); try t.writeZig(&stdout.interface); - try stdout.end(); + // Use flush instead of end because stdout is a pipe when captured by + // the build system, and pipes cannot be truncated (Windows returns + // INVALID_PARAMETER, Linux returns EINVAL). + try stdout.interface.flush(); // Uncomment when manually debugging to see our table sizes. // std.log.warn("stage1={} stage2={} stage3={}", .{ diff --git a/typos.toml b/typos.toml index 26876aef9..8eb8d9937 100644 --- a/typos.toml +++ b/typos.toml @@ -19,6 +19,7 @@ extend-exclude = [ # Fonts "*.ttf", "*.otf", + "*.bdf", # Images "*.png", "*.ico", @@ -56,6 +57,8 @@ DECID = "DECID" flate = "flate" typ = "typ" kend = "kend" +# Tai Tham is a script/writing system +Tham = "Tham" # GTK GIR = "GIR" # terminfo diff --git a/valgrind.supp b/valgrind.supp index eeb395d03..27479fd5c 100644 --- a/valgrind.supp +++ b/valgrind.supp @@ -72,7 +72,16 @@ fun:gdk_surface_handle_event ... } - +{ + GTK CSS Node Validation + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + ... + fun:gtk_css_node_validate_internal + fun:gtk_css_node_validate + ... +} { GTK CSS Provider Leak Memcheck:Leak @@ -196,8 +205,44 @@ fun:svga_context_flush ... } - { + SVGA Stuff + Memcheck:Leak + match-leak-kinds: definite + fun:calloc + fun:svga_create_surface_view + fun:svga_set_framebuffer_state + fun:st_update_framebuffer_state + fun:st_Clear + fun:gsk_gpu_render_pass_op_gl_command + ... +} +{ + GTK Icon + Memcheck:Leak + match-leak-kinds: possible + fun:*alloc + ... + fun:gtk_icon_theme_set_display + fun:gtk_icon_theme_get_for_display + ... +} +{ + GDK Wayland Connection + Memcheck:Leak + match-leak-kinds: possible + fun:calloc + fun:wl_closure_init + fun:wl_connection_demarshal + fun:wl_display_read_events + fun:gdk_wayland_poll_source_check + fun:g_main_context_check_unlocked + fun:g_main_context_iterate_unlocked.isra.0 + fun:g_main_context_iteration + ... +} +{ + GSK Renderer GPU Stuff Memcheck:Leak match-leak-kinds: possible @@ -297,6 +342,21 @@ fun:g_main_context_iteration ... } +{ + GSK More Forms + Memcheck:Leak + match-leak-kinds: possible + ... + fun:gsk_gl_device_use_program + fun:gsk_gl_frame_use_program + fun:gsk_gpu_shader_op_gl_command_n + fun:gsk_gpu_render_pass_op_gl_command + fun:gsk_gl_frame_submit + fun:gsk_gpu_renderer_render_texture + fun:gsk_renderer_render_texture + fun:render_contents + ... +} { GTK Shader Selector Memcheck:Leak