diff --git a/.gitattributes b/.gitattributes index 6bf5ceb13..87f1eb32e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,4 +9,6 @@ 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/terminal/res/** linguist-vendored diff --git a/.github/workflows/milestone.yml b/.github/workflows/milestone.yml index d12418d9c..25d8edaa0 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@b57a7e52e9913b6b0cdefb10add762af0398659d # v2.9 + uses: hustcer/milestone-action@bff2091b54a91cf1491564659c554742b285442f # v2.11 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@b57a7e52e9913b6b0cdefb10add762af0398659d # v2.9 + uses: hustcer/milestone-action@bff2091b54a91cf1491564659c554742b285442f # v2.11 if: github.event.issue.state == 'closed' with: action: bind-issue diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 09ec4aeed..da669b073 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -36,13 +36,13 @@ jobs: - name: Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig - name: Setup Nix - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/publish-tag.yml b/.github/workflows/publish-tag.yml index 710d04647..c433e7484 100644 --- a/.github/workflows/publish-tag.yml +++ b/.github/workflows/publish-tag.yml @@ -28,7 +28,7 @@ jobs: echo "Version is valid: ${{ github.event.inputs.version }}" - - name: Exract the Version + - name: Extract the Version id: extract_version run: | VERSION=${{ github.event.inputs.version }} diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 66dfe5fc2..e5af4ac38 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -83,13 +83,13 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 853378d43..2331dbba9 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -19,7 +19,6 @@ jobs: if: | github.event_name == 'workflow_dispatch' || ( - github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' ) @@ -34,7 +33,7 @@ jobs: with: # Important so that build number generation works fetch-depth: 0 - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -151,7 +150,6 @@ jobs: ( github.event_name == 'workflow_dispatch' || ( - github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' ) @@ -163,12 +161,12 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -188,7 +186,7 @@ jobs: nix develop -c minisign -S -m ghostty-source.tar.gz -s minisign.key < minisign.password - name: Update Release - uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 + uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -206,7 +204,6 @@ jobs: ( github.event_name == 'workflow_dispatch' || ( - github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' ) @@ -359,7 +356,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 + uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -373,7 +370,6 @@ jobs: # Create our appcast for Sparkle - name: Generate Appcast if: | - github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' env: @@ -408,7 +404,6 @@ jobs: # gets out of sync with the binaries. - name: Prep R2 Storage for Appcast if: | - github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' run: | @@ -418,7 +413,6 @@ jobs: - name: Upload Appcast to R2 if: | - github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4 @@ -444,7 +438,6 @@ jobs: ( github.event_name == 'workflow_dispatch' || ( - github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' ) @@ -590,7 +583,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 + uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -629,7 +622,6 @@ jobs: ( github.event_name == 'workflow_dispatch' || ( - github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' ) @@ -775,7 +767,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 + uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 with: name: 'Ghostty Tip ("Nightly")' prerelease: true diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index 6feb39887..cfe5591f4 100644 --- a/.github/workflows/snap.yml +++ b/.github/workflows/snap.yml @@ -38,7 +38,7 @@ jobs: tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9efd257ca..ef03c5f32 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,6 @@ jobs: - build-dist - build-examples - build-flatpak - - build-freebsd - build-libghostty-vt - build-linux - build-linux-libghostty @@ -73,14 +72,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -95,7 +94,7 @@ jobs: strategy: fail-fast: false matrix: - dir: [c-vt, zig-vt] + dir: [c-vt, c-vt-key-encode, c-vt-paste, zig-vt] name: Example ${{ matrix.dir }} runs-on: namespace-profile-ghostty-sm needs: test @@ -107,14 +106,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -140,14 +139,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -174,14 +173,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -205,6 +204,7 @@ jobs: aarch64-linux, x86_64-linux, x86_64-windows, + wasm32-freestanding, ] runs-on: namespace-profile-ghostty-sm needs: test @@ -216,14 +216,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -252,14 +252,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -281,14 +281,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -314,14 +314,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -333,7 +333,7 @@ jobs: run: nix build .#ghostty-releasefast - name: Check version - run: result/bin/ghostty +version | grep -q 'builtin.OptimizeMode.ReleaseFast' + run: result/bin/ghostty +version | grep -q '.ReleaseFast' - name: Check to see if the binary has been stripped run: nm result/bin/.ghostty-wrapped 2>&1 | grep -q 'no symbols' @@ -342,7 +342,7 @@ jobs: run: nix build .#ghostty-debug - name: Check version - run: result/bin/ghostty +version | grep -q 'builtin.OptimizeMode.Debug' + run: result/bin/ghostty +version | grep -q '.Debug' - name: Check to see if the binary has not been stripped run: nm result/bin/.ghostty-wrapped 2>&1 | grep -q 'main_ghostty.main' @@ -360,14 +360,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -509,11 +509,11 @@ jobs: - name: Install zig shell: pwsh run: | - # Get the zig version from build.zig so that it only needs to be updated - $fileContent = Get-Content -Path "build.zig" -Raw - $pattern = 'buildpkg\.requireZig\("(.*?)"\);' + # Get the zig version from build.zig.zon so that it only needs to be updated + $fileContent = Get-Content -Path "build.zig.zon" -Raw + $pattern = 'minimum_zig_version\s*=\s*"([^"]+)"' $zigVersion = [regex]::Match($fileContent, $pattern).Groups[1].Value - $version = "zig-windows-x86_64-$zigVersion" + $version = "zig-x86_64-windows-$zigVersion" Write-Output $version $uri = "https://ziglang.org/download/$zigVersion/$version.zip" Invoke-WebRequest -Uri "$uri" -OutFile ".\zig-windows.zip" @@ -564,6 +564,8 @@ jobs: test: if: github.repository == 'ghostty-org/ghostty' runs-on: namespace-profile-ghostty-md + outputs: + zig_version: ${{ steps.zig.outputs.version }} env: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache @@ -571,15 +573,20 @@ jobs: - name: Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: Get required Zig version + id: zig + run: | + echo "version=$(sed -n -E 's/^\s*\.?minimum_zig_version\s*=\s*"([^"]+)".*/\1/p' build.zig.zon)" >> $GITHUB_OUTPUT + - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -614,14 +621,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -662,14 +669,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -697,14 +704,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -761,14 +768,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -790,12 +797,12 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -818,12 +825,12 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -845,12 +852,12 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -872,12 +879,12 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -899,12 +906,12 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -926,12 +933,12 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -960,12 +967,12 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -987,12 +994,12 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -1022,14 +1029,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -1051,7 +1058,7 @@ jobs: uses: namespacelabs/nscloud-setup@d1c625762f7c926a54bd39252efff0705fd11c64 # v0.0.10 - name: Configure Namespace powered Buildx - uses: namespacelabs/nscloud-setup-buildx-action@01628ae51ea5d6b0c90109c7dccbf511953aff29 # v0.0.18 + uses: namespacelabs/nscloud-setup-buildx-action@91c2e6537780e3b092cb8476406be99a8f91bd5e # v0.0.20 - name: Download Source Tarball Artifacts uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 @@ -1089,7 +1096,7 @@ jobs: needs: test steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - uses: flatpak/flatpak-github-actions/flatpak-builder@10a3c29f0162516f0f68006be14c92f34bd4fa6c # v6.5 + - uses: flatpak/flatpak-github-actions/flatpak-builder@92ae9851ad316786193b1fd3f40c4b51eb5cb101 # v6.6 with: bundle: com.mitchellh.ghostty manifest-path: flatpak/com.mitchellh.ghostty.yml @@ -1110,14 +1117,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + - uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -1142,7 +1149,8 @@ jobs: matrix: release: - "14.3" - # - "15.0" # disable until fixed: https://github.com/vmactions/freebsd-vm/issues/108 + - "15.0" + timeout-minutes: 10 steps: - name: Checkout Ghostty uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -1163,14 +1171,19 @@ jobs: devel/gettext \ devel/git \ devel/pkgconf \ + ftp/curl \ graphics/wayland \ - lang/zig \ security/ca_root_nss \ textproc/hs-pandoc \ x11-fonts/jetbrains-mono \ x11-toolkits/libadwaita \ x11-toolkits/gtk40 \ x11-toolkits/gtk4-layer-shell + curl -L -o /tmp/zig.tar.xz "https://ziglang.org/download/${{ needs.test.outputs.zig_version }}/zig-x86_64-freebsd-${{ needs.test.outputs.zig_version }}.tar.xz" && \ + mkdir /opt && \ + tar -xf /tmp/zig.tar.xz -C /opt && \ + rm /tmp/zig.tar.xz && \ + ln -s "/opt/zig-x86_64-freebsd-${{ needs.test.outputs.zig_version }}/zig" /usr/local/bin/zig run: | zig env diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index 3f0d1d1e2..c14ee56a6 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -22,14 +22,14 @@ jobs: fetch-depth: 0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 + uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19 with: path: | /nix /zig - name: Setup Nix - uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 + uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/AGENTS.md b/AGENTS.md index 2e90fd94e..5a885923e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,11 +13,20 @@ A file for [guiding coding agents](https://agents.md/). ## Directory Structure - Shared Zig core: `src/` -- C API: `include/ghostty.h` +- C API: `include` - macOS app: `macos/` - GTK (Linux and FreeBSD) app: `src/apprt/gtk` +## libghostty-vt + +- Build: `zig build lib-vt` +- Test: `zig build test-lib-vt` +- Test filter: `zig build test-lib-vt -Dtest-filter=` +- When working on libghostty-vt, do not build the full app. +- For C only changes, don't run the Zig tests. Build all the examples. + ## macOS App - Do not use `xcodebuild` - Use `zig build` to build the macOS app and any shared Zig code +- Run Xcode tests using `zig build test` diff --git a/CODEOWNERS b/CODEOWNERS index c23c8767b..64dc923e9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -184,6 +184,7 @@ /po/ko_KR.UTF-8.po @ghostty-org/ko_KR /po/he_IL.UTF-8.po @ghostty-org/he_IL /po/it_IT.UTF-8.po @ghostty-org/it_IT +/po/lt_LT.UTF-8.po @ghostty-org/lt_LT /po/zh_TW.UTF-8.po @ghostty-org/zh_TW /po/hr_HR.UTF-8.po @ghostty-org/hr_HR /po/vi_VN.UTF-8.po @ghostty-org/vi_VN diff --git a/Doxyfile b/Doxyfile index fccd4a493..63e73334d 100644 --- a/Doxyfile +++ b/Doxyfile @@ -2,9 +2,42 @@ DOXYFILE_ENCODING = UTF-8 PROJECT_NAME = "libghostty" -INPUT = include/ghostty/vt.h +PROJECT_LOGO = images/gnome/64.png +INPUT = include/ghostty INPUT_ENCODING = UTF-8 -RECURSIVE = NO +RECURSIVE = YES +FILE_PATTERNS = *.h +EXAMPLE_PATH = example +EXAMPLE_RECURSIVE = YES +EXAMPLE_PATTERNS = * +FULL_PATH_NAMES = NO +STRIP_FROM_INC_PATH = include +SOURCE_BROWSER = YES +INLINE_SOURCES = NO +REFERENCES_RELATION = YES +REFERENCED_BY_RELATION = YES + +#--------------------------------------------------------------------------- +# C API Optimization +#--------------------------------------------------------------------------- + +# Optimize output for C API documentation +OPTIMIZE_OUTPUT_FOR_C = YES +TYPEDEF_HIDES_STRUCT = YES +HIDE_SCOPE_NAMES = YES + +# Clean path names +FULL_PATH_NAMES = NO +STRIP_FROM_PATH = . +STRIP_FROM_INC_PATH = include + +# Hide undocumented and internal APIs +HIDE_UNDOC_MEMBERS = YES +HIDE_UNDOC_CLASSES = YES +EXTRACT_ALL = NO +INTERNAL_DOCS = NO +EXTRACT_PRIVATE = NO +EXTRACT_LOCAL_CLASSES = NO #--------------------------------------------------------------------------- # HTML Output @@ -12,6 +45,26 @@ RECURSIVE = NO GENERATE_HTML = YES HTML_OUTPUT = zig-out/share/ghostty/doc/libghostty +HTML_EXTRA_STYLESHEET = dist/doxygen/ghostty.css +HTML_EXTRA_FILES = dist/doxygen/favicon.png \ + dist/doxygen/mobile-nav.js +HTML_COLORSTYLE = DARK +HTML_CODE_FOLDING = NO +HTML_HEADER = dist/doxygen/header.html +LAYOUT_FILE = DoxygenLayout.xml +GENERATE_TREEVIEW = YES +HTML_DYNAMIC_SECTIONS = YES +SEARCHENGINE = YES +ALPHABETICAL_INDEX = YES +HTML_TIMESTAMP = NO +DISABLE_INDEX = NO +FULL_SIDEBAR = NO + +#--------------------------------------------------------------------------- +# Graphs and Diagrams +#--------------------------------------------------------------------------- + +HAVE_DOT = NO #--------------------------------------------------------------------------- # Man Output @@ -20,6 +73,7 @@ HTML_OUTPUT = zig-out/share/ghostty/doc/libghostty GENERATE_MAN = YES MAN_OUTPUT = zig-out/share/man MAN_EXTENSION = .3 +MAN_LINKS = YES #--------------------------------------------------------------------------- # Other Output diff --git a/DoxygenLayout.xml b/DoxygenLayout.xml new file mode 100644 index 000000000..ae9c52684 --- /dev/null +++ b/DoxygenLayout.xml @@ -0,0 +1,247 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/HACKING.md b/HACKING.md index 905a244e8..0a4bbef20 100644 --- a/HACKING.md +++ b/HACKING.md @@ -50,24 +50,22 @@ macOS users don't require any additional dependencies. ## Xcode Version and SDKs Building the Ghostty macOS app requires that Xcode, the macOS SDK, -and the iOS SDK are all installed. +the iOS SDK, and Metal Toolchain are all installed. A common issue is that the incorrect version of Xcode is either installed or selected. Use the `xcode-select` command to ensure that the correct version of Xcode is selected: ```shell-session -sudo xcode-select --switch /Applications/Xcode-beta.app +sudo xcode-select --switch /Applications/Xcode.app ``` > [!IMPORTANT] > -> Main branch development of Ghostty is preparing for the next major -> macOS release, Tahoe (macOS 26). Therefore, the main branch requires -> **Xcode 26 and the macOS 26 SDK**. +> Main branch development of Ghostty requires **Xcode 26 and the macOS 26 SDK**. > > You do not need to be running on macOS 26 to build Ghostty, you can -> still use Xcode 26 beta on macOS 15 stable. +> still use Xcode 26 on macOS 15 stable. ## AI and Agents diff --git a/Makefile b/Makefile index ad8379f7e..c5511a62e 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ vendor/glad/include/glad/glad.h: vendor/glad/include/glad/gl.h clean: rm -rf \ - zig-out zig-cache \ + zig-out .zig-cache \ macos/build \ macos/GhosttyKit.xcframework .PHONY: clean diff --git a/README.md b/README.md index df86f7830..7124400fd 100644 --- a/README.md +++ b/README.md @@ -193,4 +193,4 @@ SENTRY_DSN=https://e914ee84fd895c4fe324afa3e53dac76@o4507352570920960.ingest.us. > purposely contain sensitive information, but it does contain the full > stack memory of each thread at the time of the crash. This information > is used to rebuild the stack trace but can also contain sensitive data -> depending when the crash occurred. +> depending on when the crash occurred. diff --git a/build.zig b/build.zig index c6c461b4c..68dc0028b 100644 --- a/build.zig +++ b/build.zig @@ -2,16 +2,19 @@ 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; comptime { - buildpkg.requireZig("0.14.0"); + buildpkg.requireZig(minimumZigVersion); } pub fn build(b: *std.Build) !void { // This defines all the available build options (e.g. `-D`). If you // want to know what options are available, you can run `--help` or // you can read `src/build/Config.zig`. - const config = try buildpkg.Config.init(b); + + const config = try buildpkg.Config.init(b, appVersion); const test_filters = b.option( [][]const u8, "test-filter", @@ -98,10 +101,19 @@ pub fn build(b: *std.Build) !void { ); // libghostty-vt - const libghostty_vt_shared = try buildpkg.GhosttyLibVt.initShared( - b, - &mod, - ); + const libghostty_vt_shared = shared: { + if (config.target.result.cpu.arch.isWasm()) { + break :shared try buildpkg.GhosttyLibVt.initWasm( + b, + &mod, + ); + } + + break :shared try buildpkg.GhosttyLibVt.initShared( + b, + &mod, + ); + }; libghostty_vt_shared.install(libvt_step); libghostty_vt_shared.install(b.getInstallStep()); @@ -245,12 +257,17 @@ pub fn build(b: *std.Build) !void { { const mod_vt_test = b.addTest(.{ .root_module = mod.vt, - .target = config.target, - .optimize = config.optimize, .filters = test_filters, }); const mod_vt_test_run = b.addRunArtifact(mod_vt_test); test_lib_vt_step.dependOn(&mod_vt_test_run.step); + + const mod_vt_c_test = b.addTest(.{ + .root_module = mod.vt_c, + .filters = test_filters, + }); + const mod_vt_c_test_run = b.addRunArtifact(mod_vt_c_test); + test_lib_vt_step.dependOn(&mod_vt_c_test_run.step); } // Tests @@ -267,6 +284,8 @@ pub fn build(b: *std.Build) !void { .omit_frame_pointer = false, .unwind_tables = .sync, }), + // Crash on x86_64 without this + .use_llvm = true, }); if (config.emit_test_exe) b.installArtifact(test_exe); _ = try deps.add(test_exe); @@ -276,7 +295,7 @@ pub fn build(b: *std.Build) !void { test_step.dependOn(&test_run.step); // Normal tests always test our libghostty modules - test_step.dependOn(test_lib_vt_step); + //test_step.dependOn(test_lib_vt_step); // Valgrind test running const valgrind_run = b.addSystemCommand(&.{ diff --git a/build.zig.zon b/build.zig.zon index b297f3bb0..1bd4d6a5b 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,62 +1,63 @@ .{ .name = .ghostty, - .version = "1.2.1", + .version = "1.3.0-dev", .paths = .{""}, .fingerprint = 0x64407a2a0b4147e5, + .minimum_zig_version = "0.15.2", .dependencies = .{ // Zig libs .libxev = .{ // mitchellh/libxev - .url = "https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz", - .hash = "libxev-0.0.0-86vtc2UaEwDfiTKX3iBI-s_hdzfzWQUarT3MUrmUQl-Q", + .url = "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz", + .hash = "libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs", .lazy = true, }, .vaxis = .{ // rockorager/libvaxis - .url = "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23", - .hash = "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn", + .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.8.1.tar.gz", - .hash = "z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP", + .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://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz", - .hash = "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk", + .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-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz", - .hash = "N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ", + .url = "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz", + .hash = "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi", .lazy = true, }, - .ziglyph = .{ - .url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz", - .hash = "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf", - .lazy = true, + .uucode = .{ + // TODO: currently the use-llvm branch because its broken on self-hosted + .url = "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", + .hash = "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland - .url = "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz", - .hash = "wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy", + .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/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz", - .hash = "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9", + .url = "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz", + .hash = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh", .lazy = true, }, .gobject = .{ // https://github.com/jcollie/ghostty-gobject based on zig_gobject // Temporary until we generate them at build time automatically. - .url = "https://github.com/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst", + .url = "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst", .hash = "gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV", .lazy = true, }, @@ -115,8 +116,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz", - .hash = "N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/ghostty-themes.tgz", + .hash = "N-V-__8AAPk1AwDvSIEm8ftVzPLJr-Uuzz65Ss2R4ljTXjSq", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index 702df5026..cf2857147 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -26,7 +26,7 @@ }, "gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV": { "name": "gobject", - "url": "https://github.com/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst", + "url": "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst", "hash": "sha256-SXiqGm81aUn6yq1wFXgNTAULdKOHS/Rzkp5OgNkkcXo=" }, "N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": { @@ -49,10 +49,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3": { + "N-V-__8AAPk1AwDvSIEm8ftVzPLJr-Uuzz65Ss2R4ljTXjSq": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz", - "hash": "sha256-JPY9M50d/n6rGzWt0aQZIU7IBMWru2IAqe9Vu1x5CMw=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/ghostty-themes.tgz", + "hash": "sha256-vFn6MiR8tVkGyjSV7pz4k4msviSCjGHKM2SU84lJp/k=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "name": "jetbrains_mono", @@ -64,10 +64,10 @@ "url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz", "hash": "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo=" }, - "libxev-0.0.0-86vtc2UaEwDfiTKX3iBI-s_hdzfzWQUarT3MUrmUQl-Q": { + "libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs": { "name": "libxev", - "url": "https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz", - "hash": "sha256-KaozYKEhhT/6sInef7/8O/60LDBJN+8QmdLuNY1Gkmc=" + "url": "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz", + "hash": "sha256-YAPqa5bkpRihKPkyMn15oRvTCZaxO3O66ymRY3lIfdc=" }, "N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK": { "name": "libxml2", @@ -109,10 +109,20 @@ "url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz", "hash": "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8=" }, - "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn": { + "uucode-0.1.0-ZZjBPj96QADXyt5sqwBJUnhaDYs_qBeeKijZvlRa0eqM": { + "name": "uucode", + "url": "git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732", + "hash": "sha256-sHPh+TQSdUGus/QTbj7KSJJkTuNTrK4VNmQDjS30Lf8=" + }, + "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT": { + "name": "uucode", + "url": "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", + "hash": "sha256-VomSYOF8fRJwb/8GtVG/QqR6c95zSkQt4649C/4KXAc=" + }, + "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS": { "name": "vaxis", - "url": "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23", - "hash": "sha256-bNZ3oveT6vPChjimPJ/GGfcdivlAeJdl/xfWM+S/MHY=" + "url": "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz", + "hash": "sha256-LnIzK8icW1Qexua9SHaeHz+3V8QAbz0a+UC1T5sIjvY=" }, "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t": { "name": "wayland", @@ -129,45 +139,35 @@ "url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz", "hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM=" }, - "z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP": { + "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T": { "name": "z2d", - "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz", - "hash": "sha256-0DbDKSYA1ejhVx/WbOkwTgD57PNRFcnRviqBh8xpPZ0=" + "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz", + "hash": "sha256-+QqCRoXwrFA1/l+oWvYVyAVebGQitAFQNhi9U3EVrxA=" }, - "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9": { + "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh": { "name": "zf", - "url": "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz", - "hash": "sha256-3nulNQd/4rZ4paeXJYXwAliNNyRNsIOX/q3z1JB8C7I=" + "url": "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz", + "hash": "sha256-OwFdkorwTp4mJyvBXrTbtNmp1GnrbSkKDdrmc7d8RWg=" }, - "zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM": { - "name": "zg", - "url": "git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc", - "hash": "sha256-fo3l6cjkrr/godElTGnQzalBsasN7J73IDIRmw7v1gA=" - }, - "N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ": { + "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi": { "name": "zig_js", - "url": "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz", - "hash": "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0=" + "url": "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz", + "hash": "sha256-TCAY5WAV05UEuAkDhq2c6Tk/ODgAhdnDI3O/flb8c6M=" }, - "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk": { + "zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK": { "name": "zig_objc", - "url": "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz", - "hash": "sha256-o3vl7qfkSi0bKXa6JWuF92qMEGP8Af/shcip5nRo5Nw=" + "url": "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz", + "hash": "sha256-3YSvc3YlNW/NciyzCQnzsujXAmZ89XlxSqfqvArAjsw=" }, - "wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy": { + "wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe": { "name": "zig_wayland", - "url": "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz", - "hash": "sha256-E77GZ15APYbbO1WzmuJi8eG9/iQFbc2CgkNBxjCLUhk=" + "url": "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz", + "hash": "sha256-TxRrc17Q1Sf1IOO/cdPpP3LD0PpYOujt06SFH3B5Ek4=" }, - "zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj": { + "zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms": { "name": "zigimg", - "url": "git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d", - "hash": "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0=" - }, - "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf": { - "name": "ziglyph", - "url": "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz", - "hash": "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k=" + "url": "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz", + "hash": "sha256-LB7Xa6KzVRRUSwwnyWM+y6fDG+kIDjfnoBDJO1obxVM=" }, "N-V-__8AAB0eQwD-0MdOEBmz7intriBReIsIDNlukNVoNu6o": { "name": "zlib", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index c38b34e4d..1ac748b69 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -5,7 +5,7 @@ fetchurl, fetchgit, runCommandLocal, - zig_0_14, + zig_0_15, name ? "zig-packages", }: let unpackZigArtifact = { @@ -14,7 +14,7 @@ }: runCommandLocal name { - nativeBuildInputs = [zig_0_14]; + nativeBuildInputs = [zig_0_15]; } '' hash="$(zig fetch --global-cache-dir "$TMPDIR" ${artifact})" @@ -126,7 +126,7 @@ in name = "gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV"; path = fetchZigArtifact { name = "gobject"; - url = "https://github.com/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst"; + url = "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst"; hash = "sha256-SXiqGm81aUn6yq1wFXgNTAULdKOHS/Rzkp5OgNkkcXo="; }; } @@ -163,11 +163,11 @@ in }; } { - name = "N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3"; + name = "N-V-__8AAPk1AwDvSIEm8ftVzPLJr-Uuzz65Ss2R4ljTXjSq"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz"; - hash = "sha256-JPY9M50d/n6rGzWt0aQZIU7IBMWru2IAqe9Vu1x5CMw="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/ghostty-themes.tgz"; + hash = "sha256-vFn6MiR8tVkGyjSV7pz4k4msviSCjGHKM2SU84lJp/k="; }; } { @@ -187,11 +187,11 @@ in }; } { - name = "libxev-0.0.0-86vtc2UaEwDfiTKX3iBI-s_hdzfzWQUarT3MUrmUQl-Q"; + name = "libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs"; path = fetchZigArtifact { name = "libxev"; - url = "https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz"; - hash = "sha256-KaozYKEhhT/6sInef7/8O/60LDBJN+8QmdLuNY1Gkmc="; + url = "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz"; + hash = "sha256-YAPqa5bkpRihKPkyMn15oRvTCZaxO3O66ymRY3lIfdc="; }; } { @@ -259,11 +259,27 @@ in }; } { - name = "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn"; + name = "uucode-0.1.0-ZZjBPj96QADXyt5sqwBJUnhaDYs_qBeeKijZvlRa0eqM"; + path = fetchZigArtifact { + name = "uucode"; + url = "git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732"; + hash = "sha256-sHPh+TQSdUGus/QTbj7KSJJkTuNTrK4VNmQDjS30Lf8="; + }; + } + { + name = "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT"; + path = fetchZigArtifact { + name = "uucode"; + url = "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz"; + hash = "sha256-VomSYOF8fRJwb/8GtVG/QqR6c95zSkQt4649C/4KXAc="; + }; + } + { + name = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS"; path = fetchZigArtifact { name = "vaxis"; - url = "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23"; - hash = "sha256-bNZ3oveT6vPChjimPJ/GGfcdivlAeJdl/xfWM+S/MHY="; + url = "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz"; + hash = "sha256-LnIzK8icW1Qexua9SHaeHz+3V8QAbz0a+UC1T5sIjvY="; }; } { @@ -291,67 +307,51 @@ in }; } { - name = "z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP"; + name = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T"; path = fetchZigArtifact { name = "z2d"; - url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz"; - hash = "sha256-0DbDKSYA1ejhVx/WbOkwTgD57PNRFcnRviqBh8xpPZ0="; + url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz"; + hash = "sha256-+QqCRoXwrFA1/l+oWvYVyAVebGQitAFQNhi9U3EVrxA="; }; } { - name = "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9"; + name = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh"; path = fetchZigArtifact { name = "zf"; - url = "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz"; - hash = "sha256-3nulNQd/4rZ4paeXJYXwAliNNyRNsIOX/q3z1JB8C7I="; + url = "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz"; + hash = "sha256-OwFdkorwTp4mJyvBXrTbtNmp1GnrbSkKDdrmc7d8RWg="; }; } { - name = "zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM"; - path = fetchZigArtifact { - name = "zg"; - url = "git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc"; - hash = "sha256-fo3l6cjkrr/godElTGnQzalBsasN7J73IDIRmw7v1gA="; - }; - } - { - name = "N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ"; + name = "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi"; path = fetchZigArtifact { name = "zig_js"; - url = "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz"; - hash = "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0="; + url = "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz"; + hash = "sha256-TCAY5WAV05UEuAkDhq2c6Tk/ODgAhdnDI3O/flb8c6M="; }; } { - name = "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk"; + name = "zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK"; path = fetchZigArtifact { name = "zig_objc"; - url = "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz"; - hash = "sha256-o3vl7qfkSi0bKXa6JWuF92qMEGP8Af/shcip5nRo5Nw="; + url = "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz"; + hash = "sha256-3YSvc3YlNW/NciyzCQnzsujXAmZ89XlxSqfqvArAjsw="; }; } { - name = "wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy"; + name = "wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe"; path = fetchZigArtifact { name = "zig_wayland"; - url = "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz"; - hash = "sha256-E77GZ15APYbbO1WzmuJi8eG9/iQFbc2CgkNBxjCLUhk="; + url = "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz"; + hash = "sha256-TxRrc17Q1Sf1IOO/cdPpP3LD0PpYOujt06SFH3B5Ek4="; }; } { - name = "zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj"; + name = "zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms"; path = fetchZigArtifact { name = "zigimg"; - url = "git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d"; - hash = "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0="; - }; - } - { - name = "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf"; - path = fetchZigArtifact { - name = "ziglyph"; - url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz"; - hash = "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k="; + url = "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz"; + hash = "sha256-LB7Xa6KzVRRUSwwnyWM+y6fDG+kIDjfnoBDJO1obxVM="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 1caa7b000..398231198 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -1,7 +1,4 @@ -git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc -git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d -git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23 -https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz +git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732 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 @@ -9,11 +6,13 @@ 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/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz +https://deps.files.ghostty.org/gobject-2025-09-20-20-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 https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806.tar.gz @@ -21,15 +20,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-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.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/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz -https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.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/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst -https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz -https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz -https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz -https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz -https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz +https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz +https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/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 diff --git a/dist/doxygen/favicon.png b/dist/doxygen/favicon.png new file mode 100644 index 000000000..b647bcf35 Binary files /dev/null and b/dist/doxygen/favicon.png differ diff --git a/dist/doxygen/footer.html b/dist/doxygen/footer.html new file mode 100644 index 000000000..fca4b87d9 --- /dev/null +++ b/dist/doxygen/footer.html @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/dist/doxygen/ghostty.css b/dist/doxygen/ghostty.css new file mode 100644 index 000000000..678414b70 --- /dev/null +++ b/dist/doxygen/ghostty.css @@ -0,0 +1,390 @@ +/** + * Ghostty Doxygen Custom Stylesheet + * Minimal branding customizations for Ghostty colors + */ + +/* Ghostty brand color for links and accents - high contrast for dark bg */ +a, +a:link { + color: #99b3ff; +} + +a:visited { + color: #99b3ff; +} + +a:hover { + color: #c2d4ff; +} + +/* High contrast text colors */ +body, +div.contents, +div.header, +.title, +.summary, +td, +th, +p, +li { + color: #e8e8e8 !important; +} + +h1, +h2, +h3, +h4, +h5, +h6, +.groupheader { + color: #ffffff !important; +} + +.memtitle, +.memname { + color: #ffffff !important; +} + +.memdoc { + color: #e8e8e8 !important; +} + +/* Selection color */ +::selection { + background: rgba(53, 81, 243, 0.6); +} + +/* Modern scrollbar styling for WebKit browsers (Safari, Chrome) */ +::-webkit-scrollbar { + width: 14px; + height: 14px; + -webkit-appearance: none; +} + +::-webkit-scrollbar-track { + background: #1a1f2e; + border-radius: 8px; +} + +::-webkit-scrollbar-thumb { + background: #4a5260; + border-radius: 8px; + border: 3px solid #1a1f2e; + min-height: 40px; +} + +::-webkit-scrollbar-thumb:hover { + background: #5a6270; +} + +::-webkit-scrollbar-thumb:active { + background: #6a7280; +} + +::-webkit-scrollbar-corner { + background: #1a1f2e; +} + +/* Firefox scrollbar styling */ +* { + scrollbar-width: thin; + scrollbar-color: #404754 #1a1f2e; +} + +/* Tree view selected item */ +#nav-tree .selected { + background-color: #3551f3 !important; +} + +/* Custom syntax highlighting optimized for dark backgrounds with high contrast */ +.fragment, +div.line { + color: #f0f0f0 !important; +} + +/* Keywords (int, void, const, static, etc.) */ +.keyword, +.keywordtype { + color: #ff8be6 !important; + font-weight: 500; +} + +/* Control flow (if, else, return, for, while, etc.) */ +.keywordflow { + color: #ff8be6 !important; + font-weight: 500; +} + +/* Comments */ +.comment { + color: #8bc34a !important; + font-style: italic; +} + +/* Preprocessor directives (#include, #define, etc.) */ +.preprocessor { + color: #ffcc66 !important; +} + +/* String and character literals */ +.stringliteral, +.charliteral { + color: #b8e986 !important; +} + +/* Numbers */ +span.charliteral { + color: #d4a5ff !important; +} + +/* Function names */ +.functionname { + color: #6fe87c !important; + font-weight: 500; +} + +/* Line numbers */ +span.lineno { + color: #8a8a8a !important; + background-color: transparent !important; +} + +span.lineno a { + color: #8a8a8a !important; + background-color: transparent !important; +} + +/* Desktop: ensure page-nav maintains default width */ +@media screen and (min-width: 768px) { + #page-nav-toggle { + display: none !important; + } + + #page-nav { + position: relative !important; + width: 250px !important; + height: auto !important; + right: auto !important; + top: auto !important; + box-shadow: none !important; + } +} + +/* Mobile-friendly responsive styles */ +@media screen and (max-width: 767px) { + body { + font-size: 14px !important; + } + + /* Make navigation tree collapsible on mobile */ + #side-nav { + display: none; + } + + #doc-content { + margin-left: 0 !important; + margin-right: 0 !important; + } + + /* Make right sidebar (page-nav) overlay on mobile */ + #page-nav { + position: fixed !important; + top: 0 !important; + right: -280px !important; + width: 280px !important; + height: 100vh !important; + z-index: 10000 !important; + background: #101826 !important; + box-shadow: -2px 0 10px rgba(0, 0, 0, 0.5) !important; + transition: right 0.3s ease !important; + overflow-y: auto !important; + -webkit-overflow-scrolling: touch !important; + } + + #page-nav.mobile-open { + right: 0 !important; + } + + /* Hamburger menu button for page nav */ + #page-nav-toggle { + display: block !important; + position: fixed !important; + top: 10px !important; + right: 15px !important; + z-index: 10001 !important; + width: 40px !important; + height: 40px !important; + background: rgba(53, 81, 243, 0.9) !important; + border: none !important; + border-radius: 5px !important; + cursor: pointer !important; + padding: 8px !important; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3) !important; + } + + #page-nav-toggle span { + display: block !important; + width: 24px !important; + height: 3px !important; + background: #fff !important; + margin: 4px 0 !important; + border-radius: 2px !important; + transition: 0.3s !important; + } + + /* Mobile overlay backdrop */ + #page-nav-backdrop { + display: none !important; + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 100% !important; + height: 100% !important; + background: rgba(0, 0, 0, 0.5) !important; + z-index: 9999 !important; + } + + #page-nav-backdrop.active { + display: block !important; + } + + /* Improve header and navigation */ + #top { + height: auto !important; + } + + #titlearea { + padding: 10px !important; + } + + #projectname { + font-size: 18px !important; + } + + #projectbrief, + #projectnumber { + font-size: 12px !important; + } + + /* Make tabs stack better on mobile */ + #navrow1, + #navrow2, + #navrow3, + #navrow4 { + overflow-x: auto !important; + white-space: nowrap !important; + -webkit-overflow-scrolling: touch !important; + } + + .tablist li { + display: inline-block !important; + } + + /* Content adjustments */ + .contents { + padding: 10px !important; + width: 100% !important; + box-sizing: border-box !important; + } + + .header { + padding: 5px !important; + } + + /* Code blocks */ + .fragment { + font-size: 12px !important; + overflow-x: auto !important; + -webkit-overflow-scrolling: touch !important; + } + + div.line { + font-size: 12px !important; + } + + /* Tables */ + table { + display: block !important; + overflow-x: auto !important; + -webkit-overflow-scrolling: touch !important; + width: 100% !important; + } + + .memberdecls table, + .fieldtable { + font-size: 12px !important; + } + + .memtitle { + font-size: 14px !important; + padding: 8px !important; + } + + .memname { + font-size: 13px !important; + word-break: break-word !important; + } + + .memitem { + margin: 5px 0 !important; + } + + /* Search box */ + #MSearchBox { + width: 100% !important; + right: 0 !important; + } + + /* Reduce padding and margins */ + h1, + h2, + h3, + h4, + h5, + h6 { + margin-top: 10px !important; + margin-bottom: 8px !important; + } + + h1 { + font-size: 22px !important; + } + h2 { + font-size: 18px !important; + } + h3 { + font-size: 16px !important; + } + h4 { + font-size: 14px !important; + } + + /* Directory/file listings */ + .directory .levels span { + display: none !important; + } + + .directory .arrow { + margin-right: 5px !important; + } + + /* Treeview adjustments */ + #nav-tree { + width: 100% !important; + } +} + +/* Tablet adjustments */ +@media screen and (min-width: 768px) and (max-width: 1024px) { + .contents { + padding: 15px !important; + } + + #side-nav { + width: 200px !important; + } + + #doc-content { + margin-left: 200px !important; + } +} diff --git a/dist/doxygen/header.html b/dist/doxygen/header.html new file mode 100644 index 000000000..223ec4953 --- /dev/null +++ b/dist/doxygen/header.html @@ -0,0 +1,77 @@ + + + + + + + + +$projectname: $title +$title + + + + + + + + + + + + +$treeview +$search +$mathjax +$darkmode + +$extrastylesheet + + + + +
+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
$projectname $projectnumber +
+
$projectbrief
+
+
$projectbrief
+
$searchbox
$searchbox
+
+ + diff --git a/dist/doxygen/mobile-nav.js b/dist/doxygen/mobile-nav.js new file mode 100644 index 000000000..c6c4e2214 --- /dev/null +++ b/dist/doxygen/mobile-nav.js @@ -0,0 +1,65 @@ +/** + * Mobile navigation toggle for Doxygen documentation + */ + +(function () { + // Only run on mobile devices + function isMobile() { + return window.innerWidth <= 767; + } + + function initMobileNav() { + if (!isMobile()) return; + + const pageNav = document.getElementById("page-nav"); + if (!pageNav) return; + + // Create toggle button + const toggleBtn = document.createElement("button"); + toggleBtn.id = "page-nav-toggle"; + toggleBtn.setAttribute("aria-label", "Toggle page navigation"); + toggleBtn.innerHTML = ""; + document.body.appendChild(toggleBtn); + + // Create backdrop + const backdrop = document.createElement("div"); + backdrop.id = "page-nav-backdrop"; + document.body.appendChild(backdrop); + + // Toggle function + function toggleNav() { + const isOpen = pageNav.classList.toggle("mobile-open"); + backdrop.classList.toggle("active", isOpen); + document.body.style.overflow = isOpen ? "hidden" : ""; + } + + // Event listeners + toggleBtn.addEventListener("click", toggleNav); + backdrop.addEventListener("click", toggleNav); + + // Close on escape key + document.addEventListener("keydown", function (e) { + if (e.key === "Escape" && pageNav.classList.contains("mobile-open")) { + toggleNav(); + } + }); + } + + // Initialize on load and resize + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initMobileNav); + } else { + initMobileNav(); + } + + window.addEventListener("resize", function () { + const pageNav = document.getElementById("page-nav"); + const backdrop = document.getElementById("page-nav-backdrop"); + + if (!isMobile() && pageNav) { + pageNav.classList.remove("mobile-open"); + if (backdrop) backdrop.classList.remove("active"); + document.body.style.overflow = ""; + } + }); +})(); diff --git a/dist/doxygen/stylesheet.css b/dist/doxygen/stylesheet.css new file mode 100644 index 000000000..31ebcc685 --- /dev/null +++ b/dist/doxygen/stylesheet.css @@ -0,0 +1,2659 @@ +/* The standard CSS for doxygen 1.14.0*/ + +html { + /* page base colors */ + --page-background-color: white; + --page-foreground-color: black; + --page-link-color: #3d578c; + --page-visited-link-color: #3d578c; + --page-external-link-color: #334975; + + /* index */ + --index-odd-item-bg-color: #f8f9fc; + --index-even-item-bg-color: white; + --index-header-color: black; + --index-separator-color: #a0a0a0; + + /* header */ + --header-background-color: #f9fafc; + --header-separator-color: #c4cfe5; + --group-header-separator-color: #d9e0ee; + --group-header-color: #354c7b; + --inherit-header-color: gray; + + --footer-foreground-color: #2a3d61; + --footer-logo-width: 75px; + --citation-label-color: #334975; + --glow-color: cyan; + + --title-background-color: white; + --title-separator-color: #c4cfe5; + --directory-separator-color: #9cafd4; + --separator-color: #4a6aaa; + + --blockquote-background-color: #f7f8fb; + --blockquote-border-color: #9cafd4; + + --scrollbar-thumb-color: #c4cfe5; + --scrollbar-background-color: #f9fafc; + + --icon-background-color: #728dc1; + --icon-foreground-color: white; + /* +--icon-doc-image: url('doc.svg'); +--icon-folder-open-image: url('folderopen.svg'); +--icon-folder-closed-image: url('folderclosed.svg');*/ + --icon-folder-open-fill-color: #c4cfe5; + --icon-folder-fill-color: #d8dfee; + --icon-folder-border-color: #4665a2; + --icon-doc-fill-color: #d8dfee; + --icon-doc-border-color: #4665a2; + + /* brief member declaration list */ + --memdecl-background-color: #f9fafc; + --memdecl-separator-color: #dee4f0; + --memdecl-foreground-color: #555; + --memdecl-template-color: #4665a2; + --memdecl-border-color: #d5ddec; + + /* detailed member list */ + --memdef-border-color: #a8b8d9; + --memdef-title-background-color: #e2e8f2; + --memdef-proto-background-color: #eef1f7; + --memdef-proto-text-color: #253555; + --memdef-doc-background-color: white; + --memdef-param-name-color: #602020; + --memdef-template-color: #4665a2; + + /* tables */ + --table-cell-border-color: #2d4068; + --table-header-background-color: #374f7f; + --table-header-foreground-color: #ffffff; + + /* labels */ + --label-background-color: #728dc1; + --label-left-top-border-color: #5373b4; + --label-right-bottom-border-color: #c4cfe5; + --label-foreground-color: white; + + /** navigation bar/tree/menu */ + --nav-background-color: #f9fafc; + --nav-foreground-color: #364d7c; + --nav-border-color: #c4cfe5; + --nav-breadcrumb-separator-color: #c4cfe5; + --nav-breadcrumb-active-bg: #eef1f7; + --nav-breadcrumb-color: #354c7b; + --nav-breadcrumb-border-color: #e1e7f2; + --nav-splitbar-bg-color: #dce2ef; + --nav-splitbar-handle-color: #9cafd4; + --nav-font-size-level1: 13px; + --nav-font-size-level2: 10px; + --nav-font-size-level3: 9px; + --nav-text-normal-color: #283a5d; + --nav-text-hover-color: white; + --nav-text-active-color: white; + --nav-menu-button-color: #364d7c; + --nav-menu-background-color: white; + --nav-menu-foreground-color: #555555; + --nav-menu-active-bg: #dce2ef; + --nav-menu-active-color: #9cafd4; + --nav-menu-toggle-color: rgba(255, 255, 255, 0.5); + --nav-arrow-color: #b6c4df; + --nav-arrow-selected-color: #90a5ce; + + /* sync icon */ + --sync-icon-border-color: #c4cfe5; + --sync-icon-background-color: #f9fafc; + --sync-icon-selected-background-color: #eef1f7; + --sync-icon-color: #c4cfe5; + --sync-icon-selected-color: #6884bd; + + /* table of contents */ + --toc-background-color: #f4f6fa; + --toc-border-color: #d8dfee; + --toc-header-color: #4665a2; + --toc-down-arrow-image: url("data:image/svg+xml;utf8,&%238595;"); + + /** search field */ + --search-background-color: white; + --search-foreground-color: #909090; + --search-active-color: black; + --search-filter-background-color: rgba(255, 255, 255, 0.7); + --search-filter-backdrop-filter: blur(4px); + --search-filter-foreground-color: black; + --search-filter-border-color: rgba(150, 150, 150, 0.4); + --search-filter-highlight-text-color: white; + --search-filter-highlight-bg-color: #3d578c; + --search-results-foreground-color: #425e97; + --search-results-background-color: rgba(255, 255, 255, 0.8); + --search-results-backdrop-filter: blur(4px); + --search-results-border-color: rgba(150, 150, 150, 0.4); + --search-box-border-color: #b6c4df; + --search-close-icon-bg-color: #a0a0a0; + --search-close-icon-fg-color: white; + + /** code fragments */ + --code-keyword-color: #008000; + --code-type-keyword-color: #604020; + --code-flow-keyword-color: #e08000; + --code-comment-color: #800000; + --code-preprocessor-color: #806020; + --code-string-literal-color: #002080; + --code-char-literal-color: #008080; + --code-xml-cdata-color: black; + --code-vhdl-digit-color: #ff00ff; + --code-vhdl-char-color: #000000; + --code-vhdl-keyword-color: #700070; + --code-vhdl-logic-color: #ff0000; + --code-link-color: #4665a2; + --code-external-link-color: #4665a2; + --fragment-foreground-color: black; + --fragment-background-color: #fbfcfd; + --fragment-border-color: #c4cfe5; + --fragment-lineno-border-color: #00ff00; + --fragment-lineno-background-color: #e8e8e8; + --fragment-lineno-foreground-color: black; + --fragment-lineno-link-fg-color: #4665a2; + --fragment-lineno-link-bg-color: #d8d8d8; + --fragment-lineno-link-hover-fg-color: #4665a2; + --fragment-lineno-link-hover-bg-color: #c8c8c8; + --fragment-copy-ok-color: #2ec82e; + --tooltip-foreground-color: black; + --tooltip-background-color: rgba(255, 255, 255, 0.8); + --tooltip-arrow-background-color: white; + --tooltip-border-color: rgba(150, 150, 150, 0.7); + --tooltip-backdrop-filter: blur(3px); + --tooltip-doc-color: gray; + --tooltip-declaration-color: #006318; + --tooltip-link-color: #4665a2; + --tooltip-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.25); + --fold-line-color: #808080; + + /** font-family */ + --font-family-normal: + system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, + sans-serif, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + --font-family-monospace: + "JetBrains Mono", Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace, + fixed; + --font-family-nav: "Lucida Grande", Geneva, Helvetica, Arial, sans-serif; + --font-family-title: + system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, + sans-serif, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + --font-family-toc: Verdana, "DejaVu Sans", Geneva, sans-serif; + --font-family-search: Arial, Verdana, sans-serif; + --font-family-icon: Arial, Helvetica; + --font-family-tooltip: Roboto, sans-serif; + + /** special sections */ + --warning-color-bg: #f8d1cc; + --warning-color-hl: #b61825; + --warning-color-text: #75070f; + --note-color-bg: #faf3d8; + --note-color-hl: #f3a600; + --note-color-text: #5f4204; + --todo-color-bg: #e4f3ff; + --todo-color-hl: #1879c4; + --todo-color-text: #274a5c; + --test-color-bg: #e8e8ff; + --test-color-hl: #3939c4; + --test-color-text: #1a1a5c; + --deprecated-color-bg: #ecf0f3; + --deprecated-color-hl: #5b6269; + --deprecated-color-text: #43454a; + --bug-color-bg: #e4dafd; + --bug-color-hl: #5b2bdd; + --bug-color-text: #2a0d72; + --invariant-color-bg: #d8f1e3; + --invariant-color-hl: #44b86f; + --invariant-color-text: #265532; +} + +@media (prefers-color-scheme: dark) { + html:not(.dark-mode) { + color-scheme: dark; + + /* page base colors */ + --page-background-color: black; + --page-foreground-color: #c9d1d9; + --page-link-color: #90a5ce; + --page-visited-link-color: #90a5ce; + --page-external-link-color: #a3b4d7; + + /* index */ + --index-odd-item-bg-color: #0b101a; + --index-even-item-bg-color: black; + --index-header-color: #c4cfe5; + --index-separator-color: #334975; + + /* header */ + --header-background-color: #070b11; + --header-separator-color: #141c2e; + --group-header-separator-color: #1d2a43; + --group-header-color: #90a5ce; + --inherit-header-color: #a0a0a0; + + --footer-foreground-color: #5b7ab7; + --footer-logo-width: 60px; + --citation-label-color: #90a5ce; + --glow-color: cyan; + + --title-background-color: #090d16; + --title-separator-color: #212f4b; + --directory-separator-color: #283a5d; + --separator-color: #283a5d; + + --blockquote-background-color: #101826; + --blockquote-border-color: #283a5d; + + --scrollbar-thumb-color: #2c3f65; + --scrollbar-background-color: #070b11; + + --icon-background-color: #334975; + --icon-foreground-color: #c4cfe5; + --icon-folder-open-fill-color: #4665a2; + --icon-folder-fill-color: #5373b4; + --icon-folder-border-color: #c4cfe5; + --icon-doc-fill-color: #6884bd; + --icon-doc-border-color: #c4cfe5; + + /* brief member declaration list */ + --memdecl-background-color: #0b101a; + --memdecl-separator-color: #2c3f65; + --memdecl-foreground-color: #bbb; + --memdecl-template-color: #7c95c6; + --memdecl-border-color: #233250; + + /* detailed member list */ + --memdef-border-color: #233250; + --memdef-title-background-color: #1b2840; + --memdef-proto-background-color: #19243a; + --memdef-proto-text-color: #9db0d4; + --memdef-doc-background-color: black; + --memdef-param-name-color: #d28757; + --memdef-template-color: #7c95c6; + + /* tables */ + --table-cell-border-color: #283a5d; + --table-header-background-color: #283a5d; + --table-header-foreground-color: #c4cfe5; + + /* labels */ + --label-background-color: #354c7b; + --label-left-top-border-color: #4665a2; + --label-right-bottom-border-color: #283a5d; + --label-foreground-color: #cccccc; + + /** navigation bar/tree/menu */ + --nav-background-color: #101826; + --nav-foreground-color: #364d7c; + --nav-border-color: #212f4b; + --nav-breadcrumb-separator-color: #212f4b; + --nav-breadcrumb-active-bg: #1d2a43; + --nav-breadcrumb-color: #90a5ce; + --nav-breadcrumb-border-color: #2a3d61; + --nav-splitbar-bg-color: #283a5d; + --nav-splitbar-handle-color: #4665a2; + --nav-font-size-level1: 13px; + --nav-font-size-level2: 10px; + --nav-font-size-level3: 9px; + --nav-text-normal-color: #b6c4df; + --nav-text-hover-color: #dce2ef; + --nav-text-active-color: #dce2ef; + --nav-menu-button-color: #b6c4df; + --nav-menu-background-color: #05070c; + --nav-menu-foreground-color: #bbbbbb; + --nav-menu-active-bg: #1d2a43; + --nav-menu-active-color: #c9d3e7; + --nav-menu-toggle-color: rgba(255, 255, 255, 0.2); + --nav-arrow-color: #4665a2; + --nav-arrow-selected-color: #6884bd; + + /* sync icon */ + --sync-icon-border-color: #212f4b; + --sync-icon-background-color: #101826; + --sync-icon-selected-background-color: #1d2a43; + --sync-icon-color: #4665a2; + --sync-icon-selected-color: #5373b4; + + /* table of contents */ + --toc-background-color: #151e30; + --toc-border-color: #202e4a; + --toc-header-color: #a3b4d7; + --toc-down-arrow-image: url("data:image/svg+xml;utf8,&%238595;"); + + /** search field */ + --search-background-color: black; + --search-foreground-color: #c5c5c5; + --search-active-color: #f5f5f5; + --search-filter-background-color: #101826; + --search-filter-foreground-color: #90a5ce; + --search-filter-backdrop-filter: none; + --search-filter-border-color: #7c95c6; + --search-filter-highlight-text-color: #bcc9e2; + --search-filter-highlight-bg-color: #283a5d; + --search-results-background-color: black; + --search-results-foreground-color: #90a5ce; + --search-results-backdrop-filter: none; + --search-results-border-color: #334975; + --search-box-border-color: #334975; + --search-close-icon-bg-color: #909090; + --search-close-icon-fg-color: black; + + /** code fragments */ + --code-keyword-color: #cc99cd; + --code-type-keyword-color: #ab99cd; + --code-flow-keyword-color: #e08000; + --code-comment-color: #717790; + --code-preprocessor-color: #65cabe; + --code-string-literal-color: #7ec699; + --code-char-literal-color: #00e0f0; + --code-xml-cdata-color: #c9d1d9; + --code-vhdl-digit-color: #ff00ff; + --code-vhdl-char-color: #c0c0c0; + --code-vhdl-keyword-color: #cf53c9; + --code-vhdl-logic-color: #ff0000; + --code-link-color: #79c0ff; + --code-external-link-color: #79c0ff; + --fragment-foreground-color: #c9d1d9; + --fragment-background-color: #090d16; + --fragment-border-color: #30363d; + --fragment-lineno-border-color: #30363d; + --fragment-lineno-background-color: black; + --fragment-lineno-foreground-color: #6e7681; + --fragment-lineno-link-fg-color: #6e7681; + --fragment-lineno-link-bg-color: #303030; + --fragment-lineno-link-hover-fg-color: #8e96a1; + --fragment-lineno-link-hover-bg-color: #505050; + --fragment-copy-ok-color: #0ea80e; + --tooltip-foreground-color: #c9d1d9; + --tooltip-background-color: #202020; + --tooltip-arrow-background-color: #202020; + --tooltip-backdrop-filter: none; + --tooltip-border-color: #c9d1d9; + --tooltip-doc-color: #d9e1e9; + --tooltip-declaration-color: #20c348; + --tooltip-link-color: #79c0ff; + --tooltip-shadow: none; + --fold-line-color: #808080; + + /** font-family */ + --font-family-normal: + system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, + sans-serif, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + --font-family-monospace: + "JetBrains Mono", Consolas, Monaco, "Andale Mono", "Ubuntu Mono", + monospace, fixed; + --font-family-nav: "Lucida Grande", Geneva, Helvetica, Arial, sans-serif; + --font-family-title: + system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, + sans-serif, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + --font-family-toc: Verdana, "DejaVu Sans", Geneva, sans-serif; + --font-family-search: Arial, Verdana, sans-serif; + --font-family-icon: Arial, Helvetica; + --font-family-tooltip: Roboto, sans-serif; + + /** special sections */ + --warning-color-bg: #2e1917; + --warning-color-hl: #ad2617; + --warning-color-text: #f5b1aa; + --note-color-bg: #3b2e04; + --note-color-hl: #f1b602; + --note-color-text: #ceb670; + --todo-color-bg: #163750; + --todo-color-hl: #1982d2; + --todo-color-text: #dcf0fa; + --test-color-bg: #121258; + --test-color-hl: #4242cf; + --test-color-text: #c0c0da; + --deprecated-color-bg: #2e323b; + --deprecated-color-hl: #738396; + --deprecated-color-text: #abb0bd; + --bug-color-bg: #2a2536; + --bug-color-hl: #7661b3; + --bug-color-text: #ae9ed6; + --invariant-color-bg: #303a35; + --invariant-color-hl: #76ce96; + --invariant-color-text: #cceed5; + } +} +body { + background-color: var(--page-background-color); + color: var(--page-foreground-color); +} + +body, +table, +div, +p, +dl { + font-weight: 400; + font-size: 14px; + font-family: var(--font-family-normal); + line-height: 22px; +} + +body.resizing { + user-select: none; + -webkit-user-select: none; +} + +#doc-content { + scrollbar-width: thin; +} + +/* @group Heading Levels */ + +.title { + font-family: var(--font-family-normal); + line-height: 28px; + font-size: 160%; + font-weight: 400; + margin: 10px 2px; +} + +h1.groupheader { + font-size: 150%; +} + +h2.groupheader { + box-shadow: + 12px 0 var(--page-background-color), + -12px 0 var(--page-background-color), + 12px 1px var(--group-header-separator-color), + -12px 1px var(--group-header-separator-color); + color: var(--group-header-color); + font-size: 150%; + font-weight: normal; + margin-top: 1.75em; + padding-top: 8px; + padding-bottom: 4px; + width: 100%; +} + +td h2.groupheader { + box-shadow: + 13px 0 var(--page-background-color), + -13px 0 var(--page-background-color), + 13px 1px var(--group-header-separator-color), + -13px 1px var(--group-header-separator-color); +} + +h3.groupheader { + font-size: 100%; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + -webkit-transition: text-shadow 0.5s linear; + -moz-transition: text-shadow 0.5s linear; + -ms-transition: text-shadow 0.5s linear; + -o-transition: text-shadow 0.5s linear; + transition: text-shadow 0.5s linear; + margin-right: 15px; +} + +h1.glow, +h2.glow, +h3.glow, +h4.glow, +h5.glow, +h6.glow { + text-shadow: 0 0 15px var(--glow-color); +} + +dt { + font-weight: bold; +} + +p.startli, +p.startdd { + margin-top: 2px; +} + +th p.starttd, +th p.intertd, +th p.endtd { + font-size: 100%; + font-weight: 700; +} + +p.starttd { + margin-top: 0px; +} + +p.endli { + margin-bottom: 0px; +} + +p.enddd { + margin-bottom: 4px; +} + +p.endtd { + margin-bottom: 2px; +} + +p.interli { +} + +p.interdd { +} + +p.intertd { +} + +/* @end */ + +caption { + font-weight: bold; +} + +span.legend { + font-size: 70%; + text-align: center; +} + +h3.version { + font-size: 90%; + text-align: center; +} + +div.navtab { + margin-right: 6px; + padding-right: 6px; + text-align: right; + line-height: 110%; + background-color: var(--nav-background-color); +} + +div.navtab table { + border-spacing: 0; +} + +td.navtab { + padding-right: 6px; + padding-left: 6px; +} + +td.navtabHL { + padding-right: 6px; + padding-left: 6px; + border-radius: 0 6px 6px 0; + background-color: var(--nav-menu-active-bg); +} + +div.qindex { + text-align: center; + width: 100%; + line-height: 140%; + font-size: 130%; + color: var(--index-separator-color); +} + +#main-menu a:focus { + outline: auto; + z-index: 10; + position: relative; +} + +dt.alphachar { + font-size: 180%; + font-weight: bold; +} + +.alphachar a { + color: var(--index-header-color); +} + +.alphachar a:hover, +.alphachar a:visited { + text-decoration: none; +} + +.classindex dl { + padding: 25px; + column-count: 1; +} + +.classindex dd { + display: inline-block; + margin-left: 50px; + width: 90%; + line-height: 1.15em; +} + +.classindex dl.even { + background-color: var(--index-even-item-bg-color); +} + +.classindex dl.odd { + background-color: var(--index-odd-item-bg-color); +} + +@media (min-width: 1120px) { + .classindex dl { + column-count: 2; + } +} + +@media (min-width: 1320px) { + .classindex dl { + column-count: 3; + } +} + +/* @group Link Styling */ + +a { + color: var(--page-link-color); + font-weight: normal; + text-decoration: none; +} + +.contents a:visited { + color: var(--page-visited-link-color); +} + +span.label a:hover { + text-decoration: none; + background: linear-gradient( + to bottom, + transparent 0, + transparent calc(100% - 1px), + currentColor 100% + ); +} + +a.el { + font-weight: bold; +} + +a.elRef { +} + +a.el, +a.el:visited, +a.code, +a.code:visited, +a.line, +a.line:visited { + color: var(--page-link-color); +} + +a.codeRef, +a.codeRef:visited, +a.lineRef, +a.lineRef:visited { + color: var(--page-external-link-color); +} + +a.code.hl_class { + /* style for links to class names in code snippets */ +} +a.code.hl_struct { + /* style for links to struct names in code snippets */ +} +a.code.hl_union { + /* style for links to union names in code snippets */ +} +a.code.hl_interface { + /* style for links to interface names in code snippets */ +} +a.code.hl_protocol { + /* style for links to protocol names in code snippets */ +} +a.code.hl_category { + /* style for links to category names in code snippets */ +} +a.code.hl_exception { + /* style for links to exception names in code snippets */ +} +a.code.hl_service { + /* style for links to service names in code snippets */ +} +a.code.hl_singleton { + /* style for links to singleton names in code snippets */ +} +a.code.hl_concept { + /* style for links to concept names in code snippets */ +} +a.code.hl_namespace { + /* style for links to namespace names in code snippets */ +} +a.code.hl_package { + /* style for links to package names in code snippets */ +} +a.code.hl_define { + /* style for links to macro names in code snippets */ +} +a.code.hl_function { + /* style for links to function names in code snippets */ +} +a.code.hl_variable { + /* style for links to variable names in code snippets */ +} +a.code.hl_typedef { + /* style for links to typedef names in code snippets */ +} +a.code.hl_enumvalue { + /* style for links to enum value names in code snippets */ +} +a.code.hl_enumeration { + /* style for links to enumeration names in code snippets */ +} +a.code.hl_signal { + /* style for links to Qt signal names in code snippets */ +} +a.code.hl_slot { + /* style for links to Qt slot names in code snippets */ +} +a.code.hl_friend { + /* style for links to friend names in code snippets */ +} +a.code.hl_dcop { + /* style for links to KDE3 DCOP names in code snippets */ +} +a.code.hl_property { + /* style for links to property names in code snippets */ +} +a.code.hl_event { + /* style for links to event names in code snippets */ +} +a.code.hl_sequence { + /* style for links to sequence names in code snippets */ +} +a.code.hl_dictionary { + /* style for links to dictionary names in code snippets */ +} + +/* @end */ + +dl.el { + margin-left: -1cm; +} + +ul.check { + list-style: none; + text-indent: -16px; + padding-left: 38px; +} +li.unchecked:before { + content: "\2610\A0"; +} +li.checked:before { + content: "\2611\A0"; +} + +ol { + text-indent: 0px; +} + +ul { + text-indent: 0px; + overflow: visible; +} + +ul.multicol { + -moz-column-gap: 1em; + -webkit-column-gap: 1em; + column-gap: 1em; + -moz-column-count: 3; + -webkit-column-count: 3; + column-count: 3; + list-style-type: none; +} + +#side-nav ul { + overflow: visible; /* reset ul rule for scroll bar in GENERATE_TREEVIEW window */ +} + +#main-nav ul { + overflow: visible; /* reset ul rule for the navigation bar drop down lists */ +} + +.fragment { + text-align: left; + direction: ltr; + overflow-x: auto; + overflow-y: hidden; + position: relative; + min-height: 12px; + margin: 10px 0px; + padding: 10px 10px; + border: 1px solid var(--fragment-border-color); + border-radius: 4px; + background-color: var(--fragment-background-color); + color: var(--fragment-foreground-color); +} + +pre.fragment { + word-wrap: break-word; + font-size: 10pt; + line-height: 125%; + font-family: var(--font-family-monospace); +} + +span.tt { + white-space: pre; + font-family: var(--font-family-monospace); +} + +.clipboard { + width: 24px; + height: 24px; + right: 5px; + top: 5px; + opacity: 0; + position: absolute; + display: inline; + overflow: hidden; + justify-content: center; + align-items: center; + cursor: pointer; +} + +.clipboard.success { + border: 1px solid var(--fragment-foreground-color); + border-radius: 4px; +} + +.fragment:hover .clipboard, +.clipboard.success { + opacity: 0.4; +} + +.clipboard:hover, +.clipboard.success { + opacity: 1 !important; +} + +.clipboard:active:not([class~="success"]) svg { + transform: scale(0.91); +} + +.clipboard.success svg { + fill: var(--fragment-copy-ok-color); +} + +.clipboard.success { + border-color: var(--fragment-copy-ok-color); +} + +div.line { + font-family: var(--font-family-monospace); + font-size: 13px; + min-height: 13px; + line-height: 1.2; + text-wrap: wrap; + word-break: break-all; + white-space: -moz-pre-wrap; /* Moz */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + white-space: pre-wrap; /* CSS3 */ + word-wrap: break-word; /* IE 5.5+ */ + text-indent: -62px; + padding-left: 62px; + padding-bottom: 0px; + margin: 0px; + -webkit-transition-property: background-color, box-shadow; + -webkit-transition-duration: 0.5s; + -moz-transition-property: background-color, box-shadow; + -moz-transition-duration: 0.5s; + -ms-transition-property: background-color, box-shadow; + -ms-transition-duration: 0.5s; + -o-transition-property: background-color, box-shadow; + -o-transition-duration: 0.5s; + transition-property: background-color, box-shadow; + transition-duration: 0.5s; +} + +div.line:after { + content: "\000A"; + white-space: pre; +} + +div.line.glow { + background-color: var(--glow-color); + box-shadow: 0 0 10px var(--glow-color); +} + +span.fold { + display: inline-block; + width: 12px; + height: 12px; + margin-left: 4px; + margin-right: 1px; +} + +span.foldnone { + display: inline-block; + position: relative; + cursor: pointer; + user-select: none; +} + +span.fold.plus, +span.fold.minus { + width: 10px; + height: 10px; + background-color: var(--fragment-background-color); + position: relative; + border: 1px solid var(--fold-line-color); + margin-right: 1px; +} + +span.fold.plus::before, +span.fold.minus::before { + content: ""; + position: absolute; + background-color: var(--fold-line-color); +} + +span.fold.plus::before { + width: 2px; + height: 6px; + top: 2px; + left: 4px; +} + +span.fold.plus::after { + content: ""; + position: absolute; + width: 6px; + height: 2px; + top: 4px; + left: 2px; + background-color: var(--fold-line-color); +} + +span.fold.minus::before { + width: 6px; + height: 2px; + top: 4px; + left: 2px; +} + +span.lineno { + padding-right: 4px; + margin-right: 9px; + text-align: right; + border-right: 2px solid var(--fragment-lineno-border-color); + color: var(--fragment-lineno-foreground-color); + background-color: var(--fragment-lineno-background-color); + white-space: pre; +} +span.lineno a, +span.lineno a:visited { + color: var(--fragment-lineno-link-fg-color); + background-color: var(--fragment-lineno-link-bg-color); +} + +span.lineno a:hover { + color: var(--fragment-lineno-link-hover-fg-color); + background-color: var(--fragment-lineno-link-hover-bg-color); +} + +.lineno { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +div.classindex ul { + list-style: none; + padding-left: 0; +} + +div.classindex span.ai { + display: inline-block; +} + +div.groupHeader { + box-shadow: + 13px 0 var(--page-background-color), + -13px 0 var(--page-background-color), + 13px 1px var(--group-header-separator-color), + -13px 1px var(--group-header-separator-color); + color: var(--group-header-color); + font-size: 110%; + font-weight: 500; + margin-left: 0px; + margin-top: 0em; + margin-bottom: 6px; + padding-top: 8px; + padding-bottom: 4px; +} + +div.groupText { + margin-left: 16px; + font-style: italic; +} + +body { + color: var(--page-foreground-color); + margin: 0; +} + +div.contents { + margin-top: 10px; + margin-left: 12px; + margin-right: 12px; +} + +p.formulaDsp { + text-align: center; +} + +img.dark-mode-visible { + display: none; +} +img.light-mode-visible { + display: none; +} + +img.formulaInl, +img.inline { + vertical-align: middle; +} + +div.center { + text-align: center; + margin-top: 0px; + margin-bottom: 0px; + padding: 0px; +} + +div.center img { + border: 0px; +} + +address.footer { + text-align: right; + padding-right: 12px; +} + +img.footer { + border: 0px; + vertical-align: middle; + width: var(--footer-logo-width); +} + +.compoundTemplParams { + color: var(--memdecl-template-color); + font-size: 80%; + line-height: 120%; +} + +/* @group Code Colorization */ + +span.keyword { + color: var(--code-keyword-color); +} + +span.keywordtype { + color: var(--code-type-keyword-color); +} + +span.keywordflow { + color: var(--code-flow-keyword-color); +} + +span.comment { + color: var(--code-comment-color); +} + +span.preprocessor { + color: var(--code-preprocessor-color); +} + +span.stringliteral { + color: var(--code-string-literal-color); +} + +span.charliteral { + color: var(--code-char-literal-color); +} + +span.xmlcdata { + color: var(--code-xml-cdata-color); +} + +span.vhdldigit { + color: var(--code-vhdl-digit-color); +} + +span.vhdlchar { + color: var(--code-vhdl-char-color); +} + +span.vhdlkeyword { + color: var(--code-vhdl-keyword-color); +} + +span.vhdllogic { + color: var(--code-vhdl-logic-color); +} + +blockquote { + background-color: var(--blockquote-background-color); + border-left: 2px solid var(--blockquote-border-color); + margin: 0 24px 0 4px; + padding: 0 12px 0 16px; +} + +/* @end */ + +td.tiny { + font-size: 75%; +} + +.dirtab { + padding: 4px; + border-collapse: collapse; + border: 1px solid var(--table-cell-border-color); +} + +th.dirtab { + background-color: var(--table-header-background-color); + color: var(--table-header-foreground-color); + font-weight: bold; +} + +hr { + border: none; + margin-top: 16px; + margin-bottom: 16px; + height: 1px; + box-shadow: + 13px 0 var(--page-background-color), + -13px 0 var(--page-background-color), + 13px 1px var(--group-header-separator-color), + -13px 1px var(--group-header-separator-color); +} + +hr.footer { + height: 1px; +} + +/* @group Member Descriptions */ + +table.memberdecls { + border-spacing: 0px; + padding: 0px; +} + +.memberdecls td, +.fieldtable tr { + transition-property: background-color, box-shadow; + transition-duration: 0.5s; +} + +.memberdecls td.glow, +.fieldtable tr.glow { + background-color: var(--glow-color); + box-shadow: 0 0 15px var(--glow-color); +} + +.memberdecls tr[class^="memitem"] { + font-family: var(--font-family-monospace); +} + +.mdescLeft, +.mdescRight, +.memItemLeft, +.memItemRight { + padding-top: 2px; + padding-bottom: 2px; +} + +.memTemplParams { + padding-left: 10px; + padding-top: 5px; +} + +.memItemLeft, +.memItemRight, +.memTemplParams { + background-color: var(--memdecl-background-color); +} + +.mdescLeft, +.mdescRight { + padding: 0px 8px 4px 8px; + color: var(--memdecl-foreground-color); +} + +tr[class^="memdesc"] { + box-shadow: inset 0px 1px 3px 0px rgba(0, 0, 0, 0.075); +} + +.mdescLeft { + border-left: 1px solid var(--memdecl-border-color); + border-bottom: 1px solid var(--memdecl-border-color); +} + +.mdescRight { + border-right: 1px solid var(--memdecl-border-color); + border-bottom: 1px solid var(--memdecl-border-color); +} + +.memTemplParams { + color: var(--memdecl-template-color); + white-space: nowrap; + font-size: 80%; + border-left: 1px solid var(--memdecl-border-color); + border-right: 1px solid var(--memdecl-border-color); +} + +td.ititle { + border: 1px solid var(--memdecl-border-color); + border-top-left-radius: 4px; + border-top-right-radius: 4px; + padding-left: 10px; +} + +tr:not(:first-child) > td.ititle { + border-top: 0; + border-radius: 0; +} + +.memItemLeft { + white-space: nowrap; + border-left: 1px solid var(--memdecl-border-color); + border-bottom: 1px solid var(--memdecl-border-color); + padding-left: 10px; + transition: none; +} + +.memItemRight { + width: 100%; + border-right: 1px solid var(--memdecl-border-color); + border-bottom: 1px solid var(--memdecl-border-color); + padding-right: 10px; + transition: none; +} + +tr.heading + tr[class^="memitem"] td.memItemLeft, +tr.groupHeader + tr[class^="memitem"] td.memItemLeft, +tr.inherit_header + tr[class^="memitem"] td.memItemLeft { + border-top: 1px solid var(--memdecl-border-color); + border-top-left-radius: 4px; +} + +tr.heading + tr[class^="memitem"] td.memItemRight, +tr.groupHeader + tr[class^="memitem"] td.memItemRight, +tr.inherit_header + tr[class^="memitem"] td.memItemRight { + border-top: 1px solid var(--memdecl-border-color); + border-top-right-radius: 4px; +} + +tr.heading + tr[class^="memitem"] td.memTemplParams, +tr.heading + tr td.ititle, +tr.groupHeader + tr[class^="memitem"] td.memTemplParams, +tr.groupHeader + tr td.ititle, +tr.inherit_header + tr[class^="memitem"] td.memTemplParams { + border-top: 1px solid var(--memdecl-border-color); + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +table.memberdecls tr:last-child td.memItemLeft, +table.memberdecls tr:last-child td.mdescLeft, +table.memberdecls tr[class^="memitem"]:has(+ tr.groupHeader) td.memItemLeft, +table.memberdecls tr[class^="memitem"]:has(+ tr.inherit_header) td.memItemLeft, +table.memberdecls tr[class^="memdesc"]:has(+ tr.groupHeader) td.mdescLeft, +table.memberdecls tr[class^="memdesc"]:has(+ tr.inherit_header) td.mdescLeft { + border-bottom-left-radius: 4px; +} + +table.memberdecls tr:last-child td.memItemRight, +table.memberdecls tr:last-child td.mdescRight, +table.memberdecls tr[class^="memitem"]:has(+ tr.groupHeader) td.memItemRight, +table.memberdecls tr[class^="memitem"]:has(+ tr.inherit_header) td.memItemRight, +table.memberdecls tr[class^="memdesc"]:has(+ tr.groupHeader) td.mdescRight, +table.memberdecls tr[class^="memdesc"]:has(+ tr.inherit_header) td.mdescRight { + border-bottom-right-radius: 4px; +} + +tr.template .memItemLeft, +tr.template .memItemRight { + border-top: none; + padding-top: 0; +} + +/* @end */ + +/* @group Member Details */ + +/* Styles for detailed member documentation */ + +.memtitle { + padding: 8px; + border-top: 1px solid var(--memdef-border-color); + border-left: 1px solid var(--memdef-border-color); + border-right: 1px solid var(--memdef-border-color); + border-top-right-radius: 4px; + border-top-left-radius: 4px; + margin-bottom: -1px; + background-color: var(--memdef-proto-background-color); + line-height: 1.25; + font-family: var(--font-family-monospace); + font-weight: 500; + font-size: 16px; + float: left; + box-shadow: + 0 10px 0 -1px var(--memdef-proto-background-color), + 0 2px 8px 0 rgba(0, 0, 0, 0.075); + position: relative; +} + +.memtitle:after { + content: ""; + display: block; + background: var(--memdef-proto-background-color); + height: 10px; + bottom: -10px; + left: 0px; + right: -14px; + position: absolute; + border-top-right-radius: 6px; +} + +.permalink { + font-family: var(--font-family-monospace); + font-weight: 500; + line-height: 1.25; + font-size: 16px; + display: inline-block; + vertical-align: middle; +} + +.memtemplate { + font-size: 80%; + color: var(--memdef-template-color); + font-family: var(--font-family-monospace); + font-weight: normal; + margin-left: 9px; +} + +.mempage { + width: 100%; +} + +.memitem { + padding: 0; + margin-bottom: 10px; + margin-right: 5px; + display: table !important; + width: 100%; + box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.075); + border-radius: 4px; +} + +.memitem.glow { + box-shadow: 0 0 15px var(--glow-color); +} + +.memname { + font-family: var(--font-family-monospace); + font-size: 13px; + font-weight: 400; + margin-left: 6px; +} + +.memname td { + vertical-align: bottom; +} + +.memproto, +dl.reflist dt { + border-top: 1px solid var(--memdef-border-color); + border-left: 1px solid var(--memdef-border-color); + border-right: 1px solid var(--memdef-border-color); + padding: 6px 0px 6px 0px; + color: var(--memdef-proto-text-color); + font-weight: bold; + background-color: var(--memdef-proto-background-color); + border-top-right-radius: 4px; + border-bottom: 1px solid var(--memdef-border-color); +} + +.overload { + font-family: var(--font-family-monospace); + font-size: 65%; +} + +.memdoc, +dl.reflist dd { + border-bottom: 1px solid var(--memdef-border-color); + border-left: 1px solid var(--memdef-border-color); + border-right: 1px solid var(--memdef-border-color); + padding: 6px 10px 2px 10px; + border-top-width: 0; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + +dl.reflist dt { + padding: 5px; +} + +dl.reflist dd { + margin: 0px 0px 10px 0px; + padding: 5px; +} + +.paramkey { + text-align: right; +} + +.paramtype { + white-space: nowrap; + padding: 0px; + padding-bottom: 1px; +} + +.paramname { + white-space: nowrap; + padding: 0px; + padding-bottom: 1px; + margin-left: 2px; +} + +.paramname em { + color: var(--memdef-param-name-color); + font-style: normal; + margin-right: 1px; +} + +.paramname .paramdefval { + font-family: var(--font-family-monospace); +} + +.params, +.retval, +.exception, +.tparams { + margin-left: 0px; + padding-left: 0px; +} + +.params .paramname, +.retval .paramname, +.tparams .paramname, +.exception .paramname { + font-weight: bold; + vertical-align: top; +} + +.params .paramtype, +.tparams .paramtype { + font-style: italic; + vertical-align: top; +} + +.params .paramdir, +.tparams .paramdir { + font-family: var(--font-family-monospace); + vertical-align: top; +} + +table.mlabels { + border-spacing: 0px; +} + +td.mlabels-left { + width: 100%; + padding: 0px; +} + +td.mlabels-right { + vertical-align: bottom; + padding: 0px; + white-space: nowrap; +} + +span.mlabels { + margin-left: 8px; +} + +span.mlabel { + background-color: var(--label-background-color); + border-top: 1px solid var(--label-left-top-border-color); + border-left: 1px solid var(--label-left-top-border-color); + border-right: 1px solid var(--label-right-bottom-border-color); + border-bottom: 1px solid var(--label-right-bottom-border-color); + text-shadow: none; + color: var(--label-foreground-color); + margin-right: 4px; + padding: 2px 3px; + border-radius: 3px; + font-size: 7pt; + white-space: nowrap; + vertical-align: middle; +} + +/* @end */ + +/* these are for tree view inside a (index) page */ + +div.directory { + margin: 10px 0px; + width: 100%; +} + +.directory table { + border-collapse: collapse; +} + +.directory td { + margin: 0px; + padding: 0px; + vertical-align: top; +} + +.directory td.entry { + white-space: nowrap; + padding-right: 6px; + padding-top: 3px; +} + +.directory td.entry a { + outline: none; +} + +.directory td.entry a img { + border: none; +} + +.directory td.desc { + width: 100%; + padding-left: 6px; + padding-right: 6px; + padding-top: 3px; + border-left: 1px solid rgba(0, 0, 0, 0.05); +} + +.directory tr.odd { + padding-left: 6px; + background-color: var(--index-odd-item-bg-color); +} + +.directory tr.even { + padding-left: 6px; + background-color: var(--index-even-item-bg-color); +} + +.directory img { + vertical-align: -30%; +} + +.directory .levels { + white-space: nowrap; + width: 100%; + text-align: right; + font-size: 9pt; +} + +.directory .levels span { + cursor: pointer; + padding-left: 2px; + padding-right: 2px; + color: var(--page-link-color); +} + +.arrow { + color: var(--nav-background-color); + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: pointer; + font-size: 80%; + display: inline-block; + width: 16px; + height: 14px; + transition: opacity 0.3s ease; +} + +span.arrowhead { + position: relative; + padding: 0; + margin: 0 0 0 2px; + display: inline-block; + width: 5px; + height: 5px; + border-right: 2px solid var(--nav-arrow-color); + border-bottom: 2px solid var(--nav-arrow-color); + transform: rotate(-45deg); + transition: transform 0.3s ease; +} + +span.arrowhead.opened { + transform: rotate(45deg); +} + +.selected span.arrowhead { + border-right: 2px solid var(--nav-arrow-selected-color); + border-bottom: 2px solid var(--nav-arrow-selected-color); +} + +.icon { + font-family: var(--font-family-icon); + line-height: normal; + font-weight: bold; + font-size: 12px; + height: 14px; + width: 16px; + display: inline-block; + background-color: var(--icon-background-color); + color: var(--icon-foreground-color); + text-align: center; + border-radius: 4px; + margin-left: 2px; + margin-right: 2px; +} + +.icona { + width: 24px; + height: 22px; + display: inline-block; +} + +.iconfolder { + width: 24px; + height: 18px; + margin-top: 6px; + vertical-align: top; + display: inline-block; + position: relative; +} + +.icondoc { + width: 24px; + height: 18px; + margin-top: 3px; + vertical-align: top; + display: inline-block; + position: relative; +} + +.folder-icon { + width: 16px; + height: 11px; + background-color: var(--icon-folder-fill-color); + border: 1px solid var(--icon-folder-border-color); + border-radius: 0 2px 2px 2px; + position: relative; + box-sizing: content-box; +} + +.folder-icon::after { + content: ""; + position: absolute; + top: 2px; + left: -1px; + width: 16px; + height: 7px; + background-color: var(--icon-folder-open-fill-color); + border: 1px solid var(--icon-folder-border-color); + border-radius: 7px 7px 2px 2px; + transform-origin: top left; + opacity: 0; + transition: all 0.3s linear; +} + +.folder-icon::before { + content: ""; + position: absolute; + top: -3px; + left: -1px; + width: 6px; + height: 2px; + background-color: var(--icon-folder-fill-color); + border-top: 1px solid var(--icon-folder-border-color); + border-left: 1px solid var(--icon-folder-border-color); + border-right: 1px solid var(--icon-folder-border-color); + border-radius: 2px 2px 0 0; +} + +.folder-icon.open::after { + top: 3px; + opacity: 1; +} + +.doc-icon { + left: 6px; + width: 12px; + height: 16px; + background-color: var(--icon-doc-border-color); + clip-path: polygon(0 0, 66% 0, 100% 25%, 100% 100%, 0 100%); + position: relative; + display: inline-block; +} +.doc-icon::before { + content: ""; + left: 1px; + top: 1px; + width: 10px; + height: 14px; + background-color: var(--icon-doc-fill-color); + clip-path: polygon(0 0, 66% 0, 100% 25%, 100% 100%, 0 100%); + position: absolute; + box-sizing: border-box; +} +.doc-icon::after { + content: ""; + left: 7px; + top: 0px; + width: 3px; + height: 3px; + background-color: transparent; + position: absolute; + border: 1px solid var(--icon-doc-border-color); +} + +/* @end */ + +div.dynheader { + margin-top: 8px; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +span.dynarrow { + position: relative; + display: inline-block; + width: 12px; + bottom: 1px; +} + +address { + font-style: normal; + color: var(--footer-foreground-color); +} + +table.doxtable caption { + caption-side: top; +} + +table.doxtable { + border-collapse: collapse; + margin-top: 4px; + margin-bottom: 4px; +} + +table.doxtable td, +table.doxtable th { + border: 1px solid var(--table-cell-border-color); + padding: 3px 7px 2px; +} + +table.doxtable th { + background-color: var(--table-header-background-color); + color: var(--table-header-foreground-color); + font-size: 110%; + padding-bottom: 4px; + padding-top: 5px; +} + +table.fieldtable { + margin-bottom: 10px; + border: 1px solid var(--memdef-border-color); + border-spacing: 0px; + border-radius: 4px; + box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.15); +} + +.fieldtable td, +.fieldtable th { + padding: 3px 7px 2px; +} + +.fieldtable td.fieldtype, +.fieldtable td.fieldname, +.fieldtable td.fieldinit { + white-space: nowrap; + border-right: 1px solid var(--memdef-border-color); + border-bottom: 1px solid var(--memdef-border-color); + vertical-align: top; +} + +.fieldtable td.fieldname { + padding-top: 3px; +} + +.fieldtable td.fieldinit { + padding-top: 3px; + text-align: right; +} + +.fieldtable td.fielddoc { + border-bottom: 1px solid var(--memdef-border-color); +} + +.fieldtable td.fielddoc p:first-child { + margin-top: 0px; +} + +.fieldtable td.fielddoc p:last-child { + margin-bottom: 2px; +} + +.fieldtable tr:last-child td { + border-bottom: none; +} + +.fieldtable th { + background-color: var(--memdef-title-background-color); + font-size: 90%; + color: var(--memdef-proto-text-color); + padding-bottom: 4px; + padding-top: 5px; + text-align: left; + font-weight: 400; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom: 1px solid var(--memdef-border-color); +} + +/* ----------- navigation breadcrumb styling ----------- */ + +#nav-path ul { + height: 30px; + line-height: 30px; + color: var(--nav-text-normal-color); + overflow: hidden; + margin: 0px; + padding-left: 4px; + background-image: none; + background: var(--page-background-color); + border-bottom: 1px solid var(--nav-breadcrumb-separator-color); + font-size: var(--nav-font-size-level1); + font-family: var(--font-family-nav); + position: relative; + z-index: 100; +} + +#main-nav { + border-bottom: 1px solid var(--nav-border-color); +} + +.navpath li { + list-style-type: none; + float: left; + color: var(--nav-foreground-color); +} + +.navpath li.footer { + list-style-type: none; + float: right; + padding-left: 10px; + padding-right: 15px; + background-image: none; + background-repeat: no-repeat; + background-position: right; + font-size: 8pt; + color: var(--footer-foreground-color); +} + +#nav-path li.navelem { + background-image: none; + display: flex; + align-items: center; + padding-left: 15px; +} + +.navpath li.navelem a { + text-shadow: none; + display: inline-block; + color: var(--nav-breadcrumb-color); + position: relative; + top: 0px; + height: 30px; + margin-right: -20px; +} + +#nav-path li.navelem:after { + content: ""; + display: inline-block; + position: relative; + top: 0; + right: -15px; + width: 30px; + height: 30px; + transform: scaleX(0.5) scale(0.707) rotate(45deg); + z-index: 10; + background: var(--page-background-color); + box-shadow: 2px -2px 0 2px var(--nav-breadcrumb-separator-color); + border-radius: 0 5px 0 50px; +} + +#nav-path li.navelem:first-child { + margin-left: -6px; +} + +#nav-path li.navelem:hover, +#nav-path li.navelem:hover:after { + background-color: var(--nav-breadcrumb-active-bg); +} + +/* ---------------------- */ + +div.summary { + float: right; + font-size: 8pt; + padding-right: 5px; + width: 50%; + text-align: right; +} + +div.summary a { + white-space: nowrap; +} + +table.classindex { + margin: 10px; + white-space: nowrap; + margin-left: 3%; + margin-right: 3%; + width: 94%; + border: 0; + border-spacing: 0; + padding: 0; +} + +div.ingroups { + font-size: 8pt; + width: 50%; + text-align: left; +} + +div.ingroups a { + white-space: nowrap; +} + +div.header { + margin: 0px; + background-color: var(--header-background-color); + border-bottom: 1px solid var(--header-separator-color); +} + +div.headertitle { + padding: 5px 5px 5px 10px; +} + +dl { + padding: 0 0 0 0; +} + +dl.bug dt a, +dl.deprecated dt a, +dl.todo dt a, +dl.test a { + font-weight: bold !important; +} + +dl.warning, +dl.attention, +dl.important, +dl.note, +dl.deprecated, +dl.bug, +dl.invariant, +dl.pre, +dl.post, +dl.todo, +dl.test, +dl.remark { + padding: 10px; + margin: 10px 0px; + overflow: hidden; + margin-left: 0; + border-radius: 4px; +} + +dl.section dd { + margin-bottom: 2px; +} + +dl.warning, +dl.attention, +dl.important { + background: var(--warning-color-bg); + border-left: 8px solid var(--warning-color-hl); + color: var(--warning-color-text); +} + +dl.warning dt, +dl.attention dt, +dl.important dt { + color: var(--warning-color-hl); +} + +dl.note, +dl.remark { + background: var(--note-color-bg); + border-left: 8px solid var(--note-color-hl); + color: var(--note-color-text); +} + +dl.note dt, +dl.remark dt { + color: var(--note-color-hl); +} + +dl.todo { + background: var(--todo-color-bg); + border-left: 8px solid var(--todo-color-hl); + color: var(--todo-color-text); +} + +dl.todo dt { + color: var(--todo-color-hl); +} + +dl.test { + background: var(--test-color-bg); + border-left: 8px solid var(--test-color-hl); + color: var(--test-color-text); +} + +dl.test dt { + color: var(--test-color-hl); +} + +dl.bug dt a { + color: var(--bug-color-hl) !important; +} + +dl.bug { + background: var(--bug-color-bg); + border-left: 8px solid var(--bug-color-hl); + color: var(--bug-color-text); +} + +dl.bug dt a { + color: var(--bug-color-hl) !important; +} + +dl.deprecated { + background: var(--deprecated-color-bg); + border-left: 8px solid var(--deprecated-color-hl); + color: var(--deprecated-color-text); +} + +dl.deprecated dt a { + color: var(--deprecated-color-hl) !important; +} + +dl.note dd, +dl.warning dd, +dl.pre dd, +dl.post dd, +dl.remark dd, +dl.attention dd, +dl.important dd, +dl.invariant dd, +dl.bug dd, +dl.deprecated dd, +dl.todo dd, +dl.test dd { + margin-inline-start: 0px; +} + +dl.invariant, +dl.pre, +dl.post { + background: var(--invariant-color-bg); + border-left: 8px solid var(--invariant-color-hl); + color: var(--invariant-color-text); +} + +dl.invariant dt, +dl.pre dt, +dl.post dt { + color: var(--invariant-color-hl); +} + +#projectrow { + height: 56px; +} + +#projectlogo { + text-align: center; + vertical-align: bottom; + border-collapse: separate; +} + +#projectlogo img { + border: 0px none; +} + +#projectalign { + vertical-align: middle; + padding-left: 0.5em; +} + +#projectname { + font-size: 200%; + font-family: var(--font-family-title); + margin: 0; + padding: 0; +} + +#side-nav #projectname { + font-size: 130%; +} + +#projectbrief { + font-size: 90%; + font-family: var(--font-family-title); + margin: 0px; + padding: 0px; +} + +#projectnumber { + font-size: 50%; + font-family: var(--font-family-title); + margin: 0px; + padding: 0px; +} + +#titlearea { + padding: 0 0 0 5px; + margin: 0px; + border-bottom: 1px solid var(--title-separator-color); + background-color: var(--title-background-color); +} + +.image { + text-align: center; +} + +.dotgraph { + text-align: center; +} + +.mscgraph { + text-align: center; +} + +.plantumlgraph { + text-align: center; +} + +.diagraph { + text-align: center; +} + +.caption { + font-weight: bold; +} + +dl.citelist { + margin-bottom: 50px; +} + +dl.citelist dt { + color: var(--citation-label-color); + float: left; + font-weight: bold; + margin-right: 10px; + padding: 5px; + text-align: right; + width: 52px; +} + +dl.citelist dd { + margin: 2px 0 2px 72px; + padding: 5px 0; +} + +div.toc { + padding: 14px 25px; + background-color: var(--toc-background-color); + border: 1px solid var(--toc-border-color); + border-radius: 7px 7px 7px 7px; + float: right; + height: auto; + margin: 0 8px 10px 10px; + width: 200px; +} + +div.toc li { + background: var(--toc-down-arrow-image) no-repeat scroll 0 5px transparent; + font: 10px/1.2 var(--font-family-toc); + margin-top: 5px; + padding-left: 10px; + padding-top: 2px; +} + +div.toc h3 { + font: bold 12px/1.2 var(--font-family-toc); + color: var(--toc-header-color); + border-bottom: 0 none; + margin: 0; +} + +div.toc ul { + list-style: none outside none; + border: medium none; + padding: 0px; +} + +div.toc li[class^="level"] { + margin-left: 15px; +} + +div.toc li.level1 { + margin-left: 0px; +} + +div.toc li.empty { + background-image: none; + margin-top: 0px; +} + +span.emoji { + /* font family used at the site: https://unicode.org/emoji/charts/full-emoji-list.html + * font-family: "Noto Color Emoji", "Apple Color Emoji", "Segoe UI Emoji", Times, Symbola, Aegyptus, Code2000, Code2001, Code2002, Musica, serif, LastResort; + */ +} + +span.obfuscator { + display: none; +} + +.inherit_header { + font-weight: 400; + cursor: pointer; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.inherit_header td { + padding: 6px 0 2px 0; +} + +.inherit { + display: none; +} + +tr.heading h2 { + margin-top: 12px; + margin-bottom: 12px; +} + +/* tooltip related style info */ + +.ttc { + position: absolute; + display: none; +} + +#powerTip { + cursor: default; + color: var(--tooltip-foreground-color); + background-color: var(--tooltip-background-color); + backdrop-filter: var(--tooltip-backdrop-filter); + -webkit-backdrop-filter: var(--tooltip-backdrop-filter); + border: 1px solid var(--tooltip-border-color); + border-radius: 4px; + box-shadow: var(--tooltip-shadow); + display: none; + font-size: smaller; + max-width: 80%; + padding: 1ex 1em 1em; + position: absolute; + z-index: 2147483647; +} + +#powerTip div.ttdoc { + color: var(--tooltip-doc-color); + font-style: italic; +} + +#powerTip div.ttname a { + font-weight: bold; +} + +#powerTip a { + color: var(--tooltip-link-color); +} + +#powerTip div.ttname { + font-weight: bold; +} + +#powerTip div.ttdeci { + color: var(--tooltip-declaration-color); +} + +#powerTip div { + margin: 0px; + padding: 0px; + font-size: 12px; + font-family: var(--font-family-tooltip); + line-height: 16px; +} + +#powerTip:before, +#powerTip:after { + content: ""; + position: absolute; + margin: 0px; +} + +#powerTip.n:after, +#powerTip.n:before, +#powerTip.s:after, +#powerTip.s:before, +#powerTip.w:after, +#powerTip.w:before, +#powerTip.e:after, +#powerTip.e:before, +#powerTip.ne:after, +#powerTip.ne:before, +#powerTip.se:after, +#powerTip.se:before, +#powerTip.nw:after, +#powerTip.nw:before, +#powerTip.sw:after, +#powerTip.sw:before { + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; +} + +#powerTip.n:after, +#powerTip.s:after, +#powerTip.w:after, +#powerTip.e:after, +#powerTip.nw:after, +#powerTip.ne:after, +#powerTip.sw:after, +#powerTip.se:after { + border-color: rgba(255, 255, 255, 0); +} + +#powerTip.n:before, +#powerTip.s:before, +#powerTip.w:before, +#powerTip.e:before, +#powerTip.nw:before, +#powerTip.ne:before, +#powerTip.sw:before, +#powerTip.se:before { + border-color: rgba(128, 128, 128, 0); +} + +#powerTip.n:after, +#powerTip.n:before, +#powerTip.ne:after, +#powerTip.ne:before, +#powerTip.nw:after, +#powerTip.nw:before { + top: 100%; +} + +#powerTip.n:after, +#powerTip.ne:after, +#powerTip.nw:after { + border-top-color: var(--tooltip-arrow-background-color); + border-width: 10px; + margin: 0px -10px; +} +#powerTip.n:before, +#powerTip.ne:before, +#powerTip.nw:before { + border-top-color: var(--tooltip-border-color); + border-width: 11px; + margin: 0px -11px; +} +#powerTip.n:after, +#powerTip.n:before { + left: 50%; +} + +#powerTip.nw:after, +#powerTip.nw:before { + right: 14px; +} + +#powerTip.ne:after, +#powerTip.ne:before { + left: 14px; +} + +#powerTip.s:after, +#powerTip.s:before, +#powerTip.se:after, +#powerTip.se:before, +#powerTip.sw:after, +#powerTip.sw:before { + bottom: 100%; +} + +#powerTip.s:after, +#powerTip.se:after, +#powerTip.sw:after { + border-bottom-color: var(--tooltip-arrow-background-color); + border-width: 10px; + margin: 0px -10px; +} + +#powerTip.s:before, +#powerTip.se:before, +#powerTip.sw:before { + border-bottom-color: var(--tooltip-border-color); + border-width: 11px; + margin: 0px -11px; +} + +#powerTip.s:after, +#powerTip.s:before { + left: 50%; +} + +#powerTip.sw:after, +#powerTip.sw:before { + right: 14px; +} + +#powerTip.se:after, +#powerTip.se:before { + left: 14px; +} + +#powerTip.e:after, +#powerTip.e:before { + left: 100%; +} +#powerTip.e:after { + border-left-color: var(--tooltip-border-color); + border-width: 10px; + top: 50%; + margin-top: -10px; +} +#powerTip.e:before { + border-left-color: var(--tooltip-border-color); + border-width: 11px; + top: 50%; + margin-top: -11px; +} + +#powerTip.w:after, +#powerTip.w:before { + right: 100%; +} +#powerTip.w:after { + border-right-color: var(--tooltip-border-color); + border-width: 10px; + top: 50%; + margin-top: -10px; +} +#powerTip.w:before { + border-right-color: var(--tooltip-border-color); + border-width: 11px; + top: 50%; + margin-top: -11px; +} + +@media print { + #top { + display: none; + } + #side-nav { + display: none; + } + #nav-path { + display: none; + } + body { + overflow: visible; + } + h1, + h2, + h3, + h4, + h5, + h6 { + page-break-after: avoid; + } + .summary { + display: none; + } + .memitem { + page-break-inside: avoid; + } + #doc-content { + margin-left: 0 !important; + height: auto !important; + width: auto !important; + overflow: inherit; + display: inline; + } +} + +/* @group Markdown */ + +table.markdownTable { + border-collapse: collapse; + margin-top: 4px; + margin-bottom: 4px; +} + +table.markdownTable td, +table.markdownTable th { + border: 1px solid var(--table-cell-border-color); + padding: 3px 7px 2px; +} + +table.markdownTable tr { +} + +th.markdownTableHeadLeft, +th.markdownTableHeadRight, +th.markdownTableHeadCenter, +th.markdownTableHeadNone { + background-color: var(--table-header-background-color); + color: var(--table-header-foreground-color); + font-size: 110%; + padding-bottom: 4px; + padding-top: 5px; +} + +th.markdownTableHeadLeft, +td.markdownTableBodyLeft { + text-align: left; +} + +th.markdownTableHeadRight, +td.markdownTableBodyRight { + text-align: right; +} + +th.markdownTableHeadCenter, +td.markdownTableBodyCenter { + text-align: center; +} + +tt, +code, +kbd { + display: inline-block; +} +tt, +code, +kbd { + vertical-align: top; +} +/* @end */ + +u { + text-decoration: underline; +} + +details > summary { + list-style-type: none; +} + +details > summary::-webkit-details-marker { + display: none; +} + +details > summary::before { + content: "\25ba"; + padding-right: 4px; + font-size: 80%; +} + +details[open] > summary::before { + content: "\25bc"; + padding-right: 4px; + font-size: 80%; +} + +:root { + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb-color) + var(--scrollbar-background-color); +} + +::-webkit-scrollbar { + background-color: var(--scrollbar-background-color); + height: 12px; + width: 12px; +} +::-webkit-scrollbar-thumb { + border-radius: 6px; + box-shadow: inset 0 0 12px 12px var(--scrollbar-thumb-color); + border: solid 2px transparent; +} +::-webkit-scrollbar-corner { + background-color: var(--scrollbar-background-color); +} diff --git a/example/c-vt-key-encode/README.md b/example/c-vt-key-encode/README.md new file mode 100644 index 000000000..05ee3fc31 --- /dev/null +++ b/example/c-vt-key-encode/README.md @@ -0,0 +1,22 @@ +# Example: `ghostty-vt` C Key Encoding + +This example demonstrates how to use the `ghostty-vt` C library to encode key +events into terminal escape sequences. + +This example specifically shows how to: + +1. Create a key encoder with the C API +2. Configure Kitty keyboard protocol flags (this example uses KKP) +3. Create and configure a key event +4. Encode the key event into a terminal escape sequence + +The example encodes a Ctrl key release event with the Ctrl modifier set, +producing the escape sequence `\x1b[57442;5:3u`. + +## Usage + +Run the program: + +```shell-session +zig build run +``` diff --git a/example/c-vt-key-encode/build.zig b/example/c-vt-key-encode/build.zig new file mode 100644 index 000000000..b4b759744 --- /dev/null +++ b/example/c-vt-key-encode/build.zig @@ -0,0 +1,42 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const run_step = b.step("run", "Run the app"); + + const exe_mod = b.createModule(.{ + .target = target, + .optimize = optimize, + }); + exe_mod.addCSourceFiles(.{ + .root = b.path("src"), + .files = &.{"main.c"}, + }); + + // You'll want to use a lazy dependency here so that ghostty is only + // downloaded if you actually need it. + if (b.lazyDependency("ghostty", .{ + // Setting simd to false will force a pure static build that + // doesn't even require libc, but it has a significant performance + // penalty. If your embedding app requires libc anyway, you should + // always keep simd enabled. + // .simd = false, + })) |dep| { + exe_mod.linkLibrary(dep.artifact("ghostty-vt")); + } + + // Exe + const exe = b.addExecutable(.{ + .name = "c_vt_key_encode", + .root_module = exe_mod, + }); + b.installArtifact(exe); + + // Run + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| run_cmd.addArgs(args); + run_step.dependOn(&run_cmd.step); +} diff --git a/example/c-vt-key-encode/build.zig.zon b/example/c-vt-key-encode/build.zig.zon new file mode 100644 index 000000000..5da1a9168 --- /dev/null +++ b/example/c-vt-key-encode/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt, + .version = "0.0.0", + .fingerprint = 0x413a8529b1255f9a, + .minimum_zig_version = "0.15.1", + .dependencies = .{ + // Ghostty dependency. In reality, you'd probably use a URL-based + // dependency like the one showed (and commented out) below this one. + // We use a path dependency here for simplicity and to ensure our + // examples always test against the source they're bundled with. + .ghostty = .{ .path = "../../" }, + + // Example of what a URL-based dependency looks like: + // .ghostty = .{ + // .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz", + // .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s", + // }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/example/c-vt-key-encode/src/main.c b/example/c-vt-key-encode/src/main.c new file mode 100644 index 000000000..82444f99d --- /dev/null +++ b/example/c-vt-key-encode/src/main.c @@ -0,0 +1,59 @@ +#include +#include +#include +#include +#include + +int main() { + GhosttyKeyEncoder encoder; + GhosttyResult result = ghostty_key_encoder_new(NULL, &encoder); + assert(result == GHOSTTY_SUCCESS); + + // Set kitty flags with all features enabled + ghostty_key_encoder_setopt(encoder, GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS, &(uint8_t){GHOSTTY_KITTY_KEY_ALL}); + + // Create key event + GhosttyKeyEvent event; + result = ghostty_key_event_new(NULL, &event); + assert(result == GHOSTTY_SUCCESS); + ghostty_key_event_set_action(event, GHOSTTY_KEY_ACTION_RELEASE); + ghostty_key_event_set_key(event, GHOSTTY_KEY_CONTROL_LEFT); + ghostty_key_event_set_mods(event, GHOSTTY_MODS_CTRL); + printf("Encoding event: left ctrl release with all Kitty flags enabled\n"); + + // Optionally, encode with null buffer to get required size. You can + // skip this step and provide a sufficiently large buffer directly. + // If there isn't enoug hspace, the function will return an out of memory + // error. + size_t required = 0; + result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required); + assert(result == GHOSTTY_OUT_OF_MEMORY); + printf("Required buffer size: %zu bytes\n", required); + + // Encode the key event. We don't use our required size above because + // that was just an example; we know 128 bytes is enough. + char buf[128]; + size_t written = 0; + result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); + assert(result == GHOSTTY_SUCCESS); + printf("Encoded %zu bytes\n", written); + + // Print the encoded sequence (hex and string) + printf("Hex: "); + for (size_t i = 0; i < written; i++) printf("%02x ", (unsigned char)buf[i]); + printf("\n"); + + printf("String: "); + for (size_t i = 0; i < written; i++) { + if (buf[i] == 0x1b) { + printf("\\x1b"); + } else { + printf("%c", buf[i]); + } + } + printf("\n"); + + ghostty_key_event_free(event); + ghostty_key_encoder_free(encoder); + return 0; +} diff --git a/example/c-vt-paste/README.md b/example/c-vt-paste/README.md new file mode 100644 index 000000000..0f911771f --- /dev/null +++ b/example/c-vt-paste/README.md @@ -0,0 +1,17 @@ +# Example: `ghostty-vt` Paste Safety Check + +This contains a simple example of how to use the `ghostty-vt` paste +utilities to check if paste data is safe. + +This uses a `build.zig` and `Zig` to build the C program so that we +can reuse a lot of our build logic and depend directly on our source +tree, but Ghostty emits a standard C library that can be used with any +C tooling. + +## Usage + +Run the program: + +```shell-session +zig build run +``` diff --git a/example/c-vt-paste/build.zig b/example/c-vt-paste/build.zig new file mode 100644 index 000000000..99b7ba771 --- /dev/null +++ b/example/c-vt-paste/build.zig @@ -0,0 +1,42 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const run_step = b.step("run", "Run the app"); + + const exe_mod = b.createModule(.{ + .target = target, + .optimize = optimize, + }); + exe_mod.addCSourceFiles(.{ + .root = b.path("src"), + .files = &.{"main.c"}, + }); + + // You'll want to use a lazy dependency here so that ghostty is only + // downloaded if you actually need it. + if (b.lazyDependency("ghostty", .{ + // Setting simd to false will force a pure static build that + // doesn't even require libc, but it has a significant performance + // penalty. If your embedding app requires libc anyway, you should + // always keep simd enabled. + // .simd = false, + })) |dep| { + exe_mod.linkLibrary(dep.artifact("ghostty-vt")); + } + + // Exe + const exe = b.addExecutable(.{ + .name = "c_vt_paste", + .root_module = exe_mod, + }); + b.installArtifact(exe); + + // Run + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| run_cmd.addArgs(args); + run_step.dependOn(&run_cmd.step); +} diff --git a/example/c-vt-paste/build.zig.zon b/example/c-vt-paste/build.zig.zon new file mode 100644 index 000000000..fb78db9bc --- /dev/null +++ b/example/c-vt-paste/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt_paste, + .version = "0.0.0", + .fingerprint = 0xa105002abbc8cf74, + .minimum_zig_version = "0.15.1", + .dependencies = .{ + // Ghostty dependency. In reality, you'd probably use a URL-based + // dependency like the one showed (and commented out) below this one. + // We use a path dependency here for simplicity and to ensure our + // examples always test against the source they're bundled with. + .ghostty = .{ .path = "../../" }, + + // Example of what a URL-based dependency looks like: + // .ghostty = .{ + // .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz", + // .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s", + // }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/example/c-vt-paste/src/main.c b/example/c-vt-paste/src/main.c new file mode 100644 index 000000000..153861ca9 --- /dev/null +++ b/example/c-vt-paste/src/main.c @@ -0,0 +1,31 @@ +#include +#include +#include + +int main() { + // Test safe paste data + const char *safe_data = "hello world"; + if (ghostty_paste_is_safe(safe_data, strlen(safe_data))) { + printf("'%s' is safe to paste\n", safe_data); + } + + // Test unsafe paste data with newline + const char *unsafe_newline = "rm -rf /\n"; + if (!ghostty_paste_is_safe(unsafe_newline, strlen(unsafe_newline))) { + printf("'%s' is UNSAFE - contains newline\n", unsafe_newline); + } + + // Test unsafe paste data with bracketed paste end sequence + const char *unsafe_escape = "evil\x1b[201~code"; + if (!ghostty_paste_is_safe(unsafe_escape, strlen(unsafe_escape))) { + printf("Data with escape sequence is UNSAFE\n"); + } + + // Test empty data + const char *empty_data = ""; + if (ghostty_paste_is_safe(empty_data, 0)) { + printf("Empty data is safe\n"); + } + + return 0; +} diff --git a/example/c-vt/build.zig.zon b/example/c-vt/build.zig.zon index 3230f440e..5da1a9168 100644 --- a/example/c-vt/build.zig.zon +++ b/example/c-vt/build.zig.zon @@ -2,7 +2,7 @@ .name = .c_vt, .version = "0.0.0", .fingerprint = 0x413a8529b1255f9a, - .minimum_zig_version = "0.14.1", + .minimum_zig_version = "0.15.1", .dependencies = .{ // Ghostty dependency. In reality, you'd probably use a URL-based // dependency like the one showed (and commented out) below this one. diff --git a/example/c-vt/src/main.c b/example/c-vt/src/main.c index 1eaa659d2..b1297d7a7 100644 --- a/example/c-vt/src/main.c +++ b/example/c-vt/src/main.c @@ -1,4 +1,6 @@ #include +#include +#include #include int main() { @@ -6,6 +8,29 @@ int main() { if (ghostty_osc_new(NULL, &parser) != GHOSTTY_SUCCESS) { return 1; } + + // Setup change window title command to change the title to "hello" + ghostty_osc_next(parser, '0'); + ghostty_osc_next(parser, ';'); + const char *title = "hello"; + for (size_t i = 0; i < strlen(title); i++) { + ghostty_osc_next(parser, title[i]); + } + + // End parsing and get command + GhosttyOscCommand command = ghostty_osc_end(parser, 0); + + // Get and print command type + GhosttyOscCommandType type = ghostty_osc_command_type(command); + printf("Command type: %d\n", type); + + // Extract and print the title + if (ghostty_osc_command_data(command, GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR, &title)) { + printf("Extracted title: %s\n", title); + } else { + printf("Failed to extract title\n"); + } + ghostty_osc_free(parser); return 0; } diff --git a/example/zig-vt/build.zig.zon b/example/zig-vt/build.zig.zon index 852e736ca..bc7246de5 100644 --- a/example/zig-vt/build.zig.zon +++ b/example/zig-vt/build.zig.zon @@ -2,7 +2,7 @@ .name = .zig_vt, .version = "0.0.0", .fingerprint = 0x6045575a7a8387e6, - .minimum_zig_version = "0.14.1", + .minimum_zig_version = "0.15.1", .dependencies = .{ // Ghostty dependency. In reality, you'd probably use a URL-based // dependency like the one showed (and commented out) below this one. diff --git a/flake.lock b/flake.lock index bbd4567e3..90b97ed4a 100644 --- a/flake.lock +++ b/flake.lock @@ -36,15 +36,15 @@ }, "nixpkgs": { "locked": { - "lastModified": 1748189127, - "narHash": "sha256-zRDR+EbbeObu4V2X5QCd2Bk5eltfDlCr5yvhBwUT6pY=", - "rev": "7c43f080a7f28b2774f3b3f43234ca11661bf334", + "lastModified": 315532800, + "narHash": "sha256-sV6pJNzFkiPc6j9Bi9JuHBnWdVhtKB/mHgVmMPvDFlk=", + "rev": "82c2e0d6dde50b17ae366d2aa36f224dc19af469", "type": "tarball", - "url": "https://releases.nixos.org/nixos/25.05/nixos-25.05.802491.7c43f080a7f2/nixexprs.tar.xz" + "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre877938.82c2e0d6dde5/nixexprs.tar.xz" }, "original": { "type": "tarball", - "url": "https://channels.nixos.org/nixos-25.05/nixexprs.tar.xz" + "url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz" } }, "nixpkgs_2": { @@ -97,11 +97,11 @@ ] }, "locked": { - "lastModified": 1748261582, - "narHash": "sha256-3i0IL3s18hdDlbsf0/E+5kyPRkZwGPbSFngq5eToiAA=", + "lastModified": 1760401936, + "narHash": "sha256-/zj5GYO5PKhBWGzbHbqT+ehY8EghuABdQ2WGfCwZpCQ=", "owner": "mitchellh", "repo": "zig-overlay", - "rev": "aafb1b093fb838f7a02613b719e85ec912914221", + "rev": "365085b6652259753b598d43b723858184980bbe", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 7d72a9af3..85550b989 100644 --- a/flake.nix +++ b/flake.nix @@ -5,7 +5,9 @@ # We want to stay as up to date as possible but need to be careful that the # glibc versions used by our dependencies from Nix are compatible with the # system glibc that the user is building for. - nixpkgs.url = "https://channels.nixos.org/nixos-25.05/nixexprs.tar.xz"; + # + # We are currently on unstable to get Zig 0.15 for our package.nix + nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"; flake-utils.url = "github:numtide/flake-utils"; # Used for shell.nix @@ -47,7 +49,7 @@ pkgs = nixpkgs.legacyPackages.${system}; in { devShell.${system} = pkgs.callPackage ./nix/devShell.nix { - zig = zig.packages.${system}."0.14.1"; + zig = zig.packages.${system}."0.15.2"; wraptest = pkgs.callPackage ./nix/wraptest.nix {}; zon2nix = zon2nix; }; diff --git a/flatpak/dependencies.yml b/flatpak/dependencies.yml index 082107923..87512a547 100644 --- a/flatpak/dependencies.yml +++ b/flatpak/dependencies.yml @@ -13,12 +13,12 @@ modules: - chmod a+x /app/zig/zig sources: - type: archive - sha256: 24aeeec8af16c381934a6cd7d95c807a8cb2cf7df9fa40d359aa884195c4716c - url: https://ziglang.org/download/0.14.1/zig-x86_64-linux-0.14.1.tar.xz + sha256: 02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239 + url: https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz only-arches: [x86_64] - type: archive - sha256: f7a654acc967864f7a050ddacfaa778c7504a0eca8d2b678839c21eea47c992b - url: https://ziglang.org/download/0.14.1/zig-aarch64-linux-0.14.1.tar.xz + sha256: 958ed7d1e00d0ea76590d27666efbf7a932281b3d7ba0c6b01b0ff26498f667f + url: https://ziglang.org/download/0.15.2/zig-aarch64-linux-0.15.2.tar.xz only-arches: [aarch64] - name: bzip2-redirect diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 46f6c950d..d762d82c1 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -31,7 +31,7 @@ }, { "type": "archive", - "url": "https://github.com/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst", + "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" }, @@ -61,9 +61,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz", - "dest": "vendor/p/N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3", - "sha256": "24f63d339d1dfe7eab1b35add1a419214ec804c5abbb6200a9ef55bb5c7908cc" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/ghostty-themes.tgz", + "dest": "vendor/p/N-V-__8AAPk1AwDvSIEm8ftVzPLJr-Uuzz65Ss2R4ljTXjSq", + "sha256": "bc59fa32247cb55906ca3495ee9cf89389acbe24828c61ca336494f38949a7f9" }, { "type": "archive", @@ -79,9 +79,9 @@ }, { "type": "archive", - "url": "https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz", - "dest": "vendor/p/libxev-0.0.0-86vtc2UaEwDfiTKX3iBI-s_hdzfzWQUarT3MUrmUQl-Q", - "sha256": "29aa3360a121853ffab089de7fbffc3bfeb42c304937ef1099d2ee358d469267" + "url": "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz", + "dest": "vendor/p/libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs", + "sha256": "6003ea6b96e4a518a128f932327d79a11bd30996b13b73baeb29916379487dd7" }, { "type": "archive", @@ -133,9 +133,21 @@ }, { "type": "git", - "url": "https://github.com/rockorager/libvaxis", - "commit": "1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23", - "dest": "vendor/p/vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn" + "url": "https://github.com/jacobsandlund/uucode", + "commit": "5f05f8f83a75caea201f12cc8ea32a2d82ea9732", + "dest": "vendor/p/uucode-0.1.0-ZZjBPj96QADXyt5sqwBJUnhaDYs_qBeeKijZvlRa0eqM" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", + "dest": "vendor/p/uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT", + "sha256": "56899260e17c7d12706fff06b551bf42a47a73de734a442de3ae3d0bfe0a5c07" + }, + { + "type": "archive", + "url": "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz", + "dest": "vendor/p/vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS", + "sha256": "2e72332bc89c5b541ec6e6bd48769e1f3fb757c4006f3d1af940b54f9b088ef6" }, { "type": "archive", @@ -157,51 +169,39 @@ }, { "type": "archive", - "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz", - "dest": "vendor/p/z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP", - "sha256": "d036c3292600d5e8e1571fd66ce9304e00f9ecf35115c9d1be2a8187cc693d9d" + "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" }, { "type": "archive", - "url": "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz", - "dest": "vendor/p/zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9", - "sha256": "de7ba535077fe2b678a5a7972585f002588d37244db08397feadf3d4907c0bb2" - }, - { - "type": "git", - "url": "https://codeberg.org/atman/zg", - "commit": "4a002763419a34d61dcbb1f415821b83b9bf8ddc", - "dest": "vendor/p/zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM" + "url": "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz", + "dest": "vendor/p/zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh", + "sha256": "3b015d928af04e9e26272bc15eb4dbb4d9a9d469eb6d290a0ddae673b77c4568" }, { "type": "archive", - "url": "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz", - "dest": "vendor/p/N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ", - "sha256": "7f235e0956c2f5401a28963a261019953d00e3bf4cfc029830f2161196c3583d" + "url": "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz", + "dest": "vendor/p/zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi", + "sha256": "4c2018e56015d39504b8090386ad9ce9393f38380085d9c32373bf7e56fc73a3" }, { "type": "archive", - "url": "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz", - "dest": "vendor/p/zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk", - "sha256": "a37be5eea7e44a2d1b2976ba256b85f76a8c1063fc01ffec85c8a9e67468e4dc" + "url": "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz", + "dest": "vendor/p/zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK", + "sha256": "dd84af737625356fcd722cb30909f3b2e8d702667cf579714aa7eabc0ac08ecc" }, { "type": "archive", - "url": "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz", - "dest": "vendor/p/wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy", - "sha256": "13bec6675e403d86db3b55b39ae262f1e1bdfe24056dcd82824341c6308b5219" - }, - { - "type": "git", - "url": "https://github.com/TUSF/zigimg", - "commit": "31268548fe3276c0e95f318a6c0d2ab10565b58d", - "dest": "vendor/p/zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj" + "url": "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz", + "dest": "vendor/p/wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe", + "sha256": "4f146b735ed0d527f520e3bf71d3e93f72c3d0fa583ae8edd3a4851f7079124e" }, { "type": "archive", - "url": "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz", - "dest": "vendor/p/ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf", - "sha256": "72c7bdf3e16df105235fe3fcf32c987dac49389190f4ced89b0ee31710f3f3d9" + "url": "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz", + "dest": "vendor/p/zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms", + "sha256": "2c1ed76ba2b35514544b0c27c9633ecba7c31be9080e37e7a010c93b5a1bc553" }, { "type": "archive", diff --git a/include/ghostty.h b/include/ghostty.h index 7888b380c..acb6988d6 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -353,6 +353,7 @@ typedef struct { typedef struct { const char* ptr; uintptr_t len; + bool sentinel; } ghostty_string_s; typedef struct { @@ -732,6 +733,21 @@ typedef struct { int8_t progress; } ghostty_action_progress_report_s; +// apprt.action.CommandFinished.C +typedef struct { + // -1 if no exit code was reported, otherwise 0-255 + int16_t exit_code; + // number of nanoseconds that command was running for + uint64_t duration; +} ghostty_action_command_finished_s; + +// terminal.Scrollbar +typedef struct { + uint64_t total; + uint64_t offset; + uint64_t len; +} ghostty_action_scrollbar_s; + // apprt.Action.Key typedef enum { GHOSTTY_ACTION_QUIT, @@ -758,6 +774,7 @@ typedef enum { GHOSTTY_ACTION_RESET_WINDOW_SIZE, GHOSTTY_ACTION_INITIAL_SIZE, GHOSTTY_ACTION_CELL_SIZE, + GHOSTTY_ACTION_SCROLLBAR, GHOSTTY_ACTION_RENDER, GHOSTTY_ACTION_INSPECTOR, GHOSTTY_ACTION_SHOW_GTK_INSPECTOR, @@ -787,6 +804,7 @@ typedef enum { GHOSTTY_ACTION_SHOW_CHILD_EXITED, GHOSTTY_ACTION_PROGRESS_REPORT, GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD, + GHOSTTY_ACTION_COMMAND_FINISHED, } ghostty_action_tag_e; typedef union { @@ -799,6 +817,7 @@ typedef union { ghostty_action_size_limit_s size_limit; ghostty_action_initial_size_s initial_size; ghostty_action_cell_size_s cell_size; + ghostty_action_scrollbar_s scrollbar; ghostty_action_inspector_e inspector; ghostty_action_desktop_notification_s desktop_notification; ghostty_action_set_title_s set_title; @@ -818,6 +837,7 @@ typedef union { ghostty_action_close_tab_mode_e close_tab_mode; ghostty_surface_message_childexited_s child_exited; ghostty_action_progress_report_s progress_report; + ghostty_action_command_finished_s command_finished; } ghostty_action_u; typedef struct { diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 12ed2d015..cd357f0fa 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -1,7 +1,7 @@ /** * @file vt.h * - * libghostty-vt - Virtual terminal sequence parsing library + * libghostty-vt - Virtual terminal emulator library * * This library provides functionality for parsing and handling terminal * escape sequences as well as maintaining terminal state such as styles, @@ -11,6 +11,52 @@ * stable and is definitely going to change. */ +/** + * @mainpage libghostty-vt - Virtual Terminal Emulator Library + * + * libghostty-vt is a C library which implements a modern terminal emulator, + * extracted from the [Ghostty](https://ghostty.org) terminal emulator. + * + * libghostty-vt contains the logic for handling the core parts of a terminal + * emulator: parsing terminal escape sequences, maintaining terminal state, + * encoding input events, etc. It can handle scrollback, line wrapping, + * reflow on resize, and more. + * + * @warning This library is currently in development and the API is not yet stable. + * Breaking changes are expected in future versions. Use with caution in production code. + * + * @section groups_sec API Reference + * + * The API is organized into the following groups: + * - @ref key "Key Encoding" - Encode key events into terminal sequences + * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences + * - @ref paste "Paste Utilities" - Validate paste data safety + * - @ref allocator "Memory Management" - Memory management and custom allocators + * + * @section examples_sec Examples + * + * Complete working examples: + * - @ref c-vt/src/main.c - OSC parser example + * - @ref c-vt-key-encode/src/main.c - Key encoding example + * - @ref c-vt-paste/src/main.c - Paste safety check example + * + */ + +/** @example c-vt/src/main.c + * This example demonstrates how to use the OSC parser to parse an OSC sequence, + * extract command information, and retrieve command-specific data like window titles. + */ + +/** @example c-vt-key-encode/src/main.c + * This example demonstrates how to use the key encoder to convert key events + * into terminal escape sequences using the Kitty keyboard protocol. + */ + +/** @example c-vt-paste/src/main.c + * This example demonstrates how to use the paste utilities to check if + * paste data is safe before sending it to the terminal. + */ + #ifndef GHOSTTY_VT_H #define GHOSTTY_VT_H @@ -18,201 +64,11 @@ extern "C" { #endif -#include -#include -#include - -//------------------------------------------------------------------- -// Types - -/** - * Opaque handle to an OSC parser instance. - * - * This handle represents an OSC (Operating System Command) parser that can - * be used to parse the contents of OSC sequences. This isn't a full VT - * parser; it is only the OSC parser component. This is useful if you have - * a parser already and want to only extract and handle OSC sequences. - */ -typedef struct GhosttyOscParser *GhosttyOscParser; - -/** - * Result codes for libghostty-vt operations. - */ -typedef enum { - /** Operation completed successfully */ - GHOSTTY_SUCCESS = 0, - /** Operation failed due to failed allocation */ - GHOSTTY_OUT_OF_MEMORY = -1, -} GhosttyResult; - -//------------------------------------------------------------------- -// Allocator Interface - -/** - * Function table for custom memory allocator operations. - * - * This vtable defines the interface for a custom memory allocator. All - * function pointers must be valid and non-NULL. - * - * If you're not going to use a custom allocator, you can ignore all of - * this. All functions that take an allocator pointer allow NULL to use a - * default allocator. - * - * The interface is based on the Zig allocator interface. I'll say up front - * that it is easy to look at this interface and think "wow, this is really - * overcomplicated". The reason for this complexity is well thought out by - * the Zig folks, and it enables a diverse set of allocation strategies - * as shown by the Zig ecosystem. As a consolation, please note that many - * of the arguments are only needed for advanced use cases and can be - * safely ignored in simple implementations. For example, if you look at - * the Zig implementation of the libc allocator in `lib/std/heap.zig` - * (search for CAllocator), you'll see it is very simple. - * - * We chose to align with the Zig allocator interface because: - * - * 1. It is a proven interface that serves a wide variety of use cases - * in the real world via the Zig ecosystem. It's shown to work. - * - * 2. Our core implementation itself is Zig, and this lets us very - * cheaply and easily convert between C and Zig allocators. - * - * NOTE(mitchellh): In the future, we can have default implementations of - * resize/remap and allow those to be null. - */ -typedef struct { - /** - * Return a pointer to `len` bytes with specified `alignment`, or return - * `NULL` indicating the allocation failed. - * - * @param ctx The allocator context - * @param len Number of bytes to allocate - * @param alignment Required alignment for the allocation. Guaranteed to - * be a power of two between 1 and 16 inclusive. - * @param ret_addr First return address of the allocation call stack (0 if not provided) - * @return Pointer to allocated memory, or NULL if allocation failed - */ - void* (*alloc)(void *ctx, size_t len, uint8_t alignment, uintptr_t ret_addr); - - /** - * Attempt to expand or shrink memory in place. - * - * `memory_len` must equal the length requested from the most recent - * successful call to `alloc`, `resize`, or `remap`. `alignment` must - * equal the same value that was passed as the `alignment` parameter to - * the original `alloc` call. - * - * `new_len` must be greater than zero. - * - * @param ctx The allocator context - * @param memory Pointer to the memory block to resize - * @param memory_len Current size of the memory block - * @param alignment Alignment (must match original allocation) - * @param new_len New requested size - * @param ret_addr First return address of the allocation call stack (0 if not provided) - * @return true if resize was successful in-place, false if relocation would be required - */ - bool (*resize)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr); - - /** - * Attempt to expand or shrink memory, allowing relocation. - * - * `memory_len` must equal the length requested from the most recent - * successful call to `alloc`, `resize`, or `remap`. `alignment` must - * equal the same value that was passed as the `alignment` parameter to - * the original `alloc` call. - * - * A non-`NULL` return value indicates the resize was successful. The - * allocation may have same address, or may have been relocated. In either - * case, the allocation now has size of `new_len`. A `NULL` return value - * indicates that the resize would be equivalent to allocating new memory, - * copying the bytes from the old memory, and then freeing the old memory. - * In such case, it is more efficient for the caller to perform the copy. - * - * `new_len` must be greater than zero. - * - * @param ctx The allocator context - * @param memory Pointer to the memory block to remap - * @param memory_len Current size of the memory block - * @param alignment Alignment (must match original allocation) - * @param new_len New requested size - * @param ret_addr First return address of the allocation call stack (0 if not provided) - * @return Pointer to resized memory (may be relocated), or NULL if manual copy is needed - */ - void* (*remap)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr); - - /** - * Free and invalidate a region of memory. - * - * `memory_len` must equal the length requested from the most recent - * successful call to `alloc`, `resize`, or `remap`. `alignment` must - * equal the same value that was passed as the `alignment` parameter to - * the original `alloc` call. - * - * @param ctx The allocator context - * @param memory Pointer to the memory block to free - * @param memory_len Size of the memory block - * @param alignment Alignment (must match original allocation) - * @param ret_addr First return address of the allocation call stack (0 if not provided) - */ - void (*free)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, uintptr_t ret_addr); -} GhosttyAllocatorVtable; - -/** - * Custom memory allocator. - * - * For functions that take an allocator pointer, a NULL pointer indicates - * that the default allocator should be used. The default allocator will - * be libc malloc/free if we're linking to libc. If libc isn't linked, - * a custom allocator is used (currently Zig's SMP allocator). - * - * Usage example: - * @code - * GhosttyAllocator allocator = { - * .vtable = &my_allocator_vtable, - * .ctx = my_allocator_state - * }; - * @endcode - */ -typedef struct { - /** - * Opaque context pointer passed to all vtable functions. - * This allows the allocator implementation to maintain state - * or reference external resources needed for memory management. - */ - void *ctx; - - /** - * Pointer to the allocator's vtable containing function pointers - * for memory operations (alloc, resize, remap, free). - */ - const GhosttyAllocatorVtable *vtable; -} GhosttyAllocator; - -//------------------------------------------------------------------- -// Functions - -/** - * Create a new OSC parser instance. - * - * Creates a new OSC (Operating System Command) parser using the provided - * allocator. The parser must be freed using ghostty_vt_osc_free() when - * no longer needed. - * - * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator - * @param parser Pointer to store the created parser handle - * @return GHOSTTY_SUCCESS on success, or an error code on failure - */ -GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser); - -/** - * Free an OSC parser instance. - * - * Releases all resources associated with the OSC parser. After this call, - * the parser handle becomes invalid and must not be used. - * - * @param parser The parser handle to free (may be NULL) - */ -void ghostty_osc_free(GhosttyOscParser parser); +#include +#include +#include +#include +#include #ifdef __cplusplus } diff --git a/include/ghostty/vt/allocator.h b/include/ghostty/vt/allocator.h new file mode 100644 index 000000000..4cebe91bb --- /dev/null +++ b/include/ghostty/vt/allocator.h @@ -0,0 +1,196 @@ +/** + * @file allocator.h + * + * Memory management interface for libghostty-vt. + */ + +#ifndef GHOSTTY_VT_ALLOCATOR_H +#define GHOSTTY_VT_ALLOCATOR_H + +#include +#include +#include + +/** @defgroup allocator Memory Management + * + * libghostty-vt does require memory allocation for various operations, + * but is resilient to allocation failures and will gracefully handle + * out-of-memory situations by returning error codes. + * + * The exact memory management semantics are documented in the relevant + * functions and data structures. + * + * libghostty-vt uses explicit memory allocation via an allocator + * interface provided by GhosttyAllocator. The interface is based on the + * [Zig](https://ziglang.org) allocator interface, since this has been + * shown to be a flexible and powerful interface in practice and enables + * a wide variety of allocation strategies. + * + * **For the common case, you can pass NULL as the allocator for any + * function that accepts one,** and libghostty will use a default allocator. + * The default allocator will be libc malloc/free if libc is linked. + * Otherwise, a custom allocator is used (currently Zig's SMP allocator) + * that doesn't require any external dependencies. + * + * ## Basic Usage + * + * For simple use cases, you can ignore this interface entirely by passing NULL + * as the allocator parameter to functions that accept one. This will use the + * default allocator (typically libc malloc/free, if libc is linked, but + * we provide our own default allocator if libc isn't linked). + * + * To use a custom allocator: + * 1. Implement the GhosttyAllocatorVtable function pointers + * 2. Create a GhosttyAllocator struct with your vtable and context + * 3. Pass the allocator to functions that accept one + * + * @{ + */ + +/** + * Function table for custom memory allocator operations. + * + * This vtable defines the interface for a custom memory allocator. All + * function pointers must be valid and non-NULL. + * + * @ingroup allocator + * + * If you're not going to use a custom allocator, you can ignore all of + * this. All functions that take an allocator pointer allow NULL to use a + * default allocator. + * + * The interface is based on the Zig allocator interface. I'll say up front + * that it is easy to look at this interface and think "wow, this is really + * overcomplicated". The reason for this complexity is well thought out by + * the Zig folks, and it enables a diverse set of allocation strategies + * as shown by the Zig ecosystem. As a consolation, please note that many + * of the arguments are only needed for advanced use cases and can be + * safely ignored in simple implementations. For example, if you look at + * the Zig implementation of the libc allocator in `lib/std/heap.zig` + * (search for CAllocator), you'll see it is very simple. + * + * We chose to align with the Zig allocator interface because: + * + * 1. It is a proven interface that serves a wide variety of use cases + * in the real world via the Zig ecosystem. It's shown to work. + * + * 2. Our core implementation itself is Zig, and this lets us very + * cheaply and easily convert between C and Zig allocators. + * + * NOTE(mitchellh): In the future, we can have default implementations of + * resize/remap and allow those to be null. + */ +typedef struct { + /** + * Return a pointer to `len` bytes with specified `alignment`, or return + * `NULL` indicating the allocation failed. + * + * @param ctx The allocator context + * @param len Number of bytes to allocate + * @param alignment Required alignment for the allocation. Guaranteed to + * be a power of two between 1 and 16 inclusive. + * @param ret_addr First return address of the allocation call stack (0 if not provided) + * @return Pointer to allocated memory, or NULL if allocation failed + */ + void* (*alloc)(void *ctx, size_t len, uint8_t alignment, uintptr_t ret_addr); + + /** + * Attempt to expand or shrink memory in place. + * + * `memory_len` must equal the length requested from the most recent + * successful call to `alloc`, `resize`, or `remap`. `alignment` must + * equal the same value that was passed as the `alignment` parameter to + * the original `alloc` call. + * + * `new_len` must be greater than zero. + * + * @param ctx The allocator context + * @param memory Pointer to the memory block to resize + * @param memory_len Current size of the memory block + * @param alignment Alignment (must match original allocation) + * @param new_len New requested size + * @param ret_addr First return address of the allocation call stack (0 if not provided) + * @return true if resize was successful in-place, false if relocation would be required + */ + bool (*resize)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr); + + /** + * Attempt to expand or shrink memory, allowing relocation. + * + * `memory_len` must equal the length requested from the most recent + * successful call to `alloc`, `resize`, or `remap`. `alignment` must + * equal the same value that was passed as the `alignment` parameter to + * the original `alloc` call. + * + * A non-`NULL` return value indicates the resize was successful. The + * allocation may have same address, or may have been relocated. In either + * case, the allocation now has size of `new_len`. A `NULL` return value + * indicates that the resize would be equivalent to allocating new memory, + * copying the bytes from the old memory, and then freeing the old memory. + * In such case, it is more efficient for the caller to perform the copy. + * + * `new_len` must be greater than zero. + * + * @param ctx The allocator context + * @param memory Pointer to the memory block to remap + * @param memory_len Current size of the memory block + * @param alignment Alignment (must match original allocation) + * @param new_len New requested size + * @param ret_addr First return address of the allocation call stack (0 if not provided) + * @return Pointer to resized memory (may be relocated), or NULL if manual copy is needed + */ + void* (*remap)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr); + + /** + * Free and invalidate a region of memory. + * + * `memory_len` must equal the length requested from the most recent + * successful call to `alloc`, `resize`, or `remap`. `alignment` must + * equal the same value that was passed as the `alignment` parameter to + * the original `alloc` call. + * + * @param ctx The allocator context + * @param memory Pointer to the memory block to free + * @param memory_len Size of the memory block + * @param alignment Alignment (must match original allocation) + * @param ret_addr First return address of the allocation call stack (0 if not provided) + */ + void (*free)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, uintptr_t ret_addr); +} GhosttyAllocatorVtable; + +/** + * Custom memory allocator. + * + * For functions that take an allocator pointer, a NULL pointer indicates + * that the default allocator should be used. The default allocator will + * be libc malloc/free if we're linking to libc. If libc isn't linked, + * a custom allocator is used (currently Zig's SMP allocator). + * + * @ingroup allocator + * + * Usage example: + * @code + * GhosttyAllocator allocator = { + * .vtable = &my_allocator_vtable, + * .ctx = my_allocator_state + * }; + * @endcode + */ +typedef struct GhosttyAllocator { + /** + * Opaque context pointer passed to all vtable functions. + * This allows the allocator implementation to maintain state + * or reference external resources needed for memory management. + */ + void *ctx; + + /** + * Pointer to the allocator's vtable containing function pointers + * for memory operations (alloc, resize, remap, free). + */ + const GhosttyAllocatorVtable *vtable; +} GhosttyAllocator; + +/** @} */ + +#endif /* GHOSTTY_VT_ALLOCATOR_H */ diff --git a/include/ghostty/vt/key.h b/include/ghostty/vt/key.h new file mode 100644 index 000000000..772b5d43b --- /dev/null +++ b/include/ghostty/vt/key.h @@ -0,0 +1,80 @@ +/** + * @file key.h + * + * Key encoding module - encode key events into terminal escape sequences. + */ + +#ifndef GHOSTTY_VT_KEY_H +#define GHOSTTY_VT_KEY_H + +/** @defgroup key Key Encoding + * + * Utilities for encoding key events into terminal escape sequences, + * supporting both legacy encoding as well as Kitty Keyboard Protocol. + * + * ## Basic Usage + * + * 1. Create an encoder instance with ghostty_key_encoder_new() + * 2. Configure encoder options with ghostty_key_encoder_setopt(). + * 3. For each key event: + * - Create a key event with ghostty_key_event_new() + * - Set event properties (action, key, modifiers, etc.) + * - Encode with ghostty_key_encoder_encode() + * - Free the event with ghostty_key_event_free() + * - Note: You can also reuse the same key event multiple times by + * changing its properties. + * 4. Free the encoder with ghostty_key_encoder_free() when done + * + * ## Example + * + * @code{.c} + * #include + * #include + * #include + * + * int main() { + * // Create encoder + * GhosttyKeyEncoder encoder; + * GhosttyResult result = ghostty_key_encoder_new(NULL, &encoder); + * assert(result == GHOSTTY_SUCCESS); + * + * // Enable Kitty keyboard protocol with all features + * ghostty_key_encoder_setopt(encoder, GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS, + * &(uint8_t){GHOSTTY_KITTY_KEY_ALL}); + * + * // Create and configure key event for Ctrl+C press + * GhosttyKeyEvent event; + * result = ghostty_key_event_new(NULL, &event); + * assert(result == GHOSTTY_SUCCESS); + * ghostty_key_event_set_action(event, GHOSTTY_KEY_ACTION_PRESS); + * ghostty_key_event_set_key(event, GHOSTTY_KEY_C); + * ghostty_key_event_set_mods(event, GHOSTTY_MODS_CTRL); + * + * // Encode the key event + * char buf[128]; + * size_t written = 0; + * result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); + * assert(result == GHOSTTY_SUCCESS); + * + * // Use the encoded sequence (e.g., write to terminal) + * fwrite(buf, 1, written, stdout); + * + * // Cleanup + * ghostty_key_event_free(event); + * ghostty_key_encoder_free(encoder); + * return 0; + * } + * @endcode + * + * For a complete working example, see example/c-vt-key-encode in the + * repository. + * + * @{ + */ + +#include +#include + +/** @} */ + +#endif /* GHOSTTY_VT_KEY_H */ diff --git a/include/ghostty/vt/key/encoder.h b/include/ghostty/vt/key/encoder.h new file mode 100644 index 000000000..766a29427 --- /dev/null +++ b/include/ghostty/vt/key/encoder.h @@ -0,0 +1,221 @@ +/** + * @file encoder.h + * + * Key event encoding to terminal escape sequences. + */ + +#ifndef GHOSTTY_VT_KEY_ENCODER_H +#define GHOSTTY_VT_KEY_ENCODER_H + +#include +#include +#include +#include +#include + +/** + * Opaque handle to a key encoder instance. + * + * This handle represents a key encoder that converts key events into terminal + * escape sequences. + * + * @ingroup key + */ +typedef struct GhosttyKeyEncoder *GhosttyKeyEncoder; + +/** + * Kitty keyboard protocol flags. + * + * Bitflags representing the various modes of the Kitty keyboard protocol. + * These can be combined using bitwise OR operations. Valid values all + * start with `GHOSTTY_KITTY_KEY_`. + * + * @ingroup key + */ +typedef uint8_t GhosttyKittyKeyFlags; + +/** Kitty keyboard protocol disabled (all flags off) */ +#define GHOSTTY_KITTY_KEY_DISABLED 0 + +/** Disambiguate escape codes */ +#define GHOSTTY_KITTY_KEY_DISAMBIGUATE (1 << 0) + +/** Report key press and release events */ +#define GHOSTTY_KITTY_KEY_REPORT_EVENTS (1 << 1) + +/** Report alternate key codes */ +#define GHOSTTY_KITTY_KEY_REPORT_ALTERNATES (1 << 2) + +/** Report all key events including those normally handled by the terminal */ +#define GHOSTTY_KITTY_KEY_REPORT_ALL (1 << 3) + +/** Report associated text with key events */ +#define GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED (1 << 4) + +/** All Kitty keyboard protocol flags enabled */ +#define GHOSTTY_KITTY_KEY_ALL (GHOSTTY_KITTY_KEY_DISAMBIGUATE | GHOSTTY_KITTY_KEY_REPORT_EVENTS | GHOSTTY_KITTY_KEY_REPORT_ALTERNATES | GHOSTTY_KITTY_KEY_REPORT_ALL | GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED) + +/** + * macOS option key behavior. + * + * Determines whether the "option" key on macOS is treated as "alt" or not. + * See the Ghostty `macos-option-as-alt` configuration option for more details. + * + * @ingroup key + */ +typedef enum { + /** Option key is not treated as alt */ + GHOSTTY_OPTION_AS_ALT_FALSE = 0, + /** Option key is treated as alt */ + GHOSTTY_OPTION_AS_ALT_TRUE = 1, + /** Only left option key is treated as alt */ + GHOSTTY_OPTION_AS_ALT_LEFT = 2, + /** Only right option key is treated as alt */ + GHOSTTY_OPTION_AS_ALT_RIGHT = 3, +} GhosttyOptionAsAlt; + +/** + * Key encoder option identifiers. + * + * These values are used with ghostty_key_encoder_setopt() to configure + * the behavior of the key encoder. + * + * @ingroup key + */ +typedef enum { + /** Terminal DEC mode 1: cursor key application mode (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_CURSOR_KEY_APPLICATION = 0, + + /** Terminal DEC mode 66: keypad key application mode (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_KEYPAD_KEY_APPLICATION = 1, + + /** Terminal DEC mode 1035: ignore keypad with numlock (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_IGNORE_KEYPAD_WITH_NUMLOCK = 2, + + /** Terminal DEC mode 1036: alt sends escape prefix (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_ALT_ESC_PREFIX = 3, + + /** xterm modifyOtherKeys mode 2 (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_MODIFY_OTHER_KEYS_STATE_2 = 4, + + /** Kitty keyboard protocol flags (value: GhosttyKittyKeyFlags bitmask) */ + GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS = 5, + + /** macOS option-as-alt setting (value: GhosttyOptionAsAlt) */ + GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT = 6, +} GhosttyKeyEncoderOption; + +/** + * Create a new key encoder instance. + * + * Creates a new key encoder with default options. The encoder can be configured + * using ghostty_key_encoder_setopt() and must be freed using + * ghostty_key_encoder_free() when no longer needed. + * + * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator + * @param encoder Pointer to store the created encoder handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup key + */ +GhosttyResult ghostty_key_encoder_new(const GhosttyAllocator *allocator, GhosttyKeyEncoder *encoder); + +/** + * Free a key encoder instance. + * + * Releases all resources associated with the key encoder. After this call, + * the encoder handle becomes invalid and must not be used. + * + * @param encoder The encoder handle to free (may be NULL) + * + * @ingroup key + */ +void ghostty_key_encoder_free(GhosttyKeyEncoder encoder); + +/** + * Set an option on the key encoder. + * + * Configures the behavior of the key encoder. Options control various aspects + * of encoding such as terminal modes (cursor key application mode, keypad mode), + * protocol selection (Kitty keyboard protocol flags), and platform-specific + * behaviors (macOS option-as-alt). + * + * A null pointer value does nothing. It does not reset the value to the + * default. The setopt call will do nothing. + * + * @param encoder The encoder handle, must not be NULL + * @param option The option to set + * @param value Pointer to the value to set (type depends on the option) + * + * @ingroup key + */ +void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOption option, const void *value); + +/** + * Encode a key event into a terminal escape sequence. + * + * Converts a key event into the appropriate terminal escape sequence based on + * the encoder's current options. The sequence is written to the provided buffer. + * + * Not all key events produce output. For example, unmodified modifier keys + * typically don't generate escape sequences. Check the out_len parameter to + * determine if any data was written. + * + * If the output buffer is too small, this function returns GHOSTTY_OUT_OF_MEMORY + * and out_len will contain the required buffer size. The caller can then + * allocate a larger buffer and call the function again. + * + * @param encoder The encoder handle, must not be NULL + * @param event The key event to encode, must not be NULL + * @param out_buf Buffer to write the encoded sequence to + * @param out_buf_size Size of the output buffer in bytes + * @param out_len Pointer to store the number of bytes written (may be NULL) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY if buffer too small, or other error code + * + * ## Example: Calculate required buffer size + * + * @code{.c} + * // Query the required size with a NULL buffer (always returns OUT_OF_MEMORY) + * size_t required = 0; + * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required); + * assert(result == GHOSTTY_OUT_OF_MEMORY); + * + * // Allocate buffer of required size + * char *buf = malloc(required); + * + * // Encode with properly sized buffer + * size_t written = 0; + * result = ghostty_key_encoder_encode(encoder, event, buf, required, &written); + * assert(result == GHOSTTY_SUCCESS); + * + * // Use the encoded sequence... + * + * free(buf); + * @endcode + * + * ## Example: Direct encoding with static buffer + * + * @code{.c} + * // Most escape sequences are short, so a static buffer often suffices + * char buf[128]; + * size_t written = 0; + * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); + * + * if (result == GHOSTTY_SUCCESS) { + * // Write the encoded sequence to the terminal + * write(pty_fd, buf, written); + * } else if (result == GHOSTTY_OUT_OF_MEMORY) { + * // Buffer too small, written contains required size + * char *dynamic_buf = malloc(written); + * result = ghostty_key_encoder_encode(encoder, event, dynamic_buf, written, &written); + * assert(result == GHOSTTY_SUCCESS); + * write(pty_fd, dynamic_buf, written); + * free(dynamic_buf); + * } + * @endcode + * + * @ingroup key + */ +GhosttyResult ghostty_key_encoder_encode(GhosttyKeyEncoder encoder, GhosttyKeyEvent event, char *out_buf, size_t out_buf_size, size_t *out_len); + +#endif /* GHOSTTY_VT_KEY_ENCODER_H */ diff --git a/include/ghostty/vt/key/event.h b/include/ghostty/vt/key/event.h new file mode 100644 index 000000000..dbd2e9f84 --- /dev/null +++ b/include/ghostty/vt/key/event.h @@ -0,0 +1,474 @@ +/** + * @file event.h + * + * Key event representation and manipulation. + */ + +#ifndef GHOSTTY_VT_KEY_EVENT_H +#define GHOSTTY_VT_KEY_EVENT_H + +#include +#include +#include +#include +#include + +/** + * Opaque handle to a key event. + * + * This handle represents a keyboard input event containing information about + * the physical key pressed, modifiers, and generated text. + * + * @ingroup key + */ +typedef struct GhosttyKeyEvent *GhosttyKeyEvent; + +/** + * Keyboard input event types. + * + * @ingroup key + */ +typedef enum { + /** Key was released */ + GHOSTTY_KEY_ACTION_RELEASE = 0, + /** Key was pressed */ + GHOSTTY_KEY_ACTION_PRESS = 1, + /** Key is being repeated (held down) */ + GHOSTTY_KEY_ACTION_REPEAT = 2, +} GhosttyKeyAction; + +/** + * Keyboard modifier keys bitmask. + * + * A bitmask representing all keyboard modifiers. This tracks which modifier keys + * are pressed and, where supported by the platform, which side (left or right) + * of each modifier is active. + * + * Use the GHOSTTY_MODS_* constants to test and set individual modifiers. + * + * Modifier side bits are only meaningful when the corresponding modifier bit is set. + * Not all platforms support distinguishing between left and right modifier + * keys and Ghostty is built to expect that some platforms may not provide this + * information. + * + * @ingroup key + */ +typedef uint16_t GhosttyMods; + +/** Shift key is pressed */ +#define GHOSTTY_MODS_SHIFT (1 << 0) +/** Control key is pressed */ +#define GHOSTTY_MODS_CTRL (1 << 1) +/** Alt/Option key is pressed */ +#define GHOSTTY_MODS_ALT (1 << 2) +/** Super/Command/Windows key is pressed */ +#define GHOSTTY_MODS_SUPER (1 << 3) +/** Caps Lock is active */ +#define GHOSTTY_MODS_CAPS_LOCK (1 << 4) +/** Num Lock is active */ +#define GHOSTTY_MODS_NUM_LOCK (1 << 5) + +/** + * Right shift is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_SHIFT is set. + */ +#define GHOSTTY_MODS_SHIFT_SIDE (1 << 6) +/** + * Right ctrl is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_CTRL is set. + */ +#define GHOSTTY_MODS_CTRL_SIDE (1 << 7) +/** + * Right alt is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_ALT is set. + */ +#define GHOSTTY_MODS_ALT_SIDE (1 << 8) +/** + * Right super is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_SUPER is set. + */ +#define GHOSTTY_MODS_SUPER_SIDE (1 << 9) + +/** + * Physical key codes. + * + * The set of key codes that Ghostty is aware of. These represent physical keys + * on the keyboard and are layout-independent. For example, the "a" key on a US + * keyboard is the same as the "ф" key on a Russian keyboard, but both will + * report the same key_a value. + * + * Layout-dependent strings are provided separately as UTF-8 text and are produced + * by the platform. These values are based on the W3C UI Events KeyboardEvent code + * standard. See: https://www.w3.org/TR/uievents-code + * + * @ingroup key + */ +typedef enum { + GHOSTTY_KEY_UNIDENTIFIED = 0, + + // Writing System Keys (W3C § 3.1.1) + GHOSTTY_KEY_BACKQUOTE, + GHOSTTY_KEY_BACKSLASH, + GHOSTTY_KEY_BRACKET_LEFT, + GHOSTTY_KEY_BRACKET_RIGHT, + GHOSTTY_KEY_COMMA, + GHOSTTY_KEY_DIGIT_0, + GHOSTTY_KEY_DIGIT_1, + GHOSTTY_KEY_DIGIT_2, + GHOSTTY_KEY_DIGIT_3, + GHOSTTY_KEY_DIGIT_4, + GHOSTTY_KEY_DIGIT_5, + GHOSTTY_KEY_DIGIT_6, + GHOSTTY_KEY_DIGIT_7, + GHOSTTY_KEY_DIGIT_8, + GHOSTTY_KEY_DIGIT_9, + GHOSTTY_KEY_EQUAL, + GHOSTTY_KEY_INTL_BACKSLASH, + GHOSTTY_KEY_INTL_RO, + GHOSTTY_KEY_INTL_YEN, + GHOSTTY_KEY_A, + GHOSTTY_KEY_B, + GHOSTTY_KEY_C, + GHOSTTY_KEY_D, + GHOSTTY_KEY_E, + GHOSTTY_KEY_F, + GHOSTTY_KEY_G, + GHOSTTY_KEY_H, + GHOSTTY_KEY_I, + GHOSTTY_KEY_J, + GHOSTTY_KEY_K, + GHOSTTY_KEY_L, + GHOSTTY_KEY_M, + GHOSTTY_KEY_N, + GHOSTTY_KEY_O, + GHOSTTY_KEY_P, + GHOSTTY_KEY_Q, + GHOSTTY_KEY_R, + GHOSTTY_KEY_S, + GHOSTTY_KEY_T, + GHOSTTY_KEY_U, + GHOSTTY_KEY_V, + GHOSTTY_KEY_W, + GHOSTTY_KEY_X, + GHOSTTY_KEY_Y, + GHOSTTY_KEY_Z, + GHOSTTY_KEY_MINUS, + GHOSTTY_KEY_PERIOD, + GHOSTTY_KEY_QUOTE, + GHOSTTY_KEY_SEMICOLON, + GHOSTTY_KEY_SLASH, + + // Functional Keys (W3C § 3.1.2) + GHOSTTY_KEY_ALT_LEFT, + GHOSTTY_KEY_ALT_RIGHT, + GHOSTTY_KEY_BACKSPACE, + GHOSTTY_KEY_CAPS_LOCK, + GHOSTTY_KEY_CONTEXT_MENU, + GHOSTTY_KEY_CONTROL_LEFT, + GHOSTTY_KEY_CONTROL_RIGHT, + GHOSTTY_KEY_ENTER, + GHOSTTY_KEY_META_LEFT, + GHOSTTY_KEY_META_RIGHT, + GHOSTTY_KEY_SHIFT_LEFT, + GHOSTTY_KEY_SHIFT_RIGHT, + GHOSTTY_KEY_SPACE, + GHOSTTY_KEY_TAB, + GHOSTTY_KEY_CONVERT, + GHOSTTY_KEY_KANA_MODE, + GHOSTTY_KEY_NON_CONVERT, + + // Control Pad Section (W3C § 3.2) + GHOSTTY_KEY_DELETE, + GHOSTTY_KEY_END, + GHOSTTY_KEY_HELP, + GHOSTTY_KEY_HOME, + GHOSTTY_KEY_INSERT, + GHOSTTY_KEY_PAGE_DOWN, + GHOSTTY_KEY_PAGE_UP, + + // Arrow Pad Section (W3C § 3.3) + GHOSTTY_KEY_ARROW_DOWN, + GHOSTTY_KEY_ARROW_LEFT, + GHOSTTY_KEY_ARROW_RIGHT, + GHOSTTY_KEY_ARROW_UP, + + // Numpad Section (W3C § 3.4) + GHOSTTY_KEY_NUM_LOCK, + GHOSTTY_KEY_NUMPAD_0, + GHOSTTY_KEY_NUMPAD_1, + GHOSTTY_KEY_NUMPAD_2, + GHOSTTY_KEY_NUMPAD_3, + GHOSTTY_KEY_NUMPAD_4, + GHOSTTY_KEY_NUMPAD_5, + GHOSTTY_KEY_NUMPAD_6, + GHOSTTY_KEY_NUMPAD_7, + GHOSTTY_KEY_NUMPAD_8, + GHOSTTY_KEY_NUMPAD_9, + GHOSTTY_KEY_NUMPAD_ADD, + GHOSTTY_KEY_NUMPAD_BACKSPACE, + GHOSTTY_KEY_NUMPAD_CLEAR, + GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY, + GHOSTTY_KEY_NUMPAD_COMMA, + GHOSTTY_KEY_NUMPAD_DECIMAL, + GHOSTTY_KEY_NUMPAD_DIVIDE, + GHOSTTY_KEY_NUMPAD_ENTER, + GHOSTTY_KEY_NUMPAD_EQUAL, + GHOSTTY_KEY_NUMPAD_MEMORY_ADD, + GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR, + GHOSTTY_KEY_NUMPAD_MEMORY_RECALL, + GHOSTTY_KEY_NUMPAD_MEMORY_STORE, + GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT, + GHOSTTY_KEY_NUMPAD_MULTIPLY, + GHOSTTY_KEY_NUMPAD_PAREN_LEFT, + GHOSTTY_KEY_NUMPAD_PAREN_RIGHT, + GHOSTTY_KEY_NUMPAD_SUBTRACT, + GHOSTTY_KEY_NUMPAD_SEPARATOR, + GHOSTTY_KEY_NUMPAD_UP, + GHOSTTY_KEY_NUMPAD_DOWN, + GHOSTTY_KEY_NUMPAD_RIGHT, + GHOSTTY_KEY_NUMPAD_LEFT, + GHOSTTY_KEY_NUMPAD_BEGIN, + GHOSTTY_KEY_NUMPAD_HOME, + GHOSTTY_KEY_NUMPAD_END, + GHOSTTY_KEY_NUMPAD_INSERT, + GHOSTTY_KEY_NUMPAD_DELETE, + GHOSTTY_KEY_NUMPAD_PAGE_UP, + GHOSTTY_KEY_NUMPAD_PAGE_DOWN, + + // Function Section (W3C § 3.5) + GHOSTTY_KEY_ESCAPE, + GHOSTTY_KEY_F1, + GHOSTTY_KEY_F2, + GHOSTTY_KEY_F3, + GHOSTTY_KEY_F4, + GHOSTTY_KEY_F5, + GHOSTTY_KEY_F6, + GHOSTTY_KEY_F7, + GHOSTTY_KEY_F8, + GHOSTTY_KEY_F9, + GHOSTTY_KEY_F10, + GHOSTTY_KEY_F11, + GHOSTTY_KEY_F12, + GHOSTTY_KEY_F13, + GHOSTTY_KEY_F14, + GHOSTTY_KEY_F15, + GHOSTTY_KEY_F16, + GHOSTTY_KEY_F17, + GHOSTTY_KEY_F18, + GHOSTTY_KEY_F19, + GHOSTTY_KEY_F20, + GHOSTTY_KEY_F21, + GHOSTTY_KEY_F22, + GHOSTTY_KEY_F23, + GHOSTTY_KEY_F24, + GHOSTTY_KEY_F25, + GHOSTTY_KEY_FN, + GHOSTTY_KEY_FN_LOCK, + GHOSTTY_KEY_PRINT_SCREEN, + GHOSTTY_KEY_SCROLL_LOCK, + GHOSTTY_KEY_PAUSE, + + // Media Keys (W3C § 3.6) + GHOSTTY_KEY_BROWSER_BACK, + GHOSTTY_KEY_BROWSER_FAVORITES, + GHOSTTY_KEY_BROWSER_FORWARD, + GHOSTTY_KEY_BROWSER_HOME, + GHOSTTY_KEY_BROWSER_REFRESH, + GHOSTTY_KEY_BROWSER_SEARCH, + GHOSTTY_KEY_BROWSER_STOP, + GHOSTTY_KEY_EJECT, + GHOSTTY_KEY_LAUNCH_APP_1, + GHOSTTY_KEY_LAUNCH_APP_2, + GHOSTTY_KEY_LAUNCH_MAIL, + GHOSTTY_KEY_MEDIA_PLAY_PAUSE, + GHOSTTY_KEY_MEDIA_SELECT, + GHOSTTY_KEY_MEDIA_STOP, + GHOSTTY_KEY_MEDIA_TRACK_NEXT, + GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS, + GHOSTTY_KEY_POWER, + GHOSTTY_KEY_SLEEP, + GHOSTTY_KEY_AUDIO_VOLUME_DOWN, + GHOSTTY_KEY_AUDIO_VOLUME_MUTE, + GHOSTTY_KEY_AUDIO_VOLUME_UP, + GHOSTTY_KEY_WAKE_UP, + + // Legacy, Non-standard, and Special Keys (W3C § 3.7) + GHOSTTY_KEY_COPY, + GHOSTTY_KEY_CUT, + GHOSTTY_KEY_PASTE, +} GhosttyKey; + +/** + * Create a new key event instance. + * + * Creates a new key event with default values. The event must be freed using + * ghostty_key_event_free() when no longer needed. + * + * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator + * @param event Pointer to store the created key event handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup key + */ +GhosttyResult ghostty_key_event_new(const GhosttyAllocator *allocator, GhosttyKeyEvent *event); + +/** + * Free a key event instance. + * + * Releases all resources associated with the key event. After this call, + * the event handle becomes invalid and must not be used. + * + * @param event The key event handle to free (may be NULL) + * + * @ingroup key + */ +void ghostty_key_event_free(GhosttyKeyEvent event); + +/** + * Set the key action (press, release, repeat). + * + * @param event The key event handle, must not be NULL + * @param action The action to set + * + * @ingroup key + */ +void ghostty_key_event_set_action(GhosttyKeyEvent event, GhosttyKeyAction action); + +/** + * Get the key action (press, release, repeat). + * + * @param event The key event handle, must not be NULL + * @return The key action + * + * @ingroup key + */ +GhosttyKeyAction ghostty_key_event_get_action(GhosttyKeyEvent event); + +/** + * Set the physical key code. + * + * @param event The key event handle, must not be NULL + * @param key The physical key code to set + * + * @ingroup key + */ +void ghostty_key_event_set_key(GhosttyKeyEvent event, GhosttyKey key); + +/** + * Get the physical key code. + * + * @param event The key event handle, must not be NULL + * @return The physical key code + * + * @ingroup key + */ +GhosttyKey ghostty_key_event_get_key(GhosttyKeyEvent event); + +/** + * Set the modifier keys bitmask. + * + * @param event The key event handle, must not be NULL + * @param mods The modifier keys bitmask to set + * + * @ingroup key + */ +void ghostty_key_event_set_mods(GhosttyKeyEvent event, GhosttyMods mods); + +/** + * Get the modifier keys bitmask. + * + * @param event The key event handle, must not be NULL + * @return The modifier keys bitmask + * + * @ingroup key + */ +GhosttyMods ghostty_key_event_get_mods(GhosttyKeyEvent event); + +/** + * Set the consumed modifiers bitmask. + * + * @param event The key event handle, must not be NULL + * @param consumed_mods The consumed modifiers bitmask to set + * + * @ingroup key + */ +void ghostty_key_event_set_consumed_mods(GhosttyKeyEvent event, GhosttyMods consumed_mods); + +/** + * Get the consumed modifiers bitmask. + * + * @param event The key event handle, must not be NULL + * @return The consumed modifiers bitmask + * + * @ingroup key + */ +GhosttyMods ghostty_key_event_get_consumed_mods(GhosttyKeyEvent event); + +/** + * Set whether the key event is part of a composition sequence. + * + * @param event The key event handle, must not be NULL + * @param composing Whether the key event is part of a composition sequence + * + * @ingroup key + */ +void ghostty_key_event_set_composing(GhosttyKeyEvent event, bool composing); + +/** + * Get whether the key event is part of a composition sequence. + * + * @param event The key event handle, must not be NULL + * @return Whether the key event is part of a composition sequence + * + * @ingroup key + */ +bool ghostty_key_event_get_composing(GhosttyKeyEvent event); + +/** + * Set the UTF-8 text generated by the key event. + * + * The key event does NOT take ownership of the text pointer. The caller + * must ensure the string remains valid for the lifetime needed by the event. + * + * @param event The key event handle, must not be NULL + * @param utf8 The UTF-8 text to set (or NULL for empty) + * @param len Length of the UTF-8 text in bytes + * + * @ingroup key + */ +void ghostty_key_event_set_utf8(GhosttyKeyEvent event, const char *utf8, size_t len); + +/** + * Get the UTF-8 text generated by the key event. + * + * The returned pointer is valid until the event is freed or the UTF-8 text is modified. + * + * @param event The key event handle, must not be NULL + * @param len Pointer to store the length of the UTF-8 text in bytes (may be NULL) + * @return The UTF-8 text (or NULL for empty) + * + * @ingroup key + */ +const char *ghostty_key_event_get_utf8(GhosttyKeyEvent event, size_t *len); + +/** + * Set the unshifted Unicode codepoint. + * + * @param event The key event handle, must not be NULL + * @param codepoint The unshifted Unicode codepoint to set + * + * @ingroup key + */ +void ghostty_key_event_set_unshifted_codepoint(GhosttyKeyEvent event, uint32_t codepoint); + +/** + * Get the unshifted Unicode codepoint. + * + * @param event The key event handle, must not be NULL + * @return The unshifted Unicode codepoint + * + * @ingroup key + */ +uint32_t ghostty_key_event_get_unshifted_codepoint(GhosttyKeyEvent event); + +#endif /* GHOSTTY_VT_KEY_EVENT_H */ diff --git a/include/ghostty/vt/osc.h b/include/ghostty/vt/osc.h new file mode 100644 index 000000000..7e2c8f322 --- /dev/null +++ b/include/ghostty/vt/osc.h @@ -0,0 +1,231 @@ +/** + * @file osc.h + * + * OSC (Operating System Command) sequence parser and command handling. + */ + +#ifndef GHOSTTY_VT_OSC_H +#define GHOSTTY_VT_OSC_H + +#include +#include +#include +#include +#include + +/** + * Opaque handle to an OSC parser instance. + * + * This handle represents an OSC (Operating System Command) parser that can + * be used to parse the contents of OSC sequences. + * + * @ingroup osc + */ +typedef struct GhosttyOscParser *GhosttyOscParser; + +/** + * Opaque handle to a single OSC command. + * + * This handle represents a parsed OSC (Operating System Command) command. + * The command can be queried for its type and associated data. + * + * @ingroup osc + */ +typedef struct GhosttyOscCommand *GhosttyOscCommand; + +/** @defgroup osc OSC Parser + * + * OSC (Operating System Command) sequence parser and command handling. + * + * The parser operates in a streaming fashion, processing input byte-by-byte + * to handle OSC sequences that may arrive in fragments across multiple reads. + * This interface makes it easy to integrate into most environments and avoids + * over-allocating buffers. + * + * ## Basic Usage + * + * 1. Create a parser instance with ghostty_osc_new() + * 2. Feed bytes to the parser using ghostty_osc_next() + * 3. Finalize parsing with ghostty_osc_end() to get the command + * 4. Query command type and extract data using ghostty_osc_command_type() + * and ghostty_osc_command_data() + * 5. Free the parser with ghostty_osc_free() when done + * + * @{ + */ + +/** + * OSC command types. + * + * @ingroup osc + */ +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, +} GhosttyOscCommandType; + +/** + * OSC command data types. + * + * These values specify what type of data to extract from an OSC command + * using `ghostty_osc_command_data`. + * + * @ingroup osc + */ +typedef enum { + /** Invalid data type. Never results in any data extraction. */ + GHOSTTY_OSC_DATA_INVALID = 0, + + /** + * Window title string data. + * + * Valid for: GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE + * + * Output type: const char ** (pointer to null-terminated string) + * + * Lifetime: Valid until the next call to any ghostty_osc_* function with + * the same parser instance. Memory is owned by the parser. + */ + GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR = 1, +} GhosttyOscCommandData; + +/** + * Create a new OSC parser instance. + * + * Creates a new OSC (Operating System Command) parser using the provided + * allocator. The parser must be freed using ghostty_vt_osc_free() when + * no longer needed. + * + * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator + * @param parser Pointer to store the created parser handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup osc + */ +GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser); + +/** + * Free an OSC parser instance. + * + * Releases all resources associated with the OSC parser. After this call, + * the parser handle becomes invalid and must not be used. + * + * @param parser The parser handle to free (may be NULL) + * + * @ingroup osc + */ +void ghostty_osc_free(GhosttyOscParser parser); + +/** + * Reset an OSC parser instance to its initial state. + * + * Resets the parser state, clearing any partially parsed OSC sequences + * and returning the parser to its initial state. This is useful for + * reusing a parser instance or recovering from parse errors. + * + * @param parser The parser handle to reset, must not be null. + * + * @ingroup osc + */ +void ghostty_osc_reset(GhosttyOscParser parser); + +/** + * Parse the next byte in an OSC sequence. + * + * Processes a single byte as part of an OSC sequence. The parser maintains + * internal state to track the progress through the sequence. Call this + * function for each byte in the sequence data. + * + * When finished pumping the parser with bytes, call ghostty_osc_end + * to get the final result. + * + * @param parser The parser handle, must not be null. + * @param byte The next byte to parse + * + * @ingroup osc + */ +void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte); + +/** + * Finalize OSC parsing and retrieve the parsed command. + * + * Call this function after feeding all bytes of an OSC sequence to the parser + * using ghostty_osc_next() with the exception of the terminating character + * (ESC or ST). This function finalizes the parsing process and returns the + * parsed OSC command. + * + * The return value is never NULL. Invalid commands will return a command + * with type GHOSTTY_OSC_COMMAND_INVALID. + * + * The terminator parameter specifies the byte that terminated the OSC sequence + * (typically 0x07 for BEL or 0x5C for ST after ESC). This information is + * preserved in the parsed command so that responses can use the same terminator + * format for better compatibility with the calling program. For commands that + * do not require a response, this parameter is ignored and the resulting + * command will not retain the terminator information. + * + * The returned command handle is valid until the next call to any + * `ghostty_osc_*` function with the same parser instance with the exception + * of command introspection functions such as `ghostty_osc_command_type`. + * + * @param parser The parser handle, must not be null. + * @param terminator The terminating byte of the OSC sequence (0x07 for BEL, 0x5C for ST) + * @return Handle to the parsed OSC command + * + * @ingroup osc + */ +GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator); + +/** + * Get the type of an OSC command. + * + * Returns the type identifier for the given OSC command. This can be used + * to determine what kind of command was parsed and what data might be + * available from it. + * + * @param command The OSC command handle to query (may be NULL) + * @return The command type, or GHOSTTY_OSC_COMMAND_INVALID if command is NULL + * + * @ingroup osc + */ +GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command); + +/** + * Extract data from an OSC command. + * + * Extracts typed data from the given OSC command based on the specified + * data type. The output pointer must be of the appropriate type for the + * requested data kind. Valid command types, output types, and memory + * safety information are documented in the `GhosttyOscCommandData` enum. + * + * @param command The OSC command handle to query (may be NULL) + * @param data The type of data to extract + * @param out Pointer to store the extracted data (type depends on data parameter) + * @return true if data extraction was successful, false otherwise + * + * @ingroup osc + */ +bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData data, void *out); + +/** @} */ + +#endif /* GHOSTTY_VT_OSC_H */ diff --git a/include/ghostty/vt/paste.h b/include/ghostty/vt/paste.h new file mode 100644 index 000000000..d90f303d4 --- /dev/null +++ b/include/ghostty/vt/paste.h @@ -0,0 +1,75 @@ +/** + * @file paste.h + * + * Paste utilities - validate and encode paste data for terminal input. + */ + +#ifndef GHOSTTY_VT_PASTE_H +#define GHOSTTY_VT_PASTE_H + +/** @defgroup paste Paste Utilities + * + * Utilities for validating paste data safety. + * + * ## Basic Usage + * + * Use ghostty_paste_is_safe() to check if paste data contains potentially + * dangerous sequences before sending it to the terminal. + * + * ## Example + * + * @code{.c} + * #include + * #include + * #include + * + * int main() { + * const char* safe_data = "hello world"; + * const char* unsafe_data = "rm -rf /\n"; + * + * if (ghostty_paste_is_safe(safe_data, strlen(safe_data))) { + * printf("Safe to paste\n"); + * } + * + * if (!ghostty_paste_is_safe(unsafe_data, strlen(unsafe_data))) { + * printf("Unsafe! Contains newline\n"); + * } + * + * return 0; + * } + * @endcode + * + * @{ + */ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Check if paste data is safe to paste into the terminal. + * + * Data is considered unsafe if it contains: + * - Newlines (`\n`) which can inject commands + * - The bracketed paste end sequence (`\x1b[201~`) which can be used + * to exit bracketed paste mode and inject commands + * + * This check is conservative and considers data unsafe regardless of + * current terminal state. + * + * @param data The paste data to check (must not be NULL) + * @param len The length of the data in bytes + * @return true if the data is safe to paste, false otherwise + */ +bool ghostty_paste_is_safe(const char* data, size_t len); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_PASTE_H */ diff --git a/include/ghostty/vt/result.h b/include/ghostty/vt/result.h new file mode 100644 index 000000000..cc382eade --- /dev/null +++ b/include/ghostty/vt/result.h @@ -0,0 +1,20 @@ +/** + * @file result.h + * + * Result codes for libghostty-vt operations. + */ + +#ifndef GHOSTTY_VT_RESULT_H +#define GHOSTTY_VT_RESULT_H + +/** + * Result codes for libghostty-vt operations. + */ +typedef enum { + /** Operation completed successfully */ + GHOSTTY_SUCCESS = 0, + /** Operation failed due to failed allocation */ + GHOSTTY_OUT_OF_MEMORY = -1, +} GhosttyResult; + +#endif /* GHOSTTY_VT_RESULT_H */ diff --git a/macos/Ghostty-Info.plist b/macos/Ghostty-Info.plist index ff391c0f8..2bf3b0bae 100644 --- a/macos/Ghostty-Info.plist +++ b/macos/Ghostty-Info.plist @@ -61,7 +61,7 @@ NSMenuItem default - New Ghostty Tab Here + New $(INFOPLIST_KEY_CFBundleDisplayName) Tab Here NSMessage openTab @@ -80,7 +80,7 @@ NSMenuItem default - New Ghostty Window Here + New $(INFOPLIST_KEY_CFBundleDisplayName) Window Here NSMessage openWindow diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index cd9e56186..ca420afaa 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -75,6 +75,7 @@ "Features/App Intents/CommandPaletteIntent.swift", "Features/App Intents/Entities/CommandEntity.swift", "Features/App Intents/Entities/TerminalEntity.swift", + "Features/App Intents/FocusTerminalIntent.swift", "Features/App Intents/GetTerminalDetailsIntent.swift", "Features/App Intents/GhosttyIntentError.swift", "Features/App Intents/InputIntent.swift", @@ -95,6 +96,7 @@ Features/QuickTerminal/QuickTerminalController.swift, Features/QuickTerminal/QuickTerminalPosition.swift, Features/QuickTerminal/QuickTerminalScreen.swift, + Features/QuickTerminal/QuickTerminalScreenStateCache.swift, Features/QuickTerminal/QuickTerminalSize.swift, Features/QuickTerminal/QuickTerminalSpaceBehavior.swift, Features/QuickTerminal/QuickTerminalWindow.swift, @@ -124,7 +126,14 @@ "Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift", "Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift", "Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift", + Features/Update/UpdateBadge.swift, + Features/Update/UpdateController.swift, Features/Update/UpdateDelegate.swift, + Features/Update/UpdateDriver.swift, + Features/Update/UpdatePill.swift, + Features/Update/UpdatePopoverView.swift, + Features/Update/UpdateSimulator.swift, + Features/Update/UpdateViewModel.swift, "Ghostty/FullscreenMode+Extension.swift", Ghostty/Ghostty.Command.swift, Ghostty/Ghostty.Error.swift, @@ -133,6 +142,7 @@ Ghostty/Ghostty.Surface.swift, Ghostty/InspectorView.swift, "Ghostty/NSEvent+Extension.swift", + Ghostty/SurfaceScrollView.swift, Ghostty/SurfaceView_AppKit.swift, Helpers/AppInfo.swift, Helpers/CodableBridge.swift, @@ -159,6 +169,7 @@ Helpers/KeyboardLayout.swift, Helpers/LastWindowPosition.swift, Helpers/MetalView.swift, + Helpers/NonDraggableHostingView.swift, Helpers/PermissionRequest.swift, Helpers/Private/CGS.swift, Helpers/Private/Dock.swift, @@ -544,6 +555,7 @@ INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders."; INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition."; INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges."; + INFOPLIST_PREPROCESS = YES; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -766,7 +778,7 @@ EXECUTABLE_NAME = ghostty; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Ghostty-Info.plist"; - INFOPLIST_KEY_CFBundleDisplayName = Ghostty; + INFOPLIST_KEY_CFBundleDisplayName = "Ghostty[DEBUG]"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program running within Ghostty would like to use AppleScript."; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "A program running within Ghostty would like to use Bluetooth."; @@ -784,6 +796,7 @@ INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders."; INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition."; INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges."; + INFOPLIST_PREPROCESS = YES; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -838,6 +851,7 @@ INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders."; INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition."; INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges."; + INFOPLIST_PREPROCESS = YES; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", diff --git a/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index db3dd11a5..89573fb88 100644 --- a/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle", "state" : { - "revision" : "06beff60e3b609a485290e4d5d2d1e2eedb1e55d", - "version" : "2.7.3" + "revision" : "9a1d2a19d3595fcf8d9c447173f9a1687b3dcadb", + "version" : "2.8.0" } } ], diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 942aecdd4..9a6eab47b 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -1,4 +1,5 @@ import AppKit +import SwiftUI import UserNotifications import OSLog import Sparkle @@ -98,8 +99,10 @@ class AppDelegate: NSObject, ) /// Manages updates - let updaterController: SPUStandardUpdaterController - let updaterDelegate: UpdaterDelegate = UpdaterDelegate() + let updateController = UpdateController() + var updateViewModel: UpdateViewModel { + updateController.viewModel + } /// The elapsed time since the process was started var timeSinceLaunch: TimeInterval { @@ -118,7 +121,12 @@ class AppDelegate: NSObject, /// The custom app icon image that is currently in use. @Published private(set) var appIcon: NSImage? = nil { didSet { +#if DEBUG + // if no custom icon specified, we use blueprint to distinguish from release app + NSApplication.shared.applicationIconImage = appIcon ?? NSImage(named: "BlueprintImage") +#else NSApplication.shared.applicationIconImage = appIcon +#endif let appPath = Bundle.main.bundlePath NSWorkspace.shared.setIcon(appIcon, forFile: appPath, options: []) NSWorkspace.shared.noteFileSystemChanged(appPath) @@ -126,15 +134,6 @@ class AppDelegate: NSObject, } override init() { - updaterController = SPUStandardUpdaterController( - // Important: we must not start the updater here because we need to read our configuration - // first to determine whether we're automatically checking, downloading, etc. The updater - // is started later in applicationDidFinishLaunching - startingUpdater: false, - updaterDelegate: updaterDelegate, - userDriverDelegate: nil - ) - super.init() ghostty.delegate = self @@ -179,7 +178,7 @@ class AppDelegate: NSObject, ghosttyConfigDidChange(config: ghostty.config) // Start our update checker. - updaterController.startUpdater() + updateController.startUpdater() // Register our service provider. This must happen after everything is initialized. NSApp.servicesProvider = ServiceProvider() @@ -323,6 +322,12 @@ class AppDelegate: NSObject, func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { let windows = NSApplication.shared.windows if (windows.isEmpty) { return .terminateNow } + + // If we've already accepted to install an update, then we don't need to + // confirm quit. The user is already expecting the update to happen. + if updateController.isInstalling { + return .terminateNow + } // This probably isn't fully safe. The isEmpty check above is aspirational, it doesn't // quite work with SwiftUI because windows are retained on close. So instead we check @@ -471,7 +476,12 @@ class AppDelegate: NSObject, } switch ghostty.config.macosDockDropBehavior { - case .new_tab: _ = TerminalController.newTab(ghostty, withBaseConfig: config) + case .new_tab: + _ = TerminalController.newTab( + ghostty, + from: TerminalController.preferredParent?.window, + withBaseConfig: config + ) case .new_window: _ = TerminalController.newWindow(ghostty, withBaseConfig: config) } @@ -806,12 +816,12 @@ class AppDelegate: NSObject, // defined by our "auto-update" configuration (if set) or fall back to Sparkle // user-based defaults. if Bundle.main.infoDictionary?["SUEnableAutomaticChecks"] as? Bool == false { - updaterController.updater.automaticallyChecksForUpdates = false - updaterController.updater.automaticallyDownloadsUpdates = false + updateController.updater.automaticallyChecksForUpdates = false + updateController.updater.automaticallyDownloadsUpdates = false } else if let autoUpdate = config.autoUpdate { - updaterController.updater.automaticallyChecksForUpdates = + updateController.updater.automaticallyChecksForUpdates = autoUpdate == .check || autoUpdate == .download - updaterController.updater.automaticallyDownloadsUpdates = + updateController.updater.automaticallyDownloadsUpdates = autoUpdate == .download } @@ -1004,7 +1014,8 @@ class AppDelegate: NSObject, } @IBAction func checkForUpdates(_ sender: Any?) { - updaterController.checkForUpdates(sender) + updateController.checkForUpdates() + //UpdateSimulator.happyPath.simulate(with: updateViewModel) } @IBAction func newWindow(_ sender: Any?) { @@ -1012,7 +1023,10 @@ class AppDelegate: NSObject, } @IBAction func newTab(_ sender: Any?) { - _ = TerminalController.newTab(ghostty) + _ = TerminalController.newTab( + ghostty, + from: TerminalController.preferredParent?.window + ) } @IBAction func closeAllWindows(_ sender: Any?) { diff --git a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift index 923d22c97..0155cf855 100644 --- a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift @@ -12,8 +12,10 @@ struct CloseTerminalIntent: AppIntent { ) var terminal: TerminalEntity +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = .background +#endif @MainActor func perform() async throws -> some IntentResult { diff --git a/macos/Sources/Features/App Intents/CommandPaletteIntent.swift b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift index fa983054b..2f07d7861 100644 --- a/macos/Sources/Features/App Intents/CommandPaletteIntent.swift +++ b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift @@ -19,8 +19,10 @@ struct CommandPaletteIntent: AppIntent { ) var command: CommandEntity +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = .background +#endif @MainActor func perform() async throws -> some IntentResult & ReturnsValue { diff --git a/macos/Sources/Features/App Intents/FocusTerminalIntent.swift b/macos/Sources/Features/App Intents/FocusTerminalIntent.swift new file mode 100644 index 000000000..21dd71b15 --- /dev/null +++ b/macos/Sources/Features/App Intents/FocusTerminalIntent.swift @@ -0,0 +1,37 @@ +import AppKit +import AppIntents +import GhosttyKit + +struct FocusTerminalIntent: AppIntent { + static var title: LocalizedStringResource = "Focus Terminal" + static var description = IntentDescription("Move focus to an existing terminal.") + + @Parameter( + title: "Terminal", + description: "The terminal to focus.", + ) + var terminal: TerminalEntity + +#if compiler(>=6.2) + @available(macOS 26.0, *) + static var supportedModes: IntentModes = .background +#endif + + @MainActor + func perform() async throws -> some IntentResult { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + + guard let surfaceView = terminal.surfaceView else { + throw GhosttyIntentError.surfaceNotFound + } + + guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { + return .result() + } + + controller.focusSurface(surfaceView) + return .result() + } +} diff --git a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift index 1cbaa9d68..563e3719b 100644 --- a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift +++ b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift @@ -17,8 +17,10 @@ struct GetTerminalDetailsIntent: AppIntent { ) var terminal: TerminalEntity +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = .background +#endif static var parameterSummary: some ParameterSummary { Summary("Get \(\.$detail) from \(\.$terminal)") diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift index 17c97fbbb..d169b3a8c 100644 --- a/macos/Sources/Features/App Intents/InputIntent.swift +++ b/macos/Sources/Features/App Intents/InputIntent.swift @@ -24,8 +24,10 @@ struct InputTextIntent: AppIntent { ) var terminal: TerminalEntity +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = [.background, .foreground] +#endif @MainActor func perform() async throws -> some IntentResult { @@ -74,8 +76,10 @@ struct KeyEventIntent: AppIntent { ) var terminal: TerminalEntity +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = [.background, .foreground] +#endif @MainActor func perform() async throws -> some IntentResult { @@ -136,8 +140,10 @@ struct MouseButtonIntent: AppIntent { ) var terminal: TerminalEntity +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = [.background, .foreground] +#endif @MainActor func perform() async throws -> some IntentResult { @@ -197,8 +203,10 @@ struct MousePosIntent: AppIntent { ) var terminal: TerminalEntity +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = [.background, .foreground] +#endif @MainActor func perform() async throws -> some IntentResult { @@ -265,8 +273,10 @@ struct MouseScrollIntent: AppIntent { ) var terminal: TerminalEntity +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = [.background, .foreground] +#endif @MainActor func perform() async throws -> some IntentResult { diff --git a/macos/Sources/Features/App Intents/KeybindIntent.swift b/macos/Sources/Features/App Intents/KeybindIntent.swift index b31da4a50..a8cea8561 100644 --- a/macos/Sources/Features/App Intents/KeybindIntent.swift +++ b/macos/Sources/Features/App Intents/KeybindIntent.swift @@ -16,8 +16,10 @@ struct KeybindIntent: AppIntent { ) var action: String +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = [.background, .foreground] +#endif @MainActor func perform() async throws -> some IntentResult & ReturnsValue { diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift index f7242ee56..be5c65bfa 100644 --- a/macos/Sources/Features/App Intents/NewTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -43,11 +43,15 @@ struct NewTerminalIntent: AppIntent { ) var parent: TerminalEntity? + // Performing in the background can avoid opening multiple windows at the same time + // using `foreground` will cause `perform` and `AppDelegate.applicationDidBecomeActive(_:)`/`AppDelegate.applicationShouldHandleReopen(_:hasVisibleWindows:)` running at the 'same' time +#if compiler(>=6.2) @available(macOS 26.0, *) - static var supportedModes: IntentModes = .foreground(.immediate) + static var supportedModes: IntentModes = .background +#endif @available(macOS, obsoleted: 26.0, message: "Replaced by supportedModes") - static var openAppWhenRun = true + static var openAppWhenRun = false @MainActor func perform() async throws -> some IntentResult & ReturnsValue { @@ -96,6 +100,11 @@ struct NewTerminalIntent: AppIntent { parent = nil } + defer { + if !NSApp.isActive { + NSApp.activate(ignoringOtherApps: true) + } + } switch location { case .window: let newController = TerminalController.newWindow( diff --git a/macos/Sources/Features/App Intents/QuickTerminalIntent.swift b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift index 2e6c9850c..2048a3b88 100644 --- a/macos/Sources/Features/App Intents/QuickTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift @@ -5,8 +5,10 @@ struct QuickTerminalIntent: AppIntent { static var title: LocalizedStringResource = "Open the Quick Terminal" static var description = IntentDescription("Open the Quick Terminal. If it is already open, then do nothing.") +#if compiler(>=6.2) @available(macOS 26.0, *) static var supportedModes: IntentModes = .background +#endif @MainActor func perform() async throws -> some IntentResult & ReturnsValue<[TerminalEntity]> { diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 8d15cbf9a..537137fe6 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -5,7 +5,28 @@ struct CommandOption: Identifiable, Hashable { let title: String let description: String? let symbols: [String]? + let leadingIcon: String? + let badge: String? + let emphasis: Bool let action: () -> Void + + init( + title: String, + description: String? = nil, + symbols: [String]? = nil, + leadingIcon: String? = nil, + badge: String? = nil, + emphasis: Bool = false, + action: @escaping () -> Void + ) { + self.title = title + self.description = description + self.symbols = symbols + self.leadingIcon = leadingIcon + self.badge = badge + self.emphasis = emphasis + self.action = action + } static func == (lhs: CommandOption, rhs: CommandOption) -> Bool { lhs.id == rhs.id @@ -198,7 +219,7 @@ fileprivate struct CommandTable: View { } else { ScrollViewReader { proxy in ScrollView { - VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 4) { ForEach(Array(options.enumerated()), id: \.1.id) { index, option in CommandRow( option: option, @@ -240,15 +261,36 @@ fileprivate struct CommandRow: View { var body: some View { Button(action: action) { - HStack { + HStack(spacing: 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) + Spacer() + + if let badge = option.badge, !badge.isEmpty { + Text(badge) + .font(.caption2.weight(.medium)) + .padding(.horizontal, 7) + .padding(.vertical, 3) + .background( + Capsule().fill(Color.accentColor.opacity(0.15)) + ) + .foregroundStyle(Color.accentColor) + } + if let symbols = option.symbols { ShortcutSymbolsView(symbols: symbols) .foregroundStyle(.secondary) } } .padding(8) + .contentShape(Rectangle()) .background( isSelected ? Color.accentColor.opacity(0.2) @@ -256,6 +298,10 @@ fileprivate struct CommandRow: View { ? Color.secondary.opacity(0.2) : Color.clear) ) + .overlay( + RoundedRectangle(cornerRadius: 5) + .strokeBorder(Color.accentColor.opacity(option.emphasis && !isSelected ? 0.3 : 0), lineWidth: 1.5) + ) .cornerRadius(5) } .help(option.description ?? "") diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index d02828494..673f5dd78 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -11,15 +11,54 @@ struct TerminalCommandPaletteView: View { /// The configuration so we can lookup keyboard shortcuts. @ObservedObject var ghosttyConfig: Ghostty.Config + + /// The update view model for showing update commands. + var updateViewModel: UpdateViewModel? /// The callback when an action is submitted. var onAction: ((String) -> Void) // The commands available to the command palette. private var commandOptions: [CommandOption] { - guard let surface = surfaceView.surfaceModel else { return [] } + 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 { - return try surface.commands().map { c in + let terminalCommands = try surface.commands().map { c in return CommandOption( title: c.title, description: c.description, @@ -28,9 +67,12 @@ struct TerminalCommandPaletteView: View { onAction(c.action) } } + options.append(contentsOf: terminalCommands) } catch { - return [] + return options } + + return options } var body: some View { diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 65186c5d7..4669e108a 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -21,13 +21,8 @@ class QuickTerminalController: BaseTerminalController { // The active space when the quick terminal was last shown. private var previousActiveSpace: CGSSpace? = nil - /// The saved state when the quick terminal's surface tree becomes empty. - /// - /// This preserves the user's window size and position when all terminal surfaces - /// are closed (e.g., via the `exit` command). When a new surface is created, - /// the window will be restored to this frame, preventing SwiftUI from resetting - /// the window to its default minimum size. - private var lastClosedFrames: NSMapTable + /// Cache for per-screen window state. + private let screenStateCache = QuickTerminalScreenStateCache() /// Non-nil if we have hidden dock state. private var hiddenDock: HiddenDock? = nil @@ -37,7 +32,7 @@ class QuickTerminalController: BaseTerminalController { /// Tracks if we're currently handling a manual resize to prevent recursion private var isHandlingResize: Bool = false - + init(_ ghostty: Ghostty.App, position: QuickTerminalPosition = .top, baseConfig base: Ghostty.SurfaceConfiguration? = nil, @@ -45,10 +40,6 @@ class QuickTerminalController: BaseTerminalController { ) { self.position = position self.derivedConfig = DerivedConfig(ghostty.config) - - // This is a weak to strong mapping, so that our keys being NSScreens - // can remove themselves when they disappear. - self.lastClosedFrames = .weakToStrongObjects() // Important detail here: we initialize with an empty surface tree so // that we don't start a terminal process. This gets started when the @@ -247,6 +238,22 @@ class QuickTerminalController: BaseTerminalController { // MARK: Base Controller Overrides + override func focusSurface(_ view: Ghostty.SurfaceView) { + if visible { + // If we're visible, we just focus the surface as normal. + super.focusSurface(view) + return + } + // Check if target surface belongs to this quick terminal + guard surfaceTree.contains(view) else { return } + // Set the target surface as focused + DispatchQueue.main.async { + Ghostty.moveFocus(to: view) + } + // Animation completion handler will handle window/app activation + animateIn() + } + override func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { super.surfaceTreeDidChange(from: from, to: to) @@ -363,17 +370,15 @@ class QuickTerminalController: BaseTerminalController { private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) { guard let screen = derivedConfig.quickTerminalScreen.screen else { return } - // Grab our last closed frame to use, and clear our state since we're animating in. - // We only use the last closed frame if we're opening on the same screen. - let lastClosedFrame: NSRect? = lastClosedFrames.object(forKey: screen)?.frame - lastClosedFrames.removeObject(forKey: screen) + // Grab our last closed frame to use from the cache. + let closedFrame = screenStateCache.frame(for: screen) // Move our window off screen to the initial animation position. position.setInitial( in: window, on: screen, terminalSize: derivedConfig.quickTerminalSize, - closedFrame: lastClosedFrame) + closedFrame: closedFrame) // We need to set our window level to a high value. In testing, only // popUpMenu and above do what we want. This gets it above the menu bar @@ -408,7 +413,7 @@ class QuickTerminalController: BaseTerminalController { in: window.animator(), on: screen, terminalSize: derivedConfig.quickTerminalSize, - closedFrame: lastClosedFrame) + closedFrame: closedFrame) }, completionHandler: { // There is a very minor delay here so waiting at least an event loop tick // keeps us safe from the view not being on the window. @@ -497,7 +502,7 @@ class QuickTerminalController: BaseTerminalController { // 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 { - lastClosedFrames.setObject(.init(frame: window.frame), forKey: screen) + screenStateCache.save(frame: window.frame, for: screen) } // If we hid the dock then we unhide it. @@ -582,7 +587,6 @@ class QuickTerminalController: BaseTerminalController { alert.alertStyle = .warning alert.beginSheetModal(for: window) } - // MARK: First Responder @IBAction override func closeWindow(_ sender: Any) { @@ -720,14 +724,6 @@ class QuickTerminalController: BaseTerminalController { hidden = false } } - - private class LastClosedState { - let frame: NSRect - - init(frame: NSRect) { - self.frame = frame - } - } } extension Notification.Name { diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift new file mode 100644 index 000000000..7dc53816c --- /dev/null +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift @@ -0,0 +1,113 @@ +import Foundation +import Cocoa + +/// Manages cached window state per screen for the quick terminal. +/// +/// This cache tracks the last closed window frame for each screen, allowing the quick terminal +/// 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 { + /// 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. + private static let maxSavedScreens = 10 + + /// Time-to-live for screen entries that are no longer present (14 days). + private static let screenStaleTTL: TimeInterval = 14 * 24 * 60 * 60 + + /// Keyed by display UUID to survive NSScreen garbage collection. + private var stateByDisplay: [UUID: DisplayEntry] = [:] + + init() { + NotificationCenter.default.addObserver( + self, + selector: #selector(onScreensChanged(_:)), + name: NSApplication.didChangeScreenParametersNotification, + object: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + /// Save the window frame for a screen. + func save(frame: NSRect, for screen: NSScreen) { + guard let key = screen.displayUUID else { return } + let entry = DisplayEntry( + frame: frame, + screenSize: screen.frame.size, + scale: screen.backingScaleFactor, + lastSeen: Date() + ) + stateByDisplay[key] = entry + pruneCapacity() + } + + /// Retrieve the last closed frame for a screen, if valid. + func frame(for screen: NSScreen) -> NSRect? { + guard let key = screen.displayUUID, var entry = stateByDisplay[key] else { return nil } + + // Drop on dimension/scale change that makes the entry invalid + if !entry.isValid(for: screen) { + stateByDisplay.removeValue(forKey: key) + return nil + } + + entry.lastSeen = Date() + stateByDisplay[key] = entry + return entry.frame + } + + @objc private func onScreensChanged(_ note: Notification) { + let screens = NSScreen.screens + let now = Date() + let currentIDs = Set(screens.compactMap { $0.displayUUID }) + + for screen in screens { + guard let key = screen.displayUUID else { continue } + if var entry = stateByDisplay[key] { + // Drop on dimension/scale change that makes the entry invalid + if !entry.isValid(for: screen) { + stateByDisplay.removeValue(forKey: key) + } else { + // Update the screen size if it grew (keep entry valid for larger screens) + entry.screenSize = screen.frame.size + entry.lastSeen = now + stateByDisplay[key] = entry + } + } + } + + // TTL prune for non-present screens + stateByDisplay = stateByDisplay.filter { key, entry in + currentIDs.contains(key) || now.timeIntervalSince(entry.lastSeen) < Self.screenStaleTTL + } + + pruneCapacity() + } + + private func pruneCapacity() { + guard stateByDisplay.count > Self.maxSavedScreens else { return } + let toRemove = stateByDisplay + .sorted { $0.value.lastSeen < $1.value.lastSeen } + .prefix(stateByDisplay.count - Self.maxSavedScreens) + for (key, _) in toRemove { + stateByDisplay.removeValue(forKey: key) + } + } + + private struct DisplayEntry { + var frame: NSRect + var screenSize: CGSize + var scale: CGFloat + var lastSeen: Date + + /// Returns true if this entry is still valid for the given screen. + /// Valid if the scale matches and the cached size is not larger than the current screen size. + /// This allows entries to persist when screens grow, but invalidates them when screens shrink. + func isValid(for screen: NSScreen) -> Bool { + guard scale == screen.backingScaleFactor else { return false } + return screenSize.width <= screen.frame.size.width && screenSize.height <= screen.frame.size.height + } + } +} diff --git a/macos/Sources/Features/Settings/SettingsView.swift b/macos/Sources/Features/Settings/SettingsView.swift index 82d24181a..6b0a2c46c 100644 --- a/macos/Sources/Features/Settings/SettingsView.swift +++ b/macos/Sources/Features/Settings/SettingsView.swift @@ -14,7 +14,7 @@ struct SettingsView: View { VStack(alignment: .leading) { Text("Coming Soon. 🚧").font(.title) Text("You can't configure settings in the GUI yet. To modify settings, " + - "edit the file at $HOME/.config/ghostty/config and restart Ghostty.") + "edit the file at $HOME/.config/ghostty/config.ghostty and restart Ghostty.") .multilineTextAlignment(.leading) .lineLimit(nil) } diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index f19640707..6b8171ff5 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -12,7 +12,7 @@ struct TerminalSplitTreeView: View { onResize: onResize) // 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 beaviors. + // the tree structure of splits it could result in bad behaviors. // See: https://github.com/ghostty-org/ghostty/issues/7546 .id(node.structuralIdentity) } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 2de967daf..552f864ee 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -48,6 +48,9 @@ class BaseTerminalController: NSWindowController, /// This can be set to show/hide the command palette. @Published var commandPaletteIsShowing: Bool = false + + /// Set if the terminal view should show the update overlay. + @Published var updateOverlayIsVisible: Bool = false /// Whether the terminal surface should focus when the mouse is over it. var focusFollowsMouse: Bool { @@ -233,6 +236,21 @@ class BaseTerminalController: NSWindowController, return newView } + /// Move focus to a surface view. + func focusSurface(_ view: Ghostty.SurfaceView) { + // Check if target surface is in our tree + guard surfaceTree.contains(view) else { return } + + // Move focus to the target surface and activate the window/app + DispatchQueue.main.async { + Ghostty.moveFocus(to: view) + view.window?.makeKeyAndOrderFront(nil) + if !NSApp.isActive { + NSApp.activate(ignoringOtherApps: true) + } + } + } + /// Called when the surfaceTree variable changed. /// /// Subclasses should call super first. @@ -551,23 +569,12 @@ class BaseTerminalController: NSWindowController, // Get the direction from the notification guard let directionAny = notification.userInfo?[Ghostty.Notification.SplitDirectionKey] else { return } guard let direction = directionAny as? Ghostty.SplitFocusDirection else { return } - - // Convert Ghostty.SplitFocusDirection to our SplitTree.FocusDirection - let focusDirection: SplitTree.FocusDirection - switch direction { - case .previous: focusDirection = .previous - case .next: focusDirection = .next - case .up: focusDirection = .spatial(.up) - case .down: focusDirection = .spatial(.down) - case .left: focusDirection = .spatial(.left) - case .right: focusDirection = .spatial(.right) - } // Find the node for the target surface guard let targetNode = surfaceTree.root?.node(view: target) else { return } // Find the next surface to focus - guard let nextSurface = surfaceTree.focusTarget(for: focusDirection, from: targetNode) else { + guard let nextSurface = surfaceTree.focusTarget(for: direction.toSplitTreeFocusDirection(), from: targetNode) else { return } @@ -728,6 +735,10 @@ class BaseTerminalController: NSWindowController, func cellSizeDidChange(to: NSSize) { guard derivedConfig.windowStepResize else { return } + // Stage manager can sometimes present windows in such a way that the + // cell size is temporarily zero due to the window being tiny. We can't + // set content resize increments to this value, so avoid an assertion failure. + guard to.width > 0 && to.height > 0 else { return } self.window?.contentResizeIncrements = to } @@ -799,7 +810,18 @@ class BaseTerminalController: NSWindowController, } } - func fullscreenDidChange() {} + func fullscreenDidChange() { + guard let fullscreenStyle else { return } + + // When we enter fullscreen, we want to show the update overlay so that it + // is easily visible. For native fullscreen this is visible by showing the + // menubar but we don't want to rely on that. + if fullscreenStyle.isFullscreen { + updateOverlayIsVisible = true + } else { + updateOverlayIsVisible = defaultUpdateOverlayVisibility() + } + } // MARK: Clipboard Confirmation @@ -881,6 +903,28 @@ class BaseTerminalController: NSWindowController, fullscreenStyle = NativeFullscreen(window) fullscreenStyle?.delegate = self } + + // Set our update overlay state + updateOverlayIsVisible = defaultUpdateOverlayVisibility() + } + + func defaultUpdateOverlayVisibility() -> Bool { + guard let window else { return true } + + // No titlebar we always show the update overlay because it can't support + // updates in the titlebar + guard window.styleMask.contains(.titled) else { + return true + } + + // If it's a non terminal window we can't trust it has an update accessory, + // so we always want to show the overlay. + guard let window = window as? TerminalWindow else { + return true + } + + // Show the overlay if the window isn't. + return !window.supportsUpdateAccessory } // MARK: NSWindowDelegate diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 779c13d9c..9790063d7 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -23,11 +23,15 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr case "hidden": "TerminalHiddenTitlebar" case "transparent": "TerminalTransparentTitlebar" case "tabs": +#if compiler(>=6.2) if #available(macOS 26.0, *) { "TerminalTabsTitlebarTahoe" } else { "TerminalTabsTitlebarVentura" } +#else + "TerminalTabsTitlebarVentura" +#endif default: defaultValue } diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index b5be0ae42..0cdff7c1f 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -31,6 +31,9 @@ protocol TerminalViewModel: ObservableObject { /// The command palette state. var commandPaletteIsShowing: Bool { get set } + + /// The update overlay should be visible. + var updateOverlayIsVisible: Bool { get } } /// The main terminal view. This terminal view supports splits. @@ -105,10 +108,33 @@ struct TerminalView: View { TerminalCommandPaletteView( surfaceView: surfaceView, isPresented: $viewModel.commandPaletteIsShowing, - ghosttyConfig: ghostty.config) { action in + ghosttyConfig: ghostty.config, + updateViewModel: (NSApp.delegate as? AppDelegate)?.updateViewModel) { action in self.delegate?.performAction(action, on: surfaceView) } } + + // Show update information above all else. + if viewModel.updateOverlayIsVisible { + UpdateOverlay() + } + } + } + } +} + +fileprivate struct UpdateOverlay: View { + var body: some View { + if let appDelegate = NSApp.delegate as? AppDelegate { + VStack { + Spacer() + + HStack { + Spacer() + UpdatePill(model: appDelegate.updateViewModel) + .padding(.bottom, 9) + .padding(.trailing, 9) + } } } } diff --git a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift index dc7dd7633..dd8b258f3 100644 --- a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift @@ -1,6 +1,9 @@ import AppKit class HiddenTitlebarTerminalWindow: TerminalWindow { + // No titlebar, we don't support accessories. + override var supportsUpdateAccessory: Bool { false } + override func awakeFromNib() { super.awakeFromNib() diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 3ab6293dc..16fcf227f 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -5,6 +5,12 @@ import GhosttyKit /// The base class for all standalone, "normal" terminal windows. This sets the basic /// style and configuration of the window based on the app configuration. 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" @@ -14,15 +20,25 @@ class TerminalWindow: NSWindow { /// Reset split zoom button in titlebar private let resetZoomAccessory = NSTitlebarAccessoryViewController() + + /// Update notification UI in titlebar + private let updateAccessory = NSTitlebarAccessoryViewController() /// The configuration derived from the Ghostty config so we don't need to rely on references. private(set) var derivedConfig: DerivedConfig = .init() + + /// 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 { + // Native window supports it. + true + } /// Gets the terminal controller from the window controller. var terminalController: TerminalController? { windowController as? TerminalController } - + // MARK: NSWindow Overrides override var toolbar: NSToolbar? { @@ -35,6 +51,9 @@ class TerminalWindow: NSWindow { } override func awakeFromNib() { + // Notify that this terminal window has loaded + NotificationCenter.default.post(name: Self.terminalDidAwake, object: self) + // 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. @@ -85,6 +104,17 @@ class TerminalWindow: NSWindow { })) addTitlebarAccessoryViewController(resetZoomAccessory) resetZoomAccessory.view.translatesAutoresizingMaskIntoConstraints = false + + // Create update notification accessory + if supportsUpdateAccessory { + updateAccessory.layoutAttribute = .right + updateAccessory.view = NonDraggableHostingView(rootView: UpdateAccessoryView( + viewModel: viewModel, + model: appDelegate.updateViewModel + )) + addTitlebarAccessoryViewController(updateAccessory) + updateAccessory.view.translatesAutoresizingMaskIntoConstraints = false + } } // Setup the accessory view for tabs that shows our keyboard shortcuts, @@ -103,6 +133,11 @@ 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() + } override func becomeKey() { super.becomeKey() @@ -124,6 +159,12 @@ class TerminalWindow: NSWindow { } else { tabBarDidDisappear() } + viewModel.isMainWindow = true + } + + override func resignMain() { + super.resignMain() + viewModel.isMainWindow = false } override func mergeAllWindows(_ sender: Any?) { @@ -164,9 +205,16 @@ class TerminalWindow: NSWindow { /// Returns true if there is a tab bar visible on this window. var hasTabBar: Bool { + // TODO: use titlebarView to find it instead contentView?.firstViewFromRoot(withClassName: "NSTabBar") != nil } + var hasMoreThanOneTabs: Bool { + /// accessing ``tabGroup?.windows`` here + /// will cause other edge cases, be careful + (tabbedWindows?.count ?? 0) > 1 + } + func isTabBar(_ childViewController: NSTitlebarAccessoryViewController) -> Bool { if childViewController.identifier == nil { // The good case @@ -198,6 +246,9 @@ 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. } private func tabBarDidDisappear() { @@ -260,7 +311,7 @@ class TerminalWindow: NSWindow { button.isBordered = false button.allowsExpansionToolTips = true button.toolTip = "Reset Zoom" - button.contentTintColor = .controlAccentColor + button.contentTintColor = isMainWindow ? .controlAccentColor : .secondaryLabelColor button.state = .on button.image = NSImage(named:"ResetZoom") button.frame = NSRect(x: 0, y: 0, width: 20, height: 20) @@ -277,6 +328,12 @@ class TerminalWindow: NSWindow { // Whenever we change the window title we must also update our // tab title if we're using custom fonts. tab.attributedTitle = attributedTitle + /// We also needs to update this here, just in case + /// the value is not what we want + /// + /// Check ``titlebarFont`` down below + /// to see why we need to check `hasMoreThanOneTabs` here + titlebarTextField?.usesSingleLineMode = !hasMoreThanOneTabs } } @@ -286,6 +343,12 @@ class TerminalWindow: NSWindow { let font = titlebarFont ?? NSFont.titleBarFont(ofSize: NSFont.systemFontSize) titlebarTextField?.font = font + /// We check `hasMoreThanOneTabs` here because the system + /// may copy this setting to the tab’s text field at some point(e.g. entering/exiting fullscreen), + /// which can cause the title to be vertically misaligned (shifted downward). + /// + /// This behaviour is the opposite of what happens in the title bar’s text field, which is quite odd... + titlebarTextField?.usesSingleLineMode = !hasMoreThanOneTabs tab.attributedTitle = attributedTitle } } @@ -436,7 +499,7 @@ class TerminalWindow: NSWindow { standardWindowButton(.miniaturizeButton)?.isHidden = true standardWindowButton(.zoomButton)?.isHidden = true } - + // MARK: Config struct DerivedConfig { @@ -467,28 +530,28 @@ extension TerminalWindow { class ViewModel: ObservableObject { @Published var isSurfaceZoomed: Bool = false @Published var hasToolbar: Bool = false + @Published var isMainWindow: Bool = true + + /// Calculates the top padding based on toolbar visibility and macOS version + fileprivate var accessoryTopPadding: CGFloat { + if #available(macOS 26.0, *) { + return hasToolbar ? 10 : 5 + } else { + return hasToolbar ? 9 : 4 + } + } } struct ResetZoomAccessoryView: View { @ObservedObject var viewModel: ViewModel let action: () -> Void - - // The padding from the top that the view appears. This was all just manually - // measured based on the OS. - var topPadding: CGFloat { - if #available(macOS 26.0, *) { - return viewModel.hasToolbar ? 10 : 5 - } else { - return viewModel.hasToolbar ? 9 : 4 - } - } var body: some View { if viewModel.isSurfaceZoomed { VStack { Button(action: action) { Image("ResetZoom") - .foregroundColor(.accentColor) + .foregroundColor(viewModel.isMainWindow ? .accentColor : .secondary) } .buttonStyle(.plain) .help("Reset Split Zoom") @@ -497,10 +560,24 @@ extension TerminalWindow { } // With a toolbar, the window title is taller, so we need more padding // to properly align. - .padding(.top, topPadding) + .padding(.top, viewModel.accessoryTopPadding) // We always need space at the end of the titlebar .padding(.trailing, 10) } } } + + /// 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) + .padding(.top, viewModel.accessoryTopPadding) + .padding(.trailing, viewModel.accessoryTopPadding) + } + } + } diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index 260fac4cc..4e067eddc 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -8,6 +8,10 @@ import SwiftUI class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSToolbarDelegate { /// The view model for SwiftUI views private var viewModel = ViewModel() + + /// 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 } deinit { tabBarObserver = nil @@ -15,9 +19,21 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // MARK: NSWindow + override var titlebarFont: NSFont? { + didSet { + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.viewModel.titleFont = self.titlebarFont + } + } + } + override var title: String { didSet { - viewModel.title = title + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.viewModel.title = self.title + } } } @@ -42,17 +58,33 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // Check if we have a tab bar and set it up if we have to. See the comment // on this function to learn why we need to check this here. setupTabBar() + + viewModel.isMainWindow = true } + override func resignMain() { + super.resignMain() + + viewModel.isMainWindow = false + } // 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) { // If this is the tab bar then we need to set it up for the titlebar guard isTabBar(childViewController) else { + // After dragging a tab into a new window, `hasTabBar` needs to be + // updated to properly review window title + viewModel.hasTabBar = false + super.addTitlebarAccessoryViewController(childViewController) return } + // When an existing tab is being dragged in to another tab group, + // system will also try to add tab bar to this window, so we want to reset observer, + // to put tab bar where we want again + tabBarObserver = nil + // Some setup needs to happen BEFORE it is added, such as layout. If // we don't do this before the call below, we'll trigger an AppKit // assertion. @@ -112,18 +144,33 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool guard tabBarObserver == nil else { return } // Find our tab bar. If it doesn't exist we don't do anything. - guard let tabBar = contentView?.rootView.firstDescendant(withClassName: "NSTabBar") else { return } + // + // 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 } + let titlebarView = if themeFrameView.responds(to: Selector(("titlebarView"))) { + themeFrameView.value(forKey: "titlebarView") as? NSView + } else { + NSView?.none + } + guard let tabBar = titlebarView?.firstDescendant(withClassName: "NSTabBar") else { return } // View model updates must happen on their own ticks. - DispatchQueue.main.async { - self.viewModel.hasTabBar = true + DispatchQueue.main.async { [weak self] in + self?.viewModel.hasTabBar = true } // Find our clip view guard let clipView = tabBar.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return } guard let accessoryView = clipView.subviews[safe: 0] else { return } - guard let titlebarView = clipView.firstSuperview(withClassName: "NSTitlebarView") else { return } + guard let titlebarView 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 // The container is the view that we'll constrain our tab bar within. let container = toolbarView @@ -205,6 +252,8 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool case .title: let item = NSToolbarItem(itemIdentifier: .title) item.view = NSHostingView(rootView: TitleItem(viewModel: viewModel)) + // Fix: https://github.com/ghostty-org/ghostty/discussions/9027 + item.view?.setContentCompressionResistancePriority(.required, for: .horizontal) item.visibilityPriority = .user item.isEnabled = true @@ -221,8 +270,10 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // MARK: SwiftUI class ViewModel: ObservableObject { + @Published var titleFont: NSFont? @Published var title: String = "👻 Ghostty" @Published var hasTabBar: Bool = false + @Published var isMainWindow: Bool = true } } @@ -245,15 +296,24 @@ extension TitlebarTabsTahoeTerminalWindow { var body: some View { if !viewModel.hasTabBar { - Text(title) - .lineLimit(1) - .truncationMode(.tail) + titleText } 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. + // know. This appears fixed in 26.1 Beta but keep it safe for 26.0. Color.clear.frame(width: 1, height: 1) } } + + @ViewBuilder + var titleText: some View { + Text(title) + .font(viewModel.titleFont.flatMap(Font.init(_:))) + .foregroundStyle(viewModel.isMainWindow ? .primary : .secondary) + .lineLimit(1) + .truncationMode(.tail) + .frame(maxWidth: .greatestFiniteMagnitude, alignment: .center) + .opacity(viewModel.hasTabBar ? 0 : 1) // hide when in fullscreen mode, where title bar will appear in the leading area under window buttons + } } } diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index 8589877d8..9aa8ec2eb 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -2,6 +2,10 @@ import Cocoa /// Titlebar tabs for macOS 13 to 15. 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 @@ -141,6 +145,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { super.syncAppearance(surfaceConfig) // Update our window light/darkness based on our updated background color + let themeChanged = isLightTheme != OSColor(surfaceConfig.backgroundColor).isLightColor isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor // Update our titlebar color @@ -150,7 +155,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { titlebarColor = derivedConfig.backgroundColor.withAlphaComponent(derivedConfig.backgroundOpacity) } - if (isOpaque) { + if (isOpaque || themeChanged) { // If there is transparency, calling this will make the titlebar opaque // so we only call this if we are opaque. updateTabBar() @@ -183,41 +188,33 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // so we need to do it manually. private func updateNewTabButtonOpacity() { guard let newTabButton: NSButton = titlebarContainer?.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return } - guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: { - $0 as? NSImageView != nil - }) as? NSImageView else { return } + guard let newTabButtonImageView = newTabButton.firstDescendant(withClassName: "NSImageView") as? NSImageView else { return } newTabButtonImageView.alphaValue = isKeyWindow ? 1 : 0.5 } - // Color the new tab button's image to match the color of the tab title/keyboard shortcut labels, - // just as it does in the stock tab bar. + /// Update: This method only add a vibrant overlay now, + /// since the image itself supports light/dark tint, + /// and system could restore it any time, + /// altering it will only cause maintenance burden for us. + /// + /// And if we hide original image, + /// ``updateNewTabButtonOpacity`` will not work + /// + /// ~~Color the new tab button's image to match the color of the tab title/keyboard shortcut labels,~~ + /// ~~just as it does in the stock tab bar.~~ private func updateNewTabButtonImage() { guard let newTabButton: NSButton = titlebarContainer?.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return } - guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: { - $0 as? NSImageView != nil - }) as? NSImageView else { return } + guard let newTabButtonImageView = newTabButton.firstDescendant(withClassName: "NSImageView") as? NSImageView else { return } guard let newTabButtonImage = newTabButtonImageView.image else { return } + let imageLayer = newTabButtonImageLayer ?? VibrantLayer(forAppearance: isLightTheme ? .light : .dark)! + imageLayer.frame = NSRect(origin: NSPoint(x: newTabButton.bounds.midX - newTabButtonImage.size.width/2, y: newTabButton.bounds.midY - newTabButtonImage.size.height/2), size: newTabButtonImage.size) + imageLayer.contentsGravity = .resizeAspect + imageLayer.opacity = 0.5 - if newTabButtonImageLayer == nil { - let fillColor: NSColor = isLightTheme ? .black.withAlphaComponent(0.85) : .white.withAlphaComponent(0.85) - let newImage = NSImage(size: newTabButtonImage.size, flipped: false) { rect in - newTabButtonImage.draw(in: rect) - fillColor.setFill() - rect.fill(using: .sourceAtop) - return true - } - let imageLayer = VibrantLayer(forAppearance: isLightTheme ? .light : .dark)! - imageLayer.frame = NSRect(origin: NSPoint(x: newTabButton.bounds.midX - newTabButtonImage.size.width/2, y: newTabButton.bounds.midY - newTabButtonImage.size.height/2), size: newTabButtonImage.size) - imageLayer.contentsGravity = .resizeAspect - imageLayer.contents = newImage - imageLayer.opacity = 0.5 + newTabButtonImageLayer = imageLayer - newTabButtonImageLayer = imageLayer - } - - newTabButtonImageView.isHidden = true newTabButton.layer?.sublayers?.first(where: { $0.className == "VibrantLayer" })?.removeFromSuperlayer() newTabButton.layer?.addSublayer(newTabButtonImageLayer!) } @@ -448,6 +445,13 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { } private func addWindowButtonsBackdrop(titlebarView: NSView, toolbarView: NSView) { + guard windowButtonsBackdrop?.superview != titlebarView else { + /// replacing existing backdrop aggressively + /// may cause incorrect hierarchy + /// + /// because multiple windows are adding this around the 'same time' + return + } windowButtonsBackdrop?.removeFromSuperview() windowButtonsBackdrop = nil @@ -466,16 +470,11 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { private func addWindowDragHandle(titlebarView: NSView, toolbarView: NSView) { // If we already made the view, just make sure it's unhidden and correctly placed as a subview. - if let view = windowDragHandle { - view.removeFromSuperview() - view.isHidden = false - titlebarView.superview?.addSubview(view) - view.leftAnchor.constraint(equalTo: toolbarView.leftAnchor).isActive = true - view.rightAnchor.constraint(equalTo: toolbarView.rightAnchor).isActive = true - view.topAnchor.constraint(equalTo: toolbarView.topAnchor).isActive = true - view.bottomAnchor.constraint(equalTo: toolbarView.topAnchor, constant: 12).isActive = true + guard windowDragHandle?.superview != titlebarView.superview else { + // similar to `addWindowButtonsBackdrop` return } + windowDragHandle?.removeFromSuperview() let view = WindowDragView() view.identifier = NSUserInterfaceItemIdentifier("_windowDragHandle") @@ -536,7 +535,10 @@ fileprivate class WindowButtonsBackdropView: NSView { // This must be weak because the window has this view. Otherwise // a retain cycle occurs. private weak var terminalWindow: TitlebarTabsVenturaTerminalWindow? - private let isLightTheme: Bool + private var isLightTheme: Bool { + // using up-to-date value from hosting window directly + terminalWindow?.isLightTheme ?? false + } private let overlayLayer = VibrantLayer() var isHighlighted: Bool = true { @@ -565,7 +567,6 @@ fileprivate class WindowButtonsBackdropView: NSView { init(window: TitlebarTabsVenturaTerminalWindow) { self.terminalWindow = window - self.isLightTheme = window.isLightTheme super.init(frame: .zero) diff --git a/macos/Sources/Features/Update/UpdateBadge.swift b/macos/Sources/Features/Update/UpdateBadge.swift new file mode 100644 index 000000000..054fdf971 --- /dev/null +++ b/macos/Sources/Features/Update/UpdateBadge.swift @@ -0,0 +1,83 @@ +import SwiftUI + +/// A badge view that displays the current state of an update operation. +/// +/// Shows different visual indicators based on the update state: +/// - Progress ring for downloading/extracting with progress +/// - Animated rotating icon for checking/installing +/// - Static icon for other states +struct UpdateBadge: View { + /// The update view model that provides the current state and progress + @ObservedObject var model: UpdateViewModel + + /// Current rotation angle for animated icon states + @State private var rotationAngle: Double = 0 + + var body: some View { + badgeContent + .accessibilityLabel(model.text) + } + + @ViewBuilder + private var badgeContent: some View { + switch model.state { + case .downloading(let download): + if let expectedLength = download.expectedLength, expectedLength > 0 { + let progress = min(1, max(0, Double(download.progress) / Double(expectedLength))) + ProgressRingView(progress: progress) + } else { + Image(systemName: "arrow.down.circle") + } + + case .extracting(let extracting): + ProgressRingView(progress: min(1, max(0, extracting.progress))) + + case .checking: + if let iconName = model.iconName { + Image(systemName: iconName) + .rotationEffect(.degrees(rotationAngle)) + .onAppear { + withAnimation(.linear(duration: 2.5).repeatForever(autoreverses: false)) { + rotationAngle = 360 + } + } + .onDisappear { + rotationAngle = 0 + } + } else { + EmptyView() + } + + default: + if let iconName = model.iconName { + Image(systemName: iconName) + } else { + EmptyView() + } + } + } +} + +/// A circular progress indicator with a stroke-based ring design. +/// +/// Displays a partially filled circle that represents progress from 0.0 to 1.0. +fileprivate struct ProgressRingView: View { + /// The current progress value, ranging from 0.0 (empty) to 1.0 (complete) + let progress: Double + + /// The width of the progress ring stroke + let lineWidth: CGFloat = 2 + + var body: some View { + ZStack { + Circle() + .stroke(Color.primary.opacity(0.2), lineWidth: lineWidth) + + Circle() + .trim(from: 0, to: progress) + .stroke(Color.primary, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)) + .rotationEffect(.degrees(-90)) + .animation(.easeInOut(duration: 0.2), value: progress) + } + } +} diff --git a/macos/Sources/Features/Update/UpdateController.swift b/macos/Sources/Features/Update/UpdateController.swift new file mode 100644 index 000000000..8a2a939bd --- /dev/null +++ b/macos/Sources/Features/Update/UpdateController.swift @@ -0,0 +1,124 @@ +import Sparkle +import Cocoa +import Combine + +/// Standard controller for managing Sparkle updates in Ghostty. +/// +/// This controller wraps SPUStandardUpdaterController to provide a simpler interface +/// for managing updates with Ghostty's custom driver and delegate. It handles +/// initialization, starting the updater, and provides the check for updates action. +class UpdateController { + private(set) var updater: SPUUpdater + private let userDriver: UpdateDriver + private let updaterDelegate = UpdaterDelegate() + private var installCancellable: AnyCancellable? + + var viewModel: UpdateViewModel { + userDriver.viewModel + } + + /// True if we're installing an update. + var isInstalling: Bool { + installCancellable != nil + } + + /// Initialize a new update controller. + init() { + let hostBundle = Bundle.main + self.userDriver = UpdateDriver( + viewModel: .init(), + hostBundle: hostBundle) + self.updater = SPUUpdater( + hostBundle: hostBundle, + applicationBundle: hostBundle, + userDriver: userDriver, + delegate: updaterDelegate + ) + } + + deinit { + installCancellable?.cancel() + } + + /// Start the updater. + /// + /// This must be called before the updater can check for updates. If starting fails, + /// the error will be shown to the user. + func startUpdater() { + do { + try updater.start() + } catch { + userDriver.viewModel.state = .error(.init( + error: error, + retry: { [weak self] in + self?.userDriver.viewModel.state = .idle + self?.startUpdater() + }, + dismiss: { [weak self] in + self?.userDriver.viewModel.state = .idle + } + )) + } + } + + /// Force install the current update. As long as we're in some "update available" state this will + /// trigger all the steps necessary to complete the update. + func installUpdate() { + // Must be in an installable state + guard viewModel.state.isInstallable else { return } + + // If we're already force installing then do nothing. + guard installCancellable == nil else { return } + + // Setup a combine listener to listen for state changes and to always + // confirm them. If we go to a non-installable state, cancel the listener. + // The sink runs immediately with the current state, so we don't need to + // manually confirm the first state. + installCancellable = viewModel.$state.sink { [weak self] state in + guard let self else { return } + + // If we move to a non-installable state (error, idle, etc.) then we + // stop force installing. + guard state.isInstallable else { + self.installCancellable = nil + return + } + + // Continue the `yes` chain! + state.confirm() + } + } + + /// Check for updates. + /// + /// This is typically connected to a menu item action. + @objc func checkForUpdates() { + // If we're already idle, then just check for updates immediately. + if viewModel.state == .idle { + updater.checkForUpdates() + return + } + + // If we're not idle then we need to cancel any prior state. + installCancellable?.cancel() + viewModel.state.cancel() + + // The above will take time to settle, so we delay the check for some time. + // The 100ms is arbitrary and I'd rather not, but we have to wait more than + // one loop tick it seems. + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { [weak self] in + self?.updater.checkForUpdates() + } + } + + /// Validate the check for updates menu item. + /// + /// - Parameter item: The menu item to validate + /// - Returns: Whether the menu item should be enabled + func validateMenuItem(_ item: NSMenuItem) -> Bool { + if item.action == #selector(checkForUpdates) { + return updater.canCheckForUpdates + } + return true + } +} diff --git a/macos/Sources/Features/Update/UpdateDelegate.swift b/macos/Sources/Features/Update/UpdateDelegate.swift index 4699ba14a..1112c1f44 100644 --- a/macos/Sources/Features/Update/UpdateDelegate.swift +++ b/macos/Sources/Features/Update/UpdateDelegate.swift @@ -6,7 +6,7 @@ class UpdaterDelegate: NSObject, SPUUpdaterDelegate { guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return nil } - + // Sparkle supports a native concept of "channels" but it requires that // you share a single appcast file. We don't want to do that so we // do this instead. diff --git a/macos/Sources/Features/Update/UpdateDriver.swift b/macos/Sources/Features/Update/UpdateDriver.swift new file mode 100644 index 000000000..4bddda809 --- /dev/null +++ b/macos/Sources/Features/Update/UpdateDriver.swift @@ -0,0 +1,207 @@ +import Cocoa +import Sparkle + +/// Implement the SPUUserDriver to modify our UpdateViewModel for custom presentation. +class UpdateDriver: NSObject, SPUUserDriver { + let viewModel: UpdateViewModel + let standard: SPUStandardUserDriver + + init(viewModel: UpdateViewModel, hostBundle: Bundle) { + self.viewModel = viewModel + self.standard = SPUStandardUserDriver(hostBundle: hostBundle, delegate: nil) + super.init() + + NotificationCenter.default.addObserver( + self, + selector: #selector(handleTerminalWindowWillClose), + name: TerminalWindow.terminalWillCloseNotification, + object: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc private func handleTerminalWindowWillClose() { + // If we lost the ability to show unobtrusive states, cancel whatever + // update state we're in. This will allow the manual `check for updates` + // call to initialize the standard driver. + // + // We have to do this after a short delay so that the window can fully + // close. + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) { [weak self] in + guard let self else { return } + guard !hasUnobtrusiveTarget else { return } + viewModel.state.cancel() + viewModel.state = .idle + } + } + + func show(_ request: SPUUpdatePermissionRequest, + reply: @escaping @Sendable (SUUpdatePermissionResponse) -> Void) { + viewModel.state = .permissionRequest(.init(request: request, reply: { [weak viewModel] response in + viewModel?.state = .idle + reply(response) + })) + if !hasUnobtrusiveTarget { + standard.show(request, reply: reply) + } + } + + func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) { + viewModel.state = .checking(.init(cancel: cancellation)) + + if !hasUnobtrusiveTarget { + standard.showUserInitiatedUpdateCheck(cancellation: cancellation) + } + } + + func showUpdateFound(with appcastItem: SUAppcastItem, + state: SPUUserUpdateState, + reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) { + viewModel.state = .updateAvailable(.init(appcastItem: appcastItem, reply: reply)) + if !hasUnobtrusiveTarget { + standard.showUpdateFound(with: appcastItem, state: state, reply: reply) + } + } + + func showUpdateReleaseNotes(with downloadData: SPUDownloadData) { + // We don't do anything with the release notes here because Ghostty + // doesn't use the release notes feature of Sparkle currently. + } + + func showUpdateReleaseNotesFailedToDownloadWithError(_ error: any Error) { + // We don't do anything with release notes. See `showUpdateReleaseNotes` + } + + func showUpdateNotFoundWithError(_ error: any Error, + acknowledgement: @escaping () -> Void) { + viewModel.state = .notFound(.init(acknowledgement: acknowledgement)) + + if !hasUnobtrusiveTarget { + standard.showUpdateNotFoundWithError(error, acknowledgement: acknowledgement) + } + } + + func showUpdaterError(_ error: any Error, + acknowledgement: @escaping () -> Void) { + viewModel.state = .error(.init( + error: error, + retry: { [weak self, weak viewModel] in + viewModel?.state = .idle + DispatchQueue.main.async { [weak self] in + guard let self else { return } + guard let delegate = NSApp.delegate as? AppDelegate else { return } + delegate.checkForUpdates(self) + } + }, + dismiss: { [weak viewModel] in + viewModel?.state = .idle + })) + + if !hasUnobtrusiveTarget { + standard.showUpdaterError(error, acknowledgement: acknowledgement) + } else { + acknowledgement() + } + } + + func showDownloadInitiated(cancellation: @escaping () -> Void) { + viewModel.state = .downloading(.init( + cancel: cancellation, + expectedLength: nil, + progress: 0)) + + if !hasUnobtrusiveTarget { + standard.showDownloadInitiated(cancellation: cancellation) + } + } + + func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) { + guard case let .downloading(downloading) = viewModel.state else { + return + } + + viewModel.state = .downloading(.init( + cancel: downloading.cancel, + expectedLength: expectedContentLength, + progress: 0)) + + if !hasUnobtrusiveTarget { + standard.showDownloadDidReceiveExpectedContentLength(expectedContentLength) + } + } + + func showDownloadDidReceiveData(ofLength length: UInt64) { + guard case let .downloading(downloading) = viewModel.state else { + return + } + + viewModel.state = .downloading(.init( + cancel: downloading.cancel, + expectedLength: downloading.expectedLength, + progress: downloading.progress + length)) + + if !hasUnobtrusiveTarget { + standard.showDownloadDidReceiveData(ofLength: length) + } + } + + func showDownloadDidStartExtractingUpdate() { + viewModel.state = .extracting(.init(progress: 0)) + + if !hasUnobtrusiveTarget { + standard.showDownloadDidStartExtractingUpdate() + } + } + + func showExtractionReceivedProgress(_ progress: Double) { + viewModel.state = .extracting(.init(progress: progress)) + + if !hasUnobtrusiveTarget { + standard.showExtractionReceivedProgress(progress) + } + } + + func showReady(toInstallAndRelaunch reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) { + viewModel.state = .readyToInstall(.init(reply: reply)) + + if !hasUnobtrusiveTarget { + standard.showReady(toInstallAndRelaunch: reply) + } + } + + func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) { + viewModel.state = .installing(.init(retryTerminatingApplication: retryTerminatingApplication)) + + if !hasUnobtrusiveTarget { + standard.showInstallingUpdate(withApplicationTerminated: applicationTerminated, retryTerminatingApplication: retryTerminatingApplication) + } + } + + func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) { + standard.showUpdateInstalledAndRelaunched(relaunched, acknowledgement: acknowledgement) + viewModel.state = .idle + } + + func showUpdateInFocus() { + if !hasUnobtrusiveTarget { + standard.showUpdateInFocus() + } + } + + func dismissUpdateInstallation() { + viewModel.state = .idle + standard.dismissUpdateInstallation() + } + + // MARK: No-Window Fallback + + /// True if there is a target that can render our unobtrusive update checker. + var hasUnobtrusiveTarget: Bool { + NSApp.windows.contains { window in + window is TerminalWindow && + window.isVisible + } + } +} diff --git a/macos/Sources/Features/Update/UpdatePill.swift b/macos/Sources/Features/Update/UpdatePill.swift new file mode 100644 index 000000000..29d1669e1 --- /dev/null +++ b/macos/Sources/Features/Update/UpdatePill.swift @@ -0,0 +1,81 @@ +import SwiftUI + +/// A pill-shaped button that displays update status and provides access to update actions. +struct UpdatePill: View { + /// The update view model that provides the current state and information + @ObservedObject var model: UpdateViewModel + + /// Whether the update popover is currently visible + @State private var showPopover = false + + /// Task for auto-dismissing the "No Updates" state + @State private var resetTask: Task? + + /// The font used for the pill text + private let textFont = NSFont.systemFont(ofSize: 11, weight: .medium) + + var body: some View { + if !model.state.isIdle { + pillButton + .popover(isPresented: $showPopover, arrowEdge: .bottom) { + UpdatePopoverView(model: model) + } + .transition(.opacity.combined(with: .scale(scale: 0.95))) + .onChange(of: model.state) { newState in + resetTask?.cancel() + if case .notFound(let notFound) = newState { + resetTask = Task { [weak model] in + try? await Task.sleep(for: .seconds(5)) + guard !Task.isCancelled, case .notFound? = model?.state else { return } + model?.state = .idle + notFound.acknowledgement() + } + } else { + resetTask = nil + } + } + } + } + + /// The pill-shaped button view that displays the update badge and text + @ViewBuilder + private var pillButton: some View { + Button(action: { + if case .notFound(let notFound) = model.state { + model.state = .idle + notFound.acknowledgement() + } else { + showPopover.toggle() + } + }) { + HStack(spacing: 6) { + UpdateBadge(model: model) + .frame(width: 14, height: 14) + + Text(model.text) + .font(Font(textFont)) + .lineLimit(1) + .truncationMode(.tail) + .frame(width: textWidth) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background( + Capsule() + .fill(model.backgroundColor) + ) + .foregroundColor(model.foregroundColor) + .contentShape(Capsule()) + } + .buttonStyle(.plain) + .help(model.text) + .accessibilityLabel(model.text) + } + + /// Calculated width for the text to prevent resizing during progress updates + private var textWidth: CGFloat? { + let attributes: [NSAttributedString.Key: Any] = [.font: textFont] + let size = (model.maxWidthText as NSString).size(withAttributes: attributes) + return size.width + } +} diff --git a/macos/Sources/Features/Update/UpdatePopoverView.swift b/macos/Sources/Features/Update/UpdatePopoverView.swift new file mode 100644 index 000000000..770b9aedd --- /dev/null +++ b/macos/Sources/Features/Update/UpdatePopoverView.swift @@ -0,0 +1,417 @@ +import SwiftUI +import Sparkle + +/// A popover view that displays detailed update information and action buttons. +/// +/// The view adapts its content based on the current update state, showing appropriate +/// UI for checking, downloading, installing, or handling errors. +struct UpdatePopoverView: View { + /// The update view model that provides the current state and information + @ObservedObject var model: UpdateViewModel + + /// Environment value for dismissing the popover + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + switch model.state { + case .idle: + // Shouldn't happen in a well-formed view stack. Higher levels + // should not call the popover for idles. + EmptyView() + + case .permissionRequest(let request): + PermissionRequestView(request: request, dismiss: dismiss) + + case .checking(let checking): + CheckingView(checking: checking, dismiss: dismiss) + + case .updateAvailable(let update): + UpdateAvailableView(update: update, dismiss: dismiss) + + case .downloading(let download): + DownloadingView(download: download, dismiss: dismiss) + + case .extracting(let extracting): + ExtractingView(extracting: extracting) + + case .readyToInstall(let ready): + ReadyToInstallView(ready: ready, dismiss: dismiss) + + case .installing(let installing): + InstallingView(installing: installing, dismiss: dismiss) + + case .notFound(let notFound): + NotFoundView(notFound: notFound, dismiss: dismiss) + + case .error(let error): + UpdateErrorView(error: error, dismiss: dismiss) + } + } + .frame(width: 300) + } +} + +fileprivate struct PermissionRequestView: View { + let request: UpdateState.PermissionRequest + let dismiss: DismissAction + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Text("Enable automatic updates?") + .font(.system(size: 13, weight: .semibold)) + + Text("Ghostty can automatically check for updates in the background.") + .font(.system(size: 11)) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + + HStack(spacing: 8) { + Button("Not Now") { + request.reply(SUUpdatePermissionResponse( + automaticUpdateChecks: false, + sendSystemProfile: false)) + dismiss() + } + .keyboardShortcut(.cancelAction) + + Spacer() + + Button("Allow") { + request.reply(SUUpdatePermissionResponse( + automaticUpdateChecks: true, + sendSystemProfile: false)) + dismiss() + } + .keyboardShortcut(.defaultAction) + .buttonStyle(.borderedProminent) + } + } + .padding(16) + } +} + +fileprivate struct CheckingView: View { + let checking: UpdateState.Checking + let dismiss: DismissAction + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 10) { + ProgressView() + .controlSize(.small) + Text("Checking for updates…") + .font(.system(size: 13)) + } + + HStack { + Spacer() + Button("Cancel") { + checking.cancel() + dismiss() + } + .keyboardShortcut(.cancelAction) + .controlSize(.small) + } + } + .padding(16) + } +} + +fileprivate struct UpdateAvailableView: View { + let update: UpdateState.UpdateAvailable + let dismiss: DismissAction + + private let labelWidth: CGFloat = 60 + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: 8) { + Text("Update Available") + .font(.system(size: 13, weight: .semibold)) + + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 6) { + Text("Version:") + .foregroundColor(.secondary) + .frame(width: labelWidth, alignment: .trailing) + Text(update.appcastItem.displayVersionString) + } + .font(.system(size: 11)) + + if update.appcastItem.contentLength > 0 { + HStack(spacing: 6) { + Text("Size:") + .foregroundColor(.secondary) + .frame(width: labelWidth, alignment: .trailing) + Text(ByteCountFormatter.string(fromByteCount: Int64(update.appcastItem.contentLength), countStyle: .file)) + } + .font(.system(size: 11)) + } + + if let date = update.appcastItem.date { + HStack(spacing: 6) { + Text("Released:") + .foregroundColor(.secondary) + .frame(width: labelWidth, alignment: .trailing) + Text(date.formatted(date: .abbreviated, time: .omitted)) + } + .font(.system(size: 11)) + } + } + .textSelection(.enabled) + } + + HStack(spacing: 8) { + Button("Skip") { + update.reply(.skip) + dismiss() + } + .controlSize(.small) + + Button("Later") { + update.reply(.dismiss) + dismiss() + } + .controlSize(.small) + .keyboardShortcut(.cancelAction) + + Spacer() + + Button("Install") { + update.reply(.install) + dismiss() + } + .keyboardShortcut(.defaultAction) + .buttonStyle(.borderedProminent) + .controlSize(.small) + } + } + .padding(16) + + if let notes = update.releaseNotes { + Divider() + + Link(destination: notes.url) { + HStack { + Image(systemName: "doc.text") + .font(.system(size: 11)) + Text(notes.label) + .font(.system(size: 11, weight: .medium)) + Spacer() + Image(systemName: "arrow.up.right") + .font(.system(size: 10)) + } + .foregroundColor(.primary) + .padding(12) + .frame(maxWidth: .infinity) + .background(Color(nsColor: .controlBackgroundColor)) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + } + } +} + +fileprivate struct DownloadingView: View { + let download: UpdateState.Downloading + let dismiss: DismissAction + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Text("Downloading Update") + .font(.system(size: 13, weight: .semibold)) + + if let expectedLength = download.expectedLength, expectedLength > 0 { + let progress = min(1, max(0, Double(download.progress) / Double(expectedLength))) + VStack(alignment: .leading, spacing: 6) { + ProgressView(value: progress) + Text(String(format: "%.0f%%", progress * 100)) + .font(.system(size: 11)) + .foregroundColor(.secondary) + } + } else { + ProgressView() + .controlSize(.small) + } + } + + HStack { + Spacer() + Button("Cancel") { + download.cancel() + dismiss() + } + .keyboardShortcut(.cancelAction) + .controlSize(.small) + } + } + .padding(16) + } +} + +fileprivate struct ExtractingView: View { + let extracting: UpdateState.Extracting + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Preparing Update") + .font(.system(size: 13, weight: .semibold)) + + VStack(alignment: .leading, spacing: 6) { + ProgressView(value: min(1, max(0, extracting.progress)), total: 1.0) + Text(String(format: "%.0f%%", min(1, max(0, extracting.progress)) * 100)) + .font(.system(size: 11)) + .foregroundColor(.secondary) + } + } + .padding(16) + } +} + +fileprivate struct ReadyToInstallView: View { + let ready: UpdateState.ReadyToInstall + let dismiss: DismissAction + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Text("Ready to Install") + .font(.system(size: 13, weight: .semibold)) + + Text("The update is ready to install.") + .font(.system(size: 11)) + .foregroundColor(.secondary) + } + + HStack(spacing: 8) { + Button("Later") { + ready.reply(.dismiss) + dismiss() + } + .keyboardShortcut(.cancelAction) + .controlSize(.small) + + Spacer() + + Button("Install and Relaunch") { + ready.reply(.install) + dismiss() + } + .keyboardShortcut(.defaultAction) + .buttonStyle(.borderedProminent) + .controlSize(.small) + } + } + .padding(16) + } +} + +fileprivate struct InstallingView: View { + let installing: UpdateState.Installing + let dismiss: DismissAction + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Text("Restart Required") + .font(.system(size: 13, weight: .semibold)) + + Text("The update is ready. Please restart the application to complete the installation.") + .font(.system(size: 11)) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + + HStack { + Spacer() + Button("Restart Now") { + installing.retryTerminatingApplication() + dismiss() + } + .keyboardShortcut(.defaultAction) + .buttonStyle(.borderedProminent) + .controlSize(.small) + } + } + .padding(16) + } +} + +fileprivate struct NotFoundView: View { + let notFound: UpdateState.NotFound + let dismiss: DismissAction + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Text("No Updates Found") + .font(.system(size: 13, weight: .semibold)) + + Text("You're already running the latest version.") + .font(.system(size: 11)) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + + HStack { + Spacer() + Button("OK") { + notFound.acknowledgement() + dismiss() + } + .keyboardShortcut(.defaultAction) + .controlSize(.small) + } + } + .padding(16) + } +} + +fileprivate struct UpdateErrorView: View { + let error: UpdateState.Error + let dismiss: DismissAction + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 8) { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.orange) + .font(.system(size: 13)) + Text("Update Failed") + .font(.system(size: 13, weight: .semibold)) + } + + Text(error.error.localizedDescription) + .font(.system(size: 11)) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + + HStack(spacing: 8) { + Button("OK") { + error.dismiss() + dismiss() + } + .keyboardShortcut(.cancelAction) + .controlSize(.small) + + Spacer() + + Button("Retry") { + error.retry() + dismiss() + } + .keyboardShortcut(.defaultAction) + .controlSize(.small) + } + } + .padding(16) + } +} diff --git a/macos/Sources/Features/Update/UpdateSimulator.swift b/macos/Sources/Features/Update/UpdateSimulator.swift new file mode 100644 index 000000000..c855282c0 --- /dev/null +++ b/macos/Sources/Features/Update/UpdateSimulator.swift @@ -0,0 +1,289 @@ +import Foundation +import Sparkle + +/// Simulates various update scenarios for testing the update UI. +/// +/// The expected usage is by overriding the `checkForUpdates` function in AppDelegate and +/// calling one of these instead. This will allow us to test the update flows without having to use +/// real updates. +enum UpdateSimulator { + /// Complete successful update flow: checking → available → download → extract → ready → install → idle + case happyPath + + /// No updates available: checking (2s) → "No Updates Available" (3s) → idle + case notFound + + /// Error during check: checking (2s) → error with retry callback + case error + + /// Slower download for testing progress UI: checking → available → download (20 steps, ~10s) → extract → install + case slowDownload + + /// Initial permission request flow: shows permission dialog → proceeds with happy path if accepted + case permissionRequest + + /// User cancels during download: checking → available → download (5 steps) → cancels → idle + case cancelDuringDownload + + /// User cancels while checking: checking (1s) → cancels → idle + case cancelDuringChecking + + /// Shows the installing state with restart button: installing (stays until dismissed) + case installing + + func simulate(with viewModel: UpdateViewModel) { + switch self { + case .happyPath: + simulateHappyPath(viewModel) + case .notFound: + simulateNotFound(viewModel) + case .error: + simulateError(viewModel) + case .slowDownload: + simulateSlowDownload(viewModel) + case .permissionRequest: + simulatePermissionRequest(viewModel) + case .cancelDuringDownload: + simulateCancelDuringDownload(viewModel) + case .cancelDuringChecking: + simulateCancelDuringChecking(viewModel) + case .installing: + simulateInstalling(viewModel) + } + } + + private func simulateHappyPath(_ viewModel: UpdateViewModel) { + viewModel.state = .checking(.init(cancel: { + viewModel.state = .idle + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + viewModel.state = .updateAvailable(.init( + appcastItem: SUAppcastItem.empty(), + reply: { choice in + if choice == .install { + simulateDownload(viewModel) + } else { + viewModel.state = .idle + } + } + )) + } + } + + private func simulateNotFound(_ viewModel: UpdateViewModel) { + viewModel.state = .checking(.init(cancel: { + viewModel.state = .idle + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + viewModel.state = .notFound(.init(acknowledgement: { + // Acknowledgement called when dismissed + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { + viewModel.state = .idle + } + } + } + + private func simulateError(_ viewModel: UpdateViewModel) { + viewModel.state = .checking(.init(cancel: { + viewModel.state = .idle + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + viewModel.state = .error(.init( + error: NSError(domain: "UpdateError", code: 1, userInfo: [ + NSLocalizedDescriptionKey: "Failed to check for updates" + ]), + retry: { + simulateHappyPath(viewModel) + }, + dismiss: { + viewModel.state = .idle + } + )) + } + } + + private func simulateSlowDownload(_ viewModel: UpdateViewModel) { + viewModel.state = .checking(.init(cancel: { + viewModel.state = .idle + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + viewModel.state = .updateAvailable(.init( + appcastItem: SUAppcastItem.empty(), + reply: { choice in + if choice == .install { + simulateSlowDownloadProgress(viewModel) + } else { + viewModel.state = .idle + } + } + )) + } + } + + private func simulateSlowDownloadProgress(_ viewModel: UpdateViewModel) { + let download = UpdateState.Downloading( + cancel: { + viewModel.state = .idle + }, + expectedLength: nil, + progress: 0 + ) + viewModel.state = .downloading(download) + + for i in 1...20 { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.5) { + let updatedDownload = UpdateState.Downloading( + cancel: download.cancel, + expectedLength: 2000, + progress: UInt64(i * 100) + ) + viewModel.state = .downloading(updatedDownload) + + if i == 20 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + simulateExtract(viewModel) + } + } + } + } + } + + private func simulatePermissionRequest(_ viewModel: UpdateViewModel) { + let request = SPUUpdatePermissionRequest(systemProfile: []) + viewModel.state = .permissionRequest(.init( + request: request, + reply: { response in + if response.automaticUpdateChecks { + simulateHappyPath(viewModel) + } else { + viewModel.state = .idle + } + } + )) + } + + private func simulateCancelDuringDownload(_ viewModel: UpdateViewModel) { + viewModel.state = .checking(.init(cancel: { + viewModel.state = .idle + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + viewModel.state = .updateAvailable(.init( + appcastItem: SUAppcastItem.empty(), + reply: { choice in + if choice == .install { + simulateDownloadThenCancel(viewModel) + } else { + viewModel.state = .idle + } + } + )) + } + } + + private func simulateDownloadThenCancel(_ viewModel: UpdateViewModel) { + let download = UpdateState.Downloading( + cancel: { + viewModel.state = .idle + }, + expectedLength: nil, + progress: 0 + ) + viewModel.state = .downloading(download) + + for i in 1...5 { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { + let updatedDownload = UpdateState.Downloading( + cancel: download.cancel, + expectedLength: 1000, + progress: UInt64(i * 100) + ) + viewModel.state = .downloading(updatedDownload) + + if i == 5 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + viewModel.state = .idle + } + } + } + } + } + + private func simulateCancelDuringChecking(_ viewModel: UpdateViewModel) { + viewModel.state = .checking(.init(cancel: { + viewModel.state = .idle + })) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + viewModel.state = .idle + } + } + + private func simulateDownload(_ viewModel: UpdateViewModel) { + let download = UpdateState.Downloading( + cancel: { + viewModel.state = .idle + }, + expectedLength: nil, + progress: 0 + ) + viewModel.state = .downloading(download) + + for i in 1...10 { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { + let updatedDownload = UpdateState.Downloading( + cancel: download.cancel, + expectedLength: 1000, + progress: UInt64(i * 100) + ) + viewModel.state = .downloading(updatedDownload) + + if i == 10 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + simulateExtract(viewModel) + } + } + } + } + } + + private func simulateExtract(_ viewModel: UpdateViewModel) { + viewModel.state = .extracting(.init(progress: 0.0)) + + for j in 1...5 { + DispatchQueue.main.asyncAfter(deadline: .now() + Double(j) * 0.3) { + viewModel.state = .extracting(.init(progress: Double(j) / 5.0)) + + if j == 5 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + viewModel.state = .readyToInstall(.init( + reply: { choice in + if choice == .install { + viewModel.state = .installing(.init(retryTerminatingApplication: { + print("Restart button clicked in simulator - resetting to idle") + viewModel.state = .idle + })) + } else { + viewModel.state = .idle + } + } + )) + } + } + } + } + } + + private func simulateInstalling(_ viewModel: UpdateViewModel) { + viewModel.state = .installing(.init(retryTerminatingApplication: { + print("Restart button clicked in simulator - resetting to idle") + viewModel.state = .idle + })) + } +} diff --git a/macos/Sources/Features/Update/UpdateViewModel.swift b/macos/Sources/Features/Update/UpdateViewModel.swift new file mode 100644 index 000000000..7a92337cc --- /dev/null +++ b/macos/Sources/Features/Update/UpdateViewModel.swift @@ -0,0 +1,389 @@ +import Foundation +import SwiftUI +import Sparkle + +class UpdateViewModel: ObservableObject { + @Published var state: UpdateState = .idle + + /// The text to display for the current update state. + /// Returns an empty string for idle state, progress percentages for downloading/extracting, + /// or descriptive text for other states. + var text: String { + switch state { + case .idle: + return "" + case .permissionRequest: + return "Enable Automatic Updates?" + case .checking: + return "Checking for Updates…" + case .updateAvailable(let update): + let version = update.appcastItem.displayVersionString + if !version.isEmpty { + return "Update Available: \(version)" + } + return "Update Available" + case .downloading(let download): + if let expectedLength = download.expectedLength, expectedLength > 0 { + let progress = Double(download.progress) / Double(expectedLength) + return String(format: "Downloading: %.0f%%", progress * 100) + } + return "Downloading…" + case .extracting(let extracting): + return String(format: "Preparing: %.0f%%", extracting.progress * 100) + case .readyToInstall: + return "Ready to Install Update" + case .installing: + return "Restart to Complete Update" + case .notFound: + return "No Updates Available" + case .error(let err): + return err.error.localizedDescription + } + } + + /// The maximum width text for states that show progress. + /// Used to prevent the pill from resizing as percentages change. + var maxWidthText: String { + switch state { + case .downloading: + return "Downloading: 100%" + case .extracting: + return "Preparing: 100%" + default: + return text + } + } + + /// The SF Symbol icon name for the current update state. + var iconName: String? { + switch state { + case .idle: + return nil + case .permissionRequest: + return "questionmark.circle" + case .checking: + return "arrow.triangle.2.circlepath" + case .updateAvailable: + return "shippingbox.fill" + case .downloading: + return "arrow.down.circle" + case .extracting: + return "shippingbox" + case .readyToInstall: + return "restart.circle.fill" + case .installing: + return "power.circle" + case .notFound: + return "info.circle" + case .error: + return "exclamationmark.triangle.fill" + } + } + + /// A longer description for the current update state. + /// Used in contexts like the command palette where more detail is helpful. + var description: String { + switch state { + case .idle: + return "" + case .permissionRequest: + return "Configure automatic update preferences" + case .checking: + return "Please wait while we check for available updates" + case .updateAvailable(let update): + return update.releaseNotes?.label ?? "Download and install the latest version" + case .downloading: + return "Downloading the update package" + case .extracting: + return "Extracting and preparing the update" + case .readyToInstall: + return "Update is ready to install" + case .installing: + return "Installing update and preparing to restart" + case .notFound: + return "You are running the latest version" + case .error: + return "An error occurred during the update process" + } + } + + /// A badge to display for the current update state. + /// Returns version numbers, progress percentages, or nil. + var badge: String? { + switch state { + case .updateAvailable(let update): + let version = update.appcastItem.displayVersionString + return version.isEmpty ? nil : version + case .downloading(let download): + if let expectedLength = download.expectedLength, expectedLength > 0 { + let percentage = Double(download.progress) / Double(expectedLength) * 100 + return String(format: "%.0f%%", percentage) + } + return nil + case .extracting(let extracting): + return String(format: "%.0f%%", extracting.progress * 100) + default: + return nil + } + } + + /// The color to apply to the icon for the current update state. + var iconColor: Color { + switch state { + case .idle: + return .secondary + case .permissionRequest: + return .white + case .checking: + return .secondary + case .updateAvailable, .readyToInstall: + return .accentColor + case .downloading, .extracting, .installing: + return .secondary + case .notFound: + return .secondary + case .error: + return .orange + } + } + + /// The background color for the update pill. + var backgroundColor: Color { + switch state { + case .permissionRequest: + return Color(nsColor: NSColor.systemBlue.blended(withFraction: 0.3, of: .black) ?? .systemBlue) + case .updateAvailable: + return .accentColor + case .readyToInstall: + return Color(nsColor: NSColor.systemGreen.blended(withFraction: 0.3, of: .black) ?? .systemGreen) + case .notFound: + return Color(nsColor: NSColor.systemBlue.blended(withFraction: 0.5, of: .black) ?? .systemBlue) + case .error: + return .orange.opacity(0.2) + default: + return Color(nsColor: .controlBackgroundColor) + } + } + + /// The foreground (text) color for the update pill. + var foregroundColor: Color { + switch state { + case .permissionRequest: + return .white + case .updateAvailable, .readyToInstall: + return .white + case .notFound: + return .white + case .error: + return .orange + default: + return .primary + } + } +} + +enum UpdateState: Equatable { + case idle + case permissionRequest(PermissionRequest) + case checking(Checking) + case updateAvailable(UpdateAvailable) + case notFound(NotFound) + case error(Error) + case downloading(Downloading) + case extracting(Extracting) + case readyToInstall(ReadyToInstall) + case installing(Installing) + + var isIdle: Bool { + if case .idle = self { return true } + return false + } + + /// This is true if we're in a state that can be force installed. + var isInstallable: Bool { + switch (self) { + case .checking, + .updateAvailable, + .downloading, + .extracting, + .readyToInstall, + .installing: + return true + + default: + return false + } + } + + func cancel() { + switch self { + case .checking(let checking): + checking.cancel() + case .updateAvailable(let available): + available.reply(.dismiss) + case .downloading(let downloading): + downloading.cancel() + case .readyToInstall(let ready): + ready.reply(.dismiss) + case .notFound(let notFound): + notFound.acknowledgement() + case .error(let err): + err.dismiss() + default: + break + } + } + + /// Confirms or accepts the current update state. + /// - For available updates: begins installation + /// - For ready-to-install: proceeds with installation + func confirm() { + switch self { + case .updateAvailable(let available): + available.reply(.install) + case .readyToInstall(let ready): + ready.reply(.install) + default: + break + } + } + + static func == (lhs: UpdateState, rhs: UpdateState) -> Bool { + switch (lhs, rhs) { + case (.idle, .idle): + return true + case (.permissionRequest, .permissionRequest): + return true + case (.checking, .checking): + return true + case (.updateAvailable(let lUpdate), .updateAvailable(let rUpdate)): + return lUpdate.appcastItem.displayVersionString == rUpdate.appcastItem.displayVersionString + case (.notFound, .notFound): + return true + case (.error(let lErr), .error(let rErr)): + return lErr.error.localizedDescription == rErr.error.localizedDescription + case (.downloading(let lDown), .downloading(let rDown)): + return lDown.progress == rDown.progress && lDown.expectedLength == rDown.expectedLength + case (.extracting(let lExt), .extracting(let rExt)): + return lExt.progress == rExt.progress + case (.readyToInstall, .readyToInstall): + return true + case (.installing, .installing): + return true + default: + return false + } + } + + struct NotFound { + let acknowledgement: () -> Void + } + + struct PermissionRequest { + let request: SPUUpdatePermissionRequest + let reply: @Sendable (SUUpdatePermissionResponse) -> Void + } + + struct Checking { + let cancel: () -> Void + } + + struct UpdateAvailable { + let appcastItem: SUAppcastItem + let reply: @Sendable (SPUUserUpdateChoice) -> Void + + var releaseNotes: ReleaseNotes? { + let currentCommit = Bundle.main.infoDictionary?["GhosttyCommit"] as? String + return ReleaseNotes(displayVersionString: appcastItem.displayVersionString, currentCommit: currentCommit) + } + } + + enum ReleaseNotes { + case commit(URL) + case compareTip(URL) + case tagged(URL) + + init?(displayVersionString: String, currentCommit: String?) { + let version = displayVersionString + + // Check for semantic version (x.y.z) + if let semver = Self.extractSemanticVersion(from: version) { + let slug = semver.replacingOccurrences(of: ".", with: "-") + if let url = URL(string: "https://ghostty.org/docs/install/release-notes/\(slug)") { + self = .tagged(url) + return + } + } + + // Fall back to git hash detection + guard let newHash = Self.extractGitHash(from: version) else { + return nil + } + + if let currentHash = currentCommit, !currentHash.isEmpty, + let url = URL(string: "https://github.com/ghostty-org/ghostty/compare/\(currentHash)...\(newHash)") { + self = .compareTip(url) + } else if let url = URL(string: "https://github.com/ghostty-org/ghostty/commit/\(newHash)") { + self = .commit(url) + } else { + return nil + } + } + + private static func extractSemanticVersion(from version: String) -> String? { + let pattern = #"^\d+\.\d+\.\d+$"# + if version.range(of: pattern, options: .regularExpression) != nil { + return version + } + return nil + } + + private static func extractGitHash(from version: String) -> String? { + let pattern = #"[0-9a-f]{7,40}"# + if let range = version.range(of: pattern, options: .regularExpression) { + return String(version[range]) + } + return nil + } + + var url: URL { + switch self { + case .commit(let url): return url + case .compareTip(let url): return url + case .tagged(let url): return url + } + } + + var label: String { + switch (self) { + case .commit: return "View GitHub Commit" + case .compareTip: return "Changes Since This Tip Release" + case .tagged: return "View Release Notes" + } + } + } + + struct Error { + let error: any Swift.Error + let retry: () -> Void + let dismiss: () -> Void + } + + struct Downloading { + let cancel: () -> Void + let expectedLength: UInt64? + let progress: UInt64 + } + + struct Extracting { + let progress: Double + } + + struct ReadyToInstall { + let reply: @Sendable (SPUUserUpdateChoice) -> Void + } + + struct Installing { + let retryTerminatingApplication: () -> Void + } +} diff --git a/macos/Sources/Ghostty/Ghostty.Action.swift b/macos/Sources/Ghostty/Ghostty.Action.swift index 37b1a362d..4921ef8df 100644 --- a/macos/Sources/Ghostty/Ghostty.Action.swift +++ b/macos/Sources/Ghostty/Ghostty.Action.swift @@ -100,6 +100,18 @@ extension Ghostty.Action { let state: State let progress: UInt8? } + + struct Scrollbar { + let total: UInt64 + let offset: UInt64 + let len: UInt64 + + init(c: ghostty_action_scrollbar_s) { + total = c.total + offset = c.offset + len = c.len + } + } } // 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 bdc64e9e1..3db8e7a11 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -571,6 +571,9 @@ extension Ghostty { case GHOSTTY_ACTION_REDO: return redo(app, target: target) + case GHOSTTY_ACTION_SCROLLBAR: + scrollbar(app, target: target, v: action.action.scrollbar) + case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS: fallthrough case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW: @@ -637,8 +640,9 @@ extension Ghostty { switch action.kind { case .text: - // Open with the default text editor - if let textEditor = NSWorkspace.shared.defaultTextEditor { + // Open with the default editor for `*.ghostty` file or just system text editor + let editor = NSWorkspace.shared.defaultApplicationURL(forExtension: url.pathExtension) ?? NSWorkspace.shared.defaultTextEditor + if let textEditor = editor { NSWorkspace.shared.open([url], withApplicationAt: textEditor, configuration: NSWorkspace.OpenConfiguration()) return true } @@ -1025,26 +1029,38 @@ extension Ghostty { guard let surfaceView = self.surfaceView(from: surface) else { return false } guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { return false } - // For now, we return false if the window has no splits and we return - // true if the window has ANY splits. This isn't strictly correct because - // we should only be returning true if we actually performed the action, - // but this handles the most common case of caring about goto_split performability - // which is the no-split case. + // If the window has no splits, the action is not performable guard controller.surfaceTree.isSplit else { return false } + // Convert the C API direction to our Swift type + guard let splitDirection = SplitFocusDirection.from(direction: direction) else { return false } + + // Find the current node in the tree + guard let targetNode = controller.surfaceTree.root?.node(view: surfaceView) else { return false } + + // Check if a split actually exists in the target direction before + // returning true. This ensures performable keybinds only consume + // the key event when we actually perform navigation. + let focusDirection: SplitTree.FocusDirection = splitDirection.toSplitTreeFocusDirection() + guard controller.surfaceTree.focusTarget(for: focusDirection, from: targetNode) != nil else { + return false + } + + // We have a valid target, post the notification to perform the navigation NotificationCenter.default.post( name: Notification.ghosttyFocusSplit, object: surfaceView, userInfo: [ - Notification.SplitDirectionKey: SplitFocusDirection.from(direction: direction) as Any, + Notification.SplitDirectionKey: splitDirection as Any, ] ) + return true + default: assertionFailure() + return false } - - return true } private static func resizeSplit( @@ -1559,6 +1575,33 @@ extension Ghostty { } } + private static func scrollbar( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_action_scrollbar_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("scrollbar 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 } + + let scrollbar = Ghostty.Action.Scrollbar(c: v) + NotificationCenter.default.post( + name: .ghosttyDidUpdateScrollbar, + object: surfaceView, + userInfo: [ + SwiftUI.Notification.Name.ScrollbarKey: scrollbar + ] + ) + + default: + assertionFailure() + } + } + private static func configReload( _ app: ghostty_app_t, target: ghostty_target_s, diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 05a3be2cd..f380345c7 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -314,17 +314,14 @@ extension Ghostty { var macosCustomIcon: String { #if os(macOS) - let homeDirURL = FileManager.default.homeDirectoryForCurrentUser - let ghosttyConfigIconPath = homeDirURL.appendingPathComponent( - ".config/ghostty/Ghostty.icns", - conformingTo: .fileURL).path() - let defaultValue = ghosttyConfigIconPath + let defaultValue = NSString("~/.config/ghostty/Ghostty.icns").expandingTildeInPath guard let config = self.config else { return defaultValue } var v: UnsafePointer? = nil let key = "macos-custom-icon" guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue } guard let ptr = v else { return defaultValue } - return String(cString: ptr) + guard let path = NSString(utf8String: ptr) else { return defaultValue } + return path.expandingTildeInPath #else return "" #endif @@ -606,6 +603,17 @@ extension Ghostty { let str = String(cString: ptr) return MacShortcuts(rawValue: str) ?? defaultValue } + + var scrollbar: Scrollbar { + let defaultValue = Scrollbar.system + guard let config = self.config else { return defaultValue } + var v: UnsafePointer? = nil + let key = "scrollbar" + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue } + guard let ptr = v else { return defaultValue } + let str = String(cString: ptr) + return Scrollbar(rawValue: str) ?? defaultValue + } } } @@ -644,6 +652,11 @@ extension Ghostty.Config { case ask } + enum Scrollbar: String { + case system + case never + } + enum ResizeOverlay : String { case always case never diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 85040d390..26804be78 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -223,7 +223,38 @@ extension Ghostty { } } } +} +#if canImport(AppKit) +// MARK: SplitFocusDirection Extensions + +extension Ghostty.SplitFocusDirection { + /// Convert to a SplitTree.FocusDirection for the given ViewType. + func toSplitTreeFocusDirection() -> SplitTree.FocusDirection { + switch self { + case .previous: + return .previous + + case .next: + return .next + + case .up: + return .spatial(.up) + + case .down: + return .spatial(.down) + + case .left: + return .spatial(.left) + + case .right: + return .spatial(.right) + } + } +} +#endif + +extension Ghostty { /// The type of a clipboard request enum ClipboardRequest { /// A direct paste of clipboard contents @@ -344,6 +375,10 @@ extension Notification.Name { /// Toggle maximize of current window static let ghosttyMaximizeDidToggle = Notification.Name("com.mitchellh.ghostty.maximizeDidToggle") + + /// Notification sent when scrollbar updates + static let ghosttyDidUpdateScrollbar = Notification.Name("com.mitchellh.ghostty.didUpdateScrollbar") + static let ScrollbarKey = ghosttyDidUpdateScrollbar.rawValue + ".scrollbar" } // NOTE: I am moving all of these to Notification.Name extensions over time. This diff --git a/macos/Sources/Ghostty/SurfaceScrollView.swift b/macos/Sources/Ghostty/SurfaceScrollView.swift new file mode 100644 index 000000000..b1e1b9baf --- /dev/null +++ b/macos/Sources/Ghostty/SurfaceScrollView.swift @@ -0,0 +1,273 @@ +import SwiftUI +import Combine + +/// Wraps a Ghostty surface view in an NSScrollView to provide native macOS scrollbar support. +/// +/// ## Coordinate System +/// AppKit uses a +Y-up coordinate system (origin at bottom-left), while terminals conceptually +/// use +Y-down (row 0 at top). This class handles the inversion when converting between row +/// offsets and pixel positions. +/// +/// ## Architecture +/// - `scrollView`: The outermost NSScrollView that manages scrollbar rendering and behavior +/// - `documentView`: A blank NSView whose height represents total scrollback (in pixels) +/// - `surfaceView`: The actual Ghostty renderer, positioned to fill the visible rect +class SurfaceScrollView: NSView { + private let scrollView: NSScrollView + private let documentView: NSView + private let surfaceView: Ghostty.SurfaceView + private var observers: [NSObjectProtocol] = [] + private var cancellables: Set = [] + private var isLiveScrolling = false + + /// The last row position sent via scroll_to_row action. Used to avoid + /// sending redundant actions when the user drags the scrollbar but stays + /// on the same row. + private var lastSentRow: Int? + + init(contentSize: CGSize, surfaceView: Ghostty.SurfaceView) { + self.surfaceView = surfaceView + // The scroll view is our outermost view that controls all our scrollbar + // rendering and behavior. + scrollView = NSScrollView() + scrollView.hasVerticalScroller = false + scrollView.hasHorizontalScroller = false + scrollView.autohidesScrollers = true + scrollView.usesPredominantAxisScrolling = true + // hide default background to show blur effect properly + scrollView.drawsBackground = false + + // The document view is what the scrollview is actually going + // to be directly scrolling. We set it up to a "blank" NSView + // with the desired content size. + documentView = NSView(frame: NSRect(origin: .zero, size: contentSize)) + scrollView.documentView = documentView + + // The document view contains our actual surface as a child. + // We synchronize the scrolling of the document with this surface + // so that our primary Ghostty renderer only needs to render the viewport. + documentView.addSubview(surfaceView) + + super.init(frame: .zero) + + // Our scroll view is our only view + addSubview(scrollView) + + // Apply initial scrollbar settings + synchronizeAppearance() + + // We listen for scroll events through bounds notifications on our NSClipView. + // This is based on: https://christiantietze.de/posts/2018/07/synchronize-nsscrollview/ + scrollView.contentView.postsBoundsChangedNotifications = true + observers.append(NotificationCenter.default.addObserver( + forName: NSView.boundsDidChangeNotification, + object: scrollView.contentView, + queue: .main + ) { [weak self] notification in + self?.handleScrollChange(notification) + }) + + // Listen for scrollbar updates from Ghostty + observers.append(NotificationCenter.default.addObserver( + forName: .ghosttyDidUpdateScrollbar, + object: surfaceView, + queue: .main + ) { [weak self] notification in + self?.handleScrollbarUpdate(notification) + }) + + // Listen for live scroll events + observers.append(NotificationCenter.default.addObserver( + forName: NSScrollView.willStartLiveScrollNotification, + object: scrollView, + queue: .main + ) { [weak self] _ in + self?.isLiveScrolling = true + }) + + observers.append(NotificationCenter.default.addObserver( + forName: NSScrollView.didEndLiveScrollNotification, + object: scrollView, + queue: .main + ) { [weak self] _ in + self?.isLiveScrolling = false + }) + + observers.append(NotificationCenter.default.addObserver( + forName: NSScrollView.didLiveScrollNotification, + object: scrollView, + queue: .main + ) { [weak self] _ in + self?.handleLiveScroll() + }) + + // Listen for derived config changes to update scrollbar settings live + surfaceView.$derivedConfig + .sink { [weak self] _ in + DispatchQueue.main.async { [weak self] in + self?.synchronizeAppearance() + } + } + .store(in: &cancellables) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) not implemented") + } + + deinit { + observers.forEach { NotificationCenter.default.removeObserver($0) } + } + + // The entire bounds is a safe area, so we override any default + // insets. This is necessary for the content view to match the + // surface view if we have the "hidden" titlebar style. + override var safeAreaInsets: NSEdgeInsets { return NSEdgeInsetsZero } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + + // Force layout to be called to fix up our various subviews. + needsLayout = true + } + + override func layout() { + super.layout() + + // Fill entire bounds with scroll view + scrollView.frame = bounds + + // Use contentSize to account for visible scrollers + // + // Only update sizes if we have a valid (non-zero) content size. The content size + // can be zero when this is added early to a view, or to an invisible hierarchy. + // Practically, this happened in the quick terminal. + var contentSize = scrollView.contentSize + guard contentSize.width > 0 && contentSize.height > 0 else { + synchronizeSurfaceView() + return + } + + // If we have a legacy scrollbar and its not visible, then we account for that + // in advance, because legacy scrollbars change our contentSize and force reflow + // of our terminal which is not desirable. + // See: https://github.com/ghostty-org/ghostty/discussions/9254 + let style = scrollView.verticalScroller?.scrollerStyle ?? NSScroller.preferredScrollerStyle + if style == .legacy { + if (scrollView.verticalScroller?.isHidden ?? true) { + let scrollerWidth = NSScroller.scrollerWidth(for: .regular, scrollerStyle: .legacy) + contentSize.width -= scrollerWidth + } + } + + // Keep document width synchronized with content width + documentView.setFrameSize(CGSize( + width: contentSize.width, + height: documentView.frame.height + )) + + // Inform the actual pty of our size change. This doesn't change the actual view + // frame because we do want to render the whole thing, but it will prevent our + // rows/cols from going into the non-content area. + surfaceView.sizeDidChange(contentSize) + + // When our scrollview changes make sure our surface view is synchronized + synchronizeSurfaceView() + } + + // MARK: Scrolling + + private func synchronizeAppearance() { + let scrollbarConfig = surfaceView.derivedConfig.scrollbar + scrollView.hasVerticalScroller = scrollbarConfig != .never + } + + /// Positions the surface view to fill the currently visible rectangle. + /// + /// This is called whenever the scroll position changes. The surface view (which does the + /// actual terminal rendering) always fills exactly the visible portion of the document view, + /// so the renderer only needs to render what's currently on screen. + private func synchronizeSurfaceView() { + let visibleRect = scrollView.contentView.documentVisibleRect + surfaceView.frame = visibleRect + } + + // MARK: Notifications + + /// Handles bounds changes in the scroll view's clip view, keeping the surface view synchronized. + private func handleScrollChange(_ notification: Notification) { + synchronizeSurfaceView() + } + + /// Handles live scroll events (user actively dragging the scrollbar). + /// + /// Converts the current scroll position to a row number and sends a `scroll_to_row` action + /// to the terminal core. Only sends actions when the row changes to avoid IPC spam. + private func handleLiveScroll() { + // If our cell height is currently zero then we avoid a div by zero below + // and just don't scroll (there's no where to scroll anyways). This can + // happen with a tiny terminal. + let cellHeight = surfaceView.cellSize.height + guard cellHeight > 0 else { return } + + // AppKit views are +Y going up, so we calculate from the bottom + let visibleRect = scrollView.contentView.documentVisibleRect + let documentHeight = documentView.frame.height + let scrollOffset = documentHeight - visibleRect.origin.y - visibleRect.height + let row = Int(scrollOffset / cellHeight) + + // Only send action if the row changed to avoid action spam + guard row != lastSentRow else { return } + lastSentRow = row + + // Use the keybinding action to scroll. + _ = surfaceView.surfaceModel?.perform(action: "scroll_to_row:\(row)") + } + + /// Handles scrollbar state updates from the terminal core. + /// + /// Updates the document view size to reflect total scrollback and adjusts scroll position + /// to match the terminal's viewport. During live scrolling, updates document size but skips + /// programmatic position changes to avoid fighting the user's drag. + /// + /// ## Scrollbar State + /// The scrollbar struct contains: + /// - `total`: Total rows in scrollback + active area + /// - `offset`: First visible row (0 = top of history) + /// - `len`: Number of visible rows (viewport height) + private func handleScrollbarUpdate(_ notification: Notification) { + guard let scrollbar = notification.userInfo?[SwiftUI.Notification.Name.ScrollbarKey] as? Ghostty.Action.Scrollbar else { + return + } + + // Convert row units to pixels using cell height, ignore zero height. + let cellHeight = surfaceView.cellSize.height + guard cellHeight > 0 else { return } + + // The full document height must include the vertical padding around the cell + // grid, otherwise the content view ends up misaligned with the surface. + let documentGridHeight = CGFloat(scrollbar.total) * cellHeight + let gridHeight = CGFloat(scrollbar.len) * cellHeight + let padding = scrollView.contentSize.height - gridHeight + let documentHeight = documentGridHeight + padding + + // Our width should be the content width to account for visible scrollers. + // We don't do horizontal scrolling in terminals. + let newSize = CGSize(width: scrollView.contentSize.width, height: documentHeight) + documentView.setFrameSize(newSize) + + // Only update our actual scroll position if we're not actively scrolling. + if !isLiveScrolling { + // Invert coordinate system: terminal offset is from top, AppKit position from bottom + let offsetY = CGFloat(scrollbar.total - scrollbar.offset - scrollbar.len) * cellHeight + scrollView.contentView.scroll(to: CGPoint(x: 0, y: offsetY)) + + // Track the current row position to avoid redundant movements when we + // move the scrollbar. + lastSentRow = Int(scrollbar.offset) + } + + // Always update our scrolled view with the latest dimensions + scrollView.reflectScrolledClipView(scrollView.contentView) + } +} diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index aca17c0fc..c650bdf8f 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -386,10 +386,6 @@ extension Ghostty { /// A surface is terminology in Ghostty for a terminal surface, or a place where a terminal is actually drawn /// and interacted with. The word "surface" is used because a surface may represent a window, a tab, /// a split, a small preview pane, etc. It is ANYTHING that has a terminal drawn to it. - /// - /// We just wrap an AppKit NSView here at the moment so that we can behave as low level as possible - /// since that is what the Metal renderer in Ghostty expects. In the future, it may make more sense to - /// wrap an MTKView and use that, but for legacy reasons we didn't do that to begin with. struct SurfaceRepresentable: OSViewRepresentable { /// The view to render for the terminal surface. let view: SurfaceView @@ -404,16 +400,26 @@ extension Ghostty { /// The best approach is to wrap this view in a GeometryReader and pass in the geo.size. let size: CGSize + #if canImport(AppKit) + func makeOSView(context: Context) -> SurfaceScrollView { + // On macOS, wrap the surface view in a scroll view + return SurfaceScrollView(contentSize: size, surfaceView: view) + } + + func updateOSView(_ scrollView: SurfaceScrollView, context: Context) { + // Our scrollview always takes up the full size. + scrollView.frame.size = size + } + #else func makeOSView(context: Context) -> SurfaceView { - // We need the view as part of the state to be created previously because - // the view is sent to the Ghostty API so that it can manipulate it - // directly since we draw on a render thread. - return view; + // On iOS, return the surface view directly + return view } func updateOSView(_ view: SurfaceView, context: Context) { view.sizeDidChange(size) } + #endif } /// The configuration for a surface. For any configuration not set, defaults will be chosen from diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 22784d164..410646f6f 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1027,7 +1027,7 @@ extension Ghostty { // If we are in a keyDown then we don't need to redispatch a command-modded // key event (see docs for this field) so reset this to nil because - // `interpretKeyEvents` may dispach it. + // `interpretKeyEvents` may dispatch it. self.lastPerformKeyEvent = nil self.interpretKeyEvents([translationEvent]) @@ -1532,6 +1532,7 @@ extension Ghostty { let macosWindowShadow: Bool let windowTitleFontFamily: String? let windowAppearance: NSAppearance? + let scrollbar: Ghostty.Config.Scrollbar init() { self.backgroundColor = Color(NSColor.windowBackgroundColor) @@ -1539,6 +1540,7 @@ extension Ghostty { self.macosWindowShadow = true self.windowTitleFontFamily = nil self.windowAppearance = nil + self.scrollbar = .system } init(_ config: Ghostty.Config) { @@ -1547,6 +1549,7 @@ extension Ghostty { self.macosWindowShadow = config.macosWindowShadow self.windowTitleFontFamily = config.windowTitleFontFamily self.windowAppearance = .init(ghosttyConfig: config) + self.scrollbar = config.scrollbar } } diff --git a/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift b/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift index f46106004..a8eb7b876 100644 --- a/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift @@ -5,6 +5,13 @@ extension NSScreen { var displayID: UInt32? { deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? UInt32 } + + /// The stable UUID for this display, suitable for tracking across reconnects and NSScreen garbage collection. + var displayUUID: UUID? { + guard let displayID = displayID else { return nil } + guard let cfuuid = CGDisplayCreateUUIDFromDisplayID(displayID)?.takeRetainedValue() else { return nil } + return UUID(cfuuid) + } // Returns true if the given screen has a visible dock. This isn't // point-in-time visible, this is true if the dock is always visible diff --git a/macos/Sources/Helpers/Extensions/UUID+Extension.swift b/macos/Sources/Helpers/Extensions/UUID+Extension.swift new file mode 100644 index 000000000..e536353c5 --- /dev/null +++ b/macos/Sources/Helpers/Extensions/UUID+Extension.swift @@ -0,0 +1,9 @@ +import Foundation + +extension UUID { + /// Initialize a UUID from a CFUUID. + init?(_ cfuuid: CFUUID) { + guard let uuidString = CFUUIDCreateString(nil, cfuuid) as String? else { return nil } + self.init(uuidString: uuidString) + } +} diff --git a/macos/Sources/Helpers/NonDraggableHostingView.swift b/macos/Sources/Helpers/NonDraggableHostingView.swift new file mode 100644 index 000000000..26238182f --- /dev/null +++ b/macos/Sources/Helpers/NonDraggableHostingView.swift @@ -0,0 +1,13 @@ +import SwiftUI + +/// An NSHostingView subclass that prevents window dragging when clicking on the view. +/// +/// By default, NSHostingViews in the titlebar allow the window to be dragged when +/// clicked. This subclass overrides `mouseDownCanMoveWindow` to return false, +/// preventing the window from being dragged when the user clicks on this view. +/// +/// This is useful for titlebar accessories that contain interactive elements +/// (buttons, links, etc.) where you don't want accidental window dragging. +class NonDraggableHostingView: NSHostingView { + override var mouseDownCanMoveWindow: Bool { false } +} diff --git a/macos/Tests/Update/ReleaseNotesTests.swift b/macos/Tests/Update/ReleaseNotesTests.swift new file mode 100644 index 000000000..b029fa6bc --- /dev/null +++ b/macos/Tests/Update/ReleaseNotesTests.swift @@ -0,0 +1,130 @@ +import Testing +import Foundation +@testable import Ghostty + +struct ReleaseNotesTests { + /// Test tagged release (semantic version) + @Test func testTaggedRelease() async throws { + let notes = UpdateState.ReleaseNotes( + displayVersionString: "1.2.3", + currentCommit: nil + ) + + #expect(notes != nil) + if case .tagged(let url) = notes { + #expect(url.absoluteString == "https://ghostty.org/docs/install/release-notes/1-2-3") + #expect(notes?.label == "View Release Notes") + } else { + Issue.record("Expected tagged case") + } + } + + /// Test tip release comparison with current commit + @Test func testTipReleaseComparison() async throws { + let notes = UpdateState.ReleaseNotes( + displayVersionString: "tip-abc1234", + currentCommit: "def5678" + ) + + #expect(notes != nil) + if case .compareTip(let url) = notes { + #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/compare/def5678...abc1234") + #expect(notes?.label == "Changes Since This Tip Release") + } else { + Issue.record("Expected compareTip case") + } + } + + /// Test tip release without current commit + @Test func testTipReleaseWithoutCurrentCommit() async throws { + let notes = UpdateState.ReleaseNotes( + displayVersionString: "tip-abc1234", + currentCommit: nil + ) + + #expect(notes != nil) + if case .commit(let url) = notes { + #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/commit/abc1234") + #expect(notes?.label == "View GitHub Commit") + } else { + Issue.record("Expected commit case") + } + } + + /// Test tip release with empty current commit + @Test func testTipReleaseWithEmptyCurrentCommit() async throws { + let notes = UpdateState.ReleaseNotes( + displayVersionString: "tip-abc1234", + currentCommit: "" + ) + + #expect(notes != nil) + if case .commit(let url) = notes { + #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/commit/abc1234") + } else { + Issue.record("Expected commit case") + } + } + + /// Test version with full 40-character hash + @Test func testFullGitHash() async throws { + let notes = UpdateState.ReleaseNotes( + displayVersionString: "tip-1234567890abcdef1234567890abcdef12345678", + currentCommit: nil + ) + + #expect(notes != nil) + if case .commit(let url) = notes { + #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/commit/1234567890abcdef1234567890abcdef12345678") + } else { + Issue.record("Expected commit case") + } + } + + /// Test version with no recognizable pattern + @Test func testInvalidVersion() async throws { + let notes = UpdateState.ReleaseNotes( + displayVersionString: "unknown-version", + currentCommit: nil + ) + + #expect(notes == nil) + } + + /// Test semantic version with prerelease suffix should not match + @Test func testSemanticVersionWithSuffix() async throws { + let notes = UpdateState.ReleaseNotes( + displayVersionString: "1.2.3-beta", + currentCommit: nil + ) + + // Should not match semantic version pattern, falls back to hash detection + #expect(notes == nil) + } + + /// Test semantic version with 4 components should not match + @Test func testSemanticVersionFourComponents() async throws { + let notes = UpdateState.ReleaseNotes( + displayVersionString: "1.2.3.4", + currentCommit: nil + ) + + // Should not match pattern + #expect(notes == nil) + } + + /// Test version string with git hash embedded + @Test func testVersionWithEmbeddedHash() async throws { + let notes = UpdateState.ReleaseNotes( + displayVersionString: "v2024.01.15-abc1234", + currentCommit: "def5678" + ) + + #expect(notes != nil) + if case .compareTip(let url) = notes { + #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/compare/def5678...abc1234") + } else { + Issue.record("Expected compareTip case") + } + } +} diff --git a/macos/Tests/Update/UpdateStateTests.swift b/macos/Tests/Update/UpdateStateTests.swift new file mode 100644 index 000000000..269cd3153 --- /dev/null +++ b/macos/Tests/Update/UpdateStateTests.swift @@ -0,0 +1,116 @@ +import Testing +import Foundation +import Sparkle +@testable import Ghostty + +struct UpdateStateTests { + // MARK: - Equatable Tests + + @Test func testIdleEquality() { + let state1: UpdateState = .idle + let state2: UpdateState = .idle + #expect(state1 == state2) + } + + @Test func testCheckingEquality() { + let state1: UpdateState = .checking(.init(cancel: {})) + let state2: UpdateState = .checking(.init(cancel: {})) + #expect(state1 == state2) + } + + @Test func testNotFoundEquality() { + let state1: UpdateState = .notFound(.init(acknowledgement: {})) + let state2: UpdateState = .notFound(.init(acknowledgement: {})) + #expect(state1 == state2) + } + + @Test func testInstallingEquality() { + let state1: UpdateState = .installing(.init(retryTerminatingApplication: {})) + let state2: UpdateState = .installing(.init(retryTerminatingApplication: {})) + #expect(state1 == state2) + } + + @Test func testPermissionRequestEquality() { + let request1 = SPUUpdatePermissionRequest(systemProfile: []) + let request2 = SPUUpdatePermissionRequest(systemProfile: []) + let state1: UpdateState = .permissionRequest(.init(request: request1, reply: { _ in })) + let state2: UpdateState = .permissionRequest(.init(request: request2, reply: { _ in })) + #expect(state1 == state2) + } + + @Test func testReadyToInstallEquality() { + let state1: UpdateState = .readyToInstall(.init(reply: { _ in })) + let state2: UpdateState = .readyToInstall(.init(reply: { _ in })) + #expect(state1 == state2) + } + + @Test func testDownloadingEqualityWithSameProgress() { + let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) + let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) + #expect(state1 == state2) + } + + @Test func testDownloadingInequalityWithDifferentProgress() { + let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) + let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 600)) + #expect(state1 != state2) + } + + @Test func testDownloadingInequalityWithDifferentExpectedLength() { + let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) + let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: 2000, progress: 500)) + #expect(state1 != state2) + } + + @Test func testDownloadingEqualityWithNilExpectedLength() { + let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: nil, progress: 500)) + let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: nil, progress: 500)) + #expect(state1 == state2) + } + + @Test func testExtractingEqualityWithSameProgress() { + let state1: UpdateState = .extracting(.init(progress: 0.5)) + let state2: UpdateState = .extracting(.init(progress: 0.5)) + #expect(state1 == state2) + } + + @Test func testExtractingInequalityWithDifferentProgress() { + let state1: UpdateState = .extracting(.init(progress: 0.5)) + let state2: UpdateState = .extracting(.init(progress: 0.6)) + #expect(state1 != state2) + } + + @Test func testErrorEqualityWithSameDescription() { + let error1 = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Error message"]) + let error2 = NSError(domain: "Test", code: 2, userInfo: [NSLocalizedDescriptionKey: "Error message"]) + let state1: UpdateState = .error(.init(error: error1, retry: {}, dismiss: {})) + let state2: UpdateState = .error(.init(error: error2, retry: {}, dismiss: {})) + #expect(state1 == state2) + } + + @Test func testErrorInequalityWithDifferentDescription() { + let error1 = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Error 1"]) + let error2 = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Error 2"]) + let state1: UpdateState = .error(.init(error: error1, retry: {}, dismiss: {})) + let state2: UpdateState = .error(.init(error: error2, retry: {}, dismiss: {})) + #expect(state1 != state2) + } + + @Test func testDifferentStatesAreNotEqual() { + let state1: UpdateState = .idle + let state2: UpdateState = .checking(.init(cancel: {})) + #expect(state1 != state2) + } + + // MARK: - isIdle Tests + + @Test func testIsIdleTrue() { + let state: UpdateState = .idle + #expect(state.isIdle == true) + } + + @Test func testIsIdleFalse() { + let state: UpdateState = .checking(.init(cancel: {})) + #expect(state.isIdle == false) + } +} diff --git a/macos/Tests/Update/UpdateViewModelTests.swift b/macos/Tests/Update/UpdateViewModelTests.swift new file mode 100644 index 000000000..e41804e08 --- /dev/null +++ b/macos/Tests/Update/UpdateViewModelTests.swift @@ -0,0 +1,97 @@ +import Testing +import Foundation +import SwiftUI +import Sparkle +@testable import Ghostty + +struct UpdateViewModelTests { + // MARK: - Text Formatting Tests + + @Test func testIdleText() { + let viewModel = UpdateViewModel() + viewModel.state = .idle + #expect(viewModel.text == "") + } + + @Test func testPermissionRequestText() { + let viewModel = UpdateViewModel() + let request = SPUUpdatePermissionRequest(systemProfile: []) + viewModel.state = .permissionRequest(.init(request: request, reply: { _ in })) + #expect(viewModel.text == "Enable Automatic Updates?") + } + + @Test func testCheckingText() { + let viewModel = UpdateViewModel() + viewModel.state = .checking(.init(cancel: {})) + #expect(viewModel.text == "Checking for Updates…") + } + + @Test func testDownloadingTextWithKnownLength() { + let viewModel = UpdateViewModel() + viewModel.state = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) + #expect(viewModel.text == "Downloading: 50%") + } + + @Test func testDownloadingTextWithUnknownLength() { + let viewModel = UpdateViewModel() + viewModel.state = .downloading(.init(cancel: {}, expectedLength: nil, progress: 500)) + #expect(viewModel.text == "Downloading…") + } + + @Test func testDownloadingTextWithZeroExpectedLength() { + let viewModel = UpdateViewModel() + viewModel.state = .downloading(.init(cancel: {}, expectedLength: 0, progress: 500)) + #expect(viewModel.text == "Downloading…") + } + + @Test func testExtractingText() { + let viewModel = UpdateViewModel() + viewModel.state = .extracting(.init(progress: 0.75)) + #expect(viewModel.text == "Preparing: 75%") + } + + @Test func testReadyToInstallText() { + let viewModel = UpdateViewModel() + viewModel.state = .readyToInstall(.init(reply: { _ in })) + #expect(viewModel.text == "Ready to Install Update") + } + + @Test func testInstallingText() { + let viewModel = UpdateViewModel() + viewModel.state = .installing(.init(retryTerminatingApplication: {})) + #expect(viewModel.text == "Restart to Complete Update") + } + + @Test func testNotFoundText() { + let viewModel = UpdateViewModel() + viewModel.state = .notFound(.init(acknowledgement: {})) + #expect(viewModel.text == "No Updates Available") + } + + @Test func testErrorText() { + let viewModel = UpdateViewModel() + let error = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Network error"]) + viewModel.state = .error(.init(error: error, retry: {}, dismiss: {})) + #expect(viewModel.text == "Network error") + } + + // MARK: - Max Width Text Tests + + @Test func testMaxWidthTextForDownloading() { + let viewModel = UpdateViewModel() + viewModel.state = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 50)) + #expect(viewModel.maxWidthText == "Downloading: 100%") + } + + @Test func testMaxWidthTextForExtracting() { + let viewModel = UpdateViewModel() + viewModel.state = .extracting(.init(progress: 0.5)) + #expect(viewModel.maxWidthText == "Preparing: 100%") + } + + @Test func testMaxWidthTextForNonProgressState() { + let viewModel = UpdateViewModel() + viewModel.state = .checking(.init(cancel: {})) + #expect(viewModel.maxWidthText == viewModel.text) + } +} diff --git a/nix/build-support/check-zig-cache.sh b/nix/build-support/check-zig-cache.sh index e92a27b6f..9a3927846 100755 --- a/nix/build-support/check-zig-cache.sh +++ b/nix/build-support/check-zig-cache.sh @@ -79,7 +79,7 @@ elif [ "$1" != "--update" ]; then exit 1 fi -zon2nix "$BUILD_ZIG_ZON" --14 --nix "$WORK_DIR/build.zig.zon.nix" --txt "$WORK_DIR/build.zig.zon.txt" --json "$WORK_DIR/build.zig.zon.json" --flatpak "$WORK_DIR/zig-packages.json" +zon2nix "$BUILD_ZIG_ZON" --15 --nix "$WORK_DIR/build.zig.zon.nix" --txt "$WORK_DIR/build.zig.zon.txt" --json "$WORK_DIR/build.zig.zon.json" --flatpak "$WORK_DIR/zig-packages.json" alejandra --quiet "$WORK_DIR/build.zig.zon.nix" prettier --log-level warn --write "$WORK_DIR/build.zig.zon.json" prettier --log-level warn --write "$WORK_DIR/zig-packages.json" diff --git a/nix/build-support/update-mirror.nu b/nix/build-support/update-mirror.nu new file mode 100755 index 000000000..8571ddea6 --- /dev/null +++ b/nix/build-support/update-mirror.nu @@ -0,0 +1,96 @@ +#!/usr/bin/env nu + +# This script downloads external dependencies from build.zig.zon.json that +# are not already mirrored at deps.files.ghostty.org, saves them to a local +# directory, and updates build.zig.zon to point to the new mirror URLs. +# +# The downloaded files are unmodified so their checksums and content hashes +# will match the originals. +# +# After running this script, the files in the output directory can be uploaded +# to blob storage, and build.zig.zon will already be updated with the new URLs. +def main [ + --output: string = "tmp-mirror", # Output directory for the mirrored files + --prefix: string = "https://deps.files.ghostty.org/", # Final URL prefix to ignore + --dry-run, # Print what would be downloaded without downloading +] { + let script_dir = ($env.CURRENT_FILE | path dirname) + let input_file = ($script_dir | path join ".." ".." "build.zig.zon.json") + let zon_file = ($script_dir | path join ".." ".." "build.zig.zon") + let output_dir = $output + + # Ensure the output directory exists + mkdir $output_dir + + # Read and parse the JSON file + let deps = open $input_file + + # Track URL replacements for build.zig.zon + mut url_replacements = [] + + # Process each dependency + for entry in ($deps | transpose key value) { + let key = $entry.key + let name = $entry.value.name + let url = $entry.value.url + + # Skip URLs that don't start with http(s) + if not ($url | str starts-with "http") { + continue + } + + # Skip URLs already hosted at the prefix + if ($url | str starts-with $prefix) { + continue + } + + # Extract the file extension from the URL + let extension = ($url | parse -r '(\.[a-z0-9]+(?:\.[a-z0-9]+)?)$' | get -o capture0.0 | default "") + + # Try to extract commit hash (40 hex chars) from URL + let commit_hash = ($url | parse -r '([a-f0-9]{40})' | get -o capture0.0 | default "") + + # Try to extract date pattern (YYYY-MM-DD or YYYYMMDD with optional suffixes) + let date_pattern = ($url | parse -r '((?:release-)?20\d{2}(?:-?\d{2}){2}(?:[-]\d+)*(?:[-][a-z0-9]+)?)' | get -o capture0.0 | default "") + + # Build filename based on what we found + let filename = if (not ($commit_hash | is-empty)) { + $"($name)-($commit_hash)($extension)" + } else if (not ($date_pattern | is-empty)) { + $"($name)-($date_pattern)($extension)" + } else { + $"($key)($extension)" + } + let new_url = $"($prefix)($filename)" + print $"($url) -> ($filename)" + + # Track the replacement + $url_replacements = ($url_replacements | append {old: $url, new: $new_url}) + + # Download the file + if not $dry_run { + http get $url | save -f ($output_dir | path join $filename) + } + } + + if $dry_run { + print "Dry run complete - no files were downloaded\n" + print $"Would update ($url_replacements | length) URLs in build.zig.zon" + } else { + print "All dependencies downloaded successfully\n" + print $"Updating ($zon_file)..." + + # Backup the old file + let backup_file = $"($zon_file).bak" + cp $zon_file $backup_file + print $"Backed up to ($backup_file)" + + mut zon_content = (open $zon_file) + for replacement in $url_replacements { + $zon_content = ($zon_content | str replace $replacement.old $replacement.new) + } + $zon_content | save -f $zon_file + + print $"Updated ($url_replacements | length) URLs in build.zig.zon" + } +} diff --git a/nix/build-support/update-mirror.sh b/nix/build-support/update-mirror.sh deleted file mode 100755 index f346572ed..000000000 --- a/nix/build-support/update-mirror.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh -# -# This script generates a directory that can be uploaded to blob -# storage to mirror our dependencies. The dependencies are unmodified -# so their checksum and content hashes will match. - -set -e # Exit immediately if a command exits with a non-zero status - -SCRIPT_PATH="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)" -INPUT_FILE="$SCRIPT_PATH/../../build.zig.zon2json-lock" -OUTPUT_DIR="blob" - -# Ensure the output directory exists -mkdir -p "$OUTPUT_DIR" - -# Use jq to iterate over the JSON and download files -jq -r 'to_entries[] | "\(.key) \(.value.name) \(.value.url)"' "$INPUT_FILE" | while read -r key name url; do - # Skip URLs that don't start with http(s). They aren't necessary for - # our mirror. - if ! echo "$url" | grep -Eq "^https?://"; then - continue - fi - - # Extract the file extension from the URL - extension=$(echo "$url" | grep -oE '\.[a-z0-9]+(\.[a-z0-9]+)?$') - - filename="${name}-${key}${extension}" - echo "$url -> $filename" - curl -L -o "$OUTPUT_DIR/$filename" "$url" -done diff --git a/nix/package.nix b/nix/package.nix index fcc80b9dc..3d00648ec 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -10,7 +10,7 @@ git, ncurses, pkg-config, - zig_0_14, + zig_0_15, pandoc, revision ? "dirty", optimize ? "Debug", @@ -27,7 +27,7 @@ # 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_14.hook.overrideAttrs { + 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 { @@ -40,7 +40,7 @@ in stdenv.mkDerivation (finalAttrs: { pname = "ghostty"; - version = "1.2.1"; + version = "1.3.0-dev"; # We limit source like this to try and reduce the amount of rebuilds as possible # thus we only provide the source that is needed for the build diff --git a/pkg/apple-sdk/build.zig b/pkg/apple-sdk/build.zig index 18a6c0968..c573c3910 100644 --- a/pkg/apple-sdk/build.zig +++ b/pkg/apple-sdk/build.zig @@ -46,19 +46,19 @@ pub fn addPaths( // find the SDK path. const libc = try std.zig.LibCInstallation.findNative(.{ .allocator = b.allocator, - .target = step.rootModuleTarget(), + .target = &step.rootModuleTarget(), .verbose = false, }); // Render the file compatible with the `--libc` Zig flag. - var list: std.ArrayList(u8) = .init(b.allocator); - defer list.deinit(); - try libc.render(list.writer()); + var stream: std.io.Writer.Allocating = .init(b.allocator); + defer stream.deinit(); + try libc.render(&stream.writer); // Create a temporary file to store the libc path because // `--libc` expects a file path. const wf = b.addWriteFiles(); - const path = wf.add("libc.txt", list.items); + const path = wf.add("libc.txt", stream.written()); // Determine our framework path. Zig has a bug where it doesn't // parse this from the libc txt file for `-framework` flags: diff --git a/pkg/breakpad/build.zig b/pkg/breakpad/build.zig index 9ab6b89cd..56d51b159 100644 --- a/pkg/breakpad/build.zig +++ b/pkg/breakpad/build.zig @@ -19,9 +19,8 @@ pub fn build(b: *std.Build) !void { try apple_sdk.addPaths(b, lib); } - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{}); + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); if (b.lazyDependency("breakpad", .{})) |upstream| { lib.addIncludePath(upstream.path("src")); diff --git a/pkg/cimgui/build.zig b/pkg/cimgui/build.zig index f14bc1242..b94f11943 100644 --- a/pkg/cimgui/build.zig +++ b/pkg/cimgui/build.zig @@ -55,19 +55,19 @@ pub fn build(b: *std.Build) !void { if (imgui_) |imgui| lib.addIncludePath(imgui.path("")); module.addIncludePath(b.path("vendor")); - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + 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(&.{ + try flags.appendSlice(b.allocator, &.{ "-DIMGUI_IMPL_API=extern\t\"C\"\t__declspec(dllexport)", }); } else { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DIMGUI_IMPL_API=extern\t\"C\"", }); } diff --git a/pkg/fontconfig/build.zig b/pkg/fontconfig/build.zig index c9ea517ed..7c87d1f2e 100644 --- a/pkg/fontconfig/build.zig +++ b/pkg/fontconfig/build.zig @@ -82,9 +82,9 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu lib.addIncludePath(b.path("override/include")); module.addIncludePath(b.path("override/include")); - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ "-DHAVE_DIRENT_H", "-DHAVE_FCNTL_H", "-DHAVE_STDLIB_H", @@ -129,12 +129,12 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu }); switch (target.result.ptrBitWidth()) { - 32 => try flags.appendSlice(&.{ + 32 => try flags.appendSlice(b.allocator, &.{ "-DSIZEOF_VOID_P=4", "-DALIGNOF_VOID_P=4", }), - 64 => try flags.appendSlice(&.{ + 64 => try flags.appendSlice(b.allocator, &.{ "-DSIZEOF_VOID_P=8", "-DALIGNOF_VOID_P=8", }), @@ -142,14 +142,14 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu else => @panic("unsupported arch"), } if (target.result.os.tag == .windows) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DFC_CACHEDIR=\"LOCAL_APPDATA_FONTCONFIG_CACHE\"", "-DFC_TEMPLATEDIR=\"c:/share/fontconfig/conf.avail\"", "-DCONFIGDIR=\"c:/etc/fonts/conf.d\"", "-DFC_DEFAULT_FONTS=\"\\tWINDOWSFONTDIR\\n\\tWINDOWSUSERFONTDIR\\n\"", }); } else { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DHAVE_FSTATFS", "-DHAVE_FSTATVFS", "-DHAVE_GETOPT", @@ -173,13 +173,13 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu }); if (target.result.os.tag == .freebsd) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DFC_TEMPLATEDIR=\"/usr/local/etc/fonts/conf.avail\"", "-DFONTCONFIG_PATH=\"/usr/local/etc/fonts\"", "-DCONFIGDIR=\"/usr/local/etc/fonts/conf.d\"", }); } else { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DFC_TEMPLATEDIR=\"/usr/share/fontconfig/conf.avail\"", "-DFONTCONFIG_PATH=\"/etc/fonts\"", "-DCONFIGDIR=\"/usr/local/fontconfig/conf.d\"", @@ -187,7 +187,7 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu } if (target.result.os.tag == .linux) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DHAVE_SYS_STATFS_H", "-DHAVE_SYS_VFS_H", }); @@ -214,14 +214,14 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu // Libxml2 _ = b.systemIntegrationOption("libxml2", .{}); // So it shows up in help if (libxml2_enabled) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DENABLE_LIBXML2", "-DLIBXML_STATIC", "-DLIBXML_PUSH_ENABLED", }); if (target.result.os.tag == .windows) { // NOTE: this should be defined on all targets - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-Werror=implicit-function-declaration", }); } diff --git a/pkg/freetype/build.zig b/pkg/freetype/build.zig index d000442be..a25dc18da 100644 --- a/pkg/freetype/build.zig +++ b/pkg/freetype/build.zig @@ -77,9 +77,9 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu try apple_sdk.addPaths(b, lib); } - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ "-DFT2_BUILD_LIBRARY", "-DFT_CONFIG_OPTION_SYSTEM_ZLIB=1", @@ -103,7 +103,7 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu // Libpng _ = b.systemIntegrationOption("libpng", .{}); // So it shows up in help if (libpng_enabled) { - try flags.append("-DFT_CONFIG_OPTION_USE_PNG=1"); + try flags.append(b.allocator, "-DFT_CONFIG_OPTION_USE_PNG=1"); if (b.systemIntegrationOption("libpng", .{})) { lib.linkSystemLibrary2("libpng", dynamic_link_opts); diff --git a/pkg/freetype/face.zig b/pkg/freetype/face.zig index 84178b860..f8714d4fe 100644 --- a/pkg/freetype/face.zig +++ b/pkg/freetype/face.zig @@ -193,8 +193,8 @@ pub const Face = struct { ) void { c.FT_Set_Transform( self.handle, - @constCast(@ptrCast(matrix)), - @constCast(@ptrCast(delta)), + @ptrCast(@constCast(matrix)), + @ptrCast(@constCast(delta)), ); } }; diff --git a/pkg/freetype/main.zig b/pkg/freetype/main.zig index b39650423..6ec818181 100644 --- a/pkg/freetype/main.zig +++ b/pkg/freetype/main.zig @@ -9,6 +9,7 @@ pub const Library = @import("Library.zig"); pub const Error = errors.Error; pub const Face = face.Face; +pub const LoadFlags = face.LoadFlags; pub const Tag = tag.Tag; pub const mulFix = computations.mulFix; diff --git a/pkg/glslang/build.zig b/pkg/glslang/build.zig index 52993a662..746a41497 100644 --- a/pkg/glslang/build.zig +++ b/pkg/glslang/build.zig @@ -59,9 +59,9 @@ fn buildGlslang( try apple_sdk.addPaths(b, lib); } - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ "-fno-sanitize=undefined", "-fno-sanitize-trap=undefined", }); diff --git a/pkg/gtk4-layer-shell/build.zig b/pkg/gtk4-layer-shell/build.zig index 543faf129..818b48f45 100644 --- a/pkg/gtk4-layer-shell/build.zig +++ b/pkg/gtk4-layer-shell/build.zig @@ -1,7 +1,6 @@ const std = @import("std"); -// TODO: Import this from build.zig.zon when possible -const version: std.SemanticVersion = .{ .major = 1, .minor = 1, .patch = 0 }; +const version = @import("build.zig.zon").version; const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{ .preferred_link_mode = .dynamic, @@ -32,14 +31,18 @@ pub fn build(b: *std.Build) !void { } fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Build.Step.Compile { + const lib_version = try std.SemanticVersion.parse(version); const target = options.target; const optimize = options.optimize; // Shared library - const lib = b.addSharedLibrary(.{ + const lib = b.addLibrary(.{ .name = "gtk4-layer-shell", - .target = target, - .optimize = optimize, + .linkage = .dynamic, + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + }), }); b.installArtifact(lib); @@ -114,19 +117,9 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu .root = upstream.path("src"), .files = srcs, .flags = &.{ - b.fmt("-DGTK_LAYER_SHELL_MAJOR={}", .{version.major}), - b.fmt("-DGTK_LAYER_SHELL_MINOR={}", .{version.minor}), - b.fmt("-DGTK_LAYER_SHELL_MICRO={}", .{version.patch}), - - // Zig 0.14 regression: this is required because building with - // ubsan results in unknown symbols. Bundling the ubsan/compiler - // RT doesn't help. I'm not sure what the root cause is but I - // suspect its related to this: - // https://github.com/ziglang/zig/issues/23052 - // - // We can remove this in the future for Zig updates and see - // if our binaries run in debug on NixOS. - "-fno-sanitize=undefined", + b.fmt("-DGTK_LAYER_SHELL_MAJOR={}", .{lib_version.major}), + b.fmt("-DGTK_LAYER_SHELL_MINOR={}", .{lib_version.minor}), + b.fmt("-DGTK_LAYER_SHELL_MICRO={}", .{lib_version.patch}), }, }); diff --git a/pkg/harfbuzz/build.zig b/pkg/harfbuzz/build.zig index bf247461a..8696c0203 100644 --- a/pkg/harfbuzz/build.zig +++ b/pkg/harfbuzz/build.zig @@ -111,13 +111,13 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu const dynamic_link_opts = options.dynamic_link_opts; - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ "-DHAVE_STDBOOL_H", }); if (target.result.os.tag != .windows) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DHAVE_UNISTD_H", "-DHAVE_SYS_MMAN_H", "-DHAVE_PTHREAD=1", @@ -127,7 +127,7 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu // Freetype _ = b.systemIntegrationOption("freetype", .{}); // So it shows up in help if (freetype_enabled) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DHAVE_FREETYPE=1", // Let's just assume a new freetype @@ -153,7 +153,7 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu } if (coretext_enabled) { - try flags.appendSlice(&.{"-DHAVE_CORETEXT=1"}); + try flags.appendSlice(b.allocator, &.{"-DHAVE_CORETEXT=1"}); lib.linkFramework("CoreText"); module.linkFramework("CoreText", .{}); } diff --git a/pkg/highway/build.zig b/pkg/highway/build.zig index 1013f1643..4c75de49e 100644 --- a/pkg/highway/build.zig +++ b/pkg/highway/build.zig @@ -31,9 +31,9 @@ pub fn build(b: *std.Build) !void { try apple_sdk.addPaths(b, lib); } - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ // Avoid changing binaries based on the current time and date. "-Wno-builtin-macro-redefined", "-D__DATE__=\"redacted\"", @@ -69,7 +69,7 @@ pub fn build(b: *std.Build) !void { "-fno-vectorize", }); if (target.result.os.tag != .windows) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-fmath-errno", "-fno-exceptions", }); diff --git a/pkg/libintl/build.zig b/pkg/libintl/build.zig index 0e32648e7..32221e5ad 100644 --- a/pkg/libintl/build.zig +++ b/pkg/libintl/build.zig @@ -22,9 +22,9 @@ pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ "-DHAVE_CONFIG_H", "-DLOCALEDIR=\"\"", }); diff --git a/pkg/libpng/build.zig b/pkg/libpng/build.zig index 11ed29b18..dbedac632 100644 --- a/pkg/libpng/build.zig +++ b/pkg/libpng/build.zig @@ -46,9 +46,9 @@ pub fn build(b: *std.Build) !void { } if (b.lazyDependency("libpng", .{})) |upstream| { - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ "-DPNG_ARM_NEON_OPT=0", "-DPNG_POWERPC_VSX_OPT=0", "-DPNG_INTEL_SSE_OPT=0", diff --git a/pkg/libxml2/build.zig b/pkg/libxml2/build.zig index acebfaf63..a9b3e4b1a 100644 --- a/pkg/libxml2/build.zig +++ b/pkg/libxml2/build.zig @@ -25,9 +25,9 @@ pub fn build(b: *std.Build) !void { lib.addIncludePath(b.path("override/config/posix")); } - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ // Version info, hardcoded comptime "-DLIBXML_VERSION=" ++ Version.number(), comptime "-DLIBXML_VERSION_STRING=" ++ Version.string(), @@ -46,7 +46,7 @@ pub fn build(b: *std.Build) !void { "-DWITHOUT_TRIO=1", }); if (target.result.os.tag != .windows) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DHAVE_ARPA_INET_H=1", "-DHAVE_ARPA_NAMESER_H=1", "-DHAVE_DL_H=1", @@ -74,25 +74,25 @@ pub fn build(b: *std.Build) !void { var nameBuf: [32]u8 = undefined; const name = std.ascii.upperString(&nameBuf, field.name); const define = try std.fmt.allocPrint(b.allocator, "-DLIBXML_{s}_ENABLED=1", .{name}); - try flags.append(define); + try flags.append(b.allocator, define); if (std.mem.eql(u8, field.name, "history")) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DHAVE_LIBHISTORY=1", "-DHAVE_LIBREADLINE=1", }); } if (std.mem.eql(u8, field.name, "mem_debug")) { - try flags.append("-DDEBUG_MEMORY_LOCATION=1"); + try flags.append(b.allocator, "-DDEBUG_MEMORY_LOCATION=1"); } if (std.mem.eql(u8, field.name, "regexp")) { - try flags.append("-DLIBXML_UNICODE_ENABLED=1"); + try flags.append(b.allocator, "-DLIBXML_UNICODE_ENABLED=1"); } if (std.mem.eql(u8, field.name, "run_debug")) { - try flags.append("-DLIBXML_DEBUG_RUNTIME=1"); + try flags.append(b.allocator, "-DLIBXML_DEBUG_RUNTIME=1"); } if (std.mem.eql(u8, field.name, "thread")) { - try flags.append("-DHAVE_LIBPTHREAD=1"); + try flags.append(b.allocator, "-DHAVE_LIBPTHREAD=1"); } } } diff --git a/pkg/macos/foundation/array.zig b/pkg/macos/foundation/array.zig index d3a977539..7b580eb03 100644 --- a/pkg/macos/foundation/array.zig +++ b/pkg/macos/foundation/array.zig @@ -68,7 +68,7 @@ pub const MutableArray = opaque { comptime Elem: type, value: *const Elem, ) void { - CFArrayAppendValue(self, @constCast(@ptrCast(value))); + CFArrayAppendValue(self, @ptrCast(@constCast(value))); } pub fn removeValue(self: *MutableArray, idx: usize) void { diff --git a/pkg/macos/foundation/attributed_string.zig b/pkg/macos/foundation/attributed_string.zig index de509b2c0..c7f27d7d7 100644 --- a/pkg/macos/foundation/attributed_string.zig +++ b/pkg/macos/foundation/attributed_string.zig @@ -10,7 +10,7 @@ pub const AttributedString = opaque { str: *foundation.String, attributes: *foundation.Dictionary, ) Allocator.Error!*AttributedString { - return @constCast(@ptrCast(c.CFAttributedStringCreate( + return @ptrCast(@constCast(c.CFAttributedStringCreate( null, @ptrCast(str), @ptrCast(attributes), diff --git a/pkg/macos/foundation/dictionary.zig b/pkg/macos/foundation/dictionary.zig index 90642e59a..a529442ac 100644 --- a/pkg/macos/foundation/dictionary.zig +++ b/pkg/macos/foundation/dictionary.zig @@ -17,8 +17,8 @@ pub const Dictionary = opaque { return @as(?*Dictionary, @ptrFromInt(@intFromPtr(c.CFDictionaryCreate( null, - @constCast(@ptrCast(if (keys) |slice| slice.ptr else null)), - @constCast(@ptrCast(if (values) |slice| slice.ptr else null)), + @ptrCast(@constCast(if (keys) |slice| slice.ptr else null)), + @ptrCast(@constCast(if (values) |slice| slice.ptr else null)), @intCast(if (keys) |slice| slice.len else 0), &c.kCFTypeDictionaryKeyCallBacks, &c.kCFTypeDictionaryValueCallBacks, diff --git a/pkg/macos/os/log.zig b/pkg/macos/os/log.zig index 32ecb3296..219c914da 100644 --- a/pkg/macos/os/log.zig +++ b/pkg/macos/os/log.zig @@ -32,10 +32,11 @@ pub const Log = opaque { comptime format: []const u8, args: anytype, ) void { - const str = nosuspend std.fmt.allocPrintZ( + const str = nosuspend std.fmt.allocPrintSentinel( alloc, format, args, + 0, ) catch return; defer alloc.free(str); zig_os_log_with_type(self, typ, str.ptr); diff --git a/pkg/macos/text.zig b/pkg/macos/text.zig index 0589f8692..bfaa388b3 100644 --- a/pkg/macos/text.zig +++ b/pkg/macos/text.zig @@ -4,6 +4,7 @@ const font_descriptor = @import("text/font_descriptor.zig"); const font_manager = @import("text/font_manager.zig"); const frame = @import("text/frame.zig"); const framesetter = @import("text/framesetter.zig"); +const typesetter = @import("text/typesetter.zig"); const line = @import("text/line.zig"); const paragraph_style = @import("text/paragraph_style.zig"); const run = @import("text/run.zig"); @@ -23,6 +24,7 @@ pub const createFontDescriptorsFromData = font_manager.createFontDescriptorsFrom pub const createFontDescriptorFromData = font_manager.createFontDescriptorFromData; pub const Frame = frame.Frame; pub const Framesetter = framesetter.Framesetter; +pub const Typesetter = typesetter.Typesetter; pub const Line = line.Line; pub const ParagraphStyle = paragraph_style.ParagraphStyle; pub const ParagraphStyleSetting = paragraph_style.ParagraphStyleSetting; diff --git a/pkg/macos/text/font.zig b/pkg/macos/text/font.zig index 383861d62..ea37891f5 100644 --- a/pkg/macos/text/font.zig +++ b/pkg/macos/text/font.zig @@ -68,7 +68,7 @@ pub const Font = opaque { } pub fn copyTable(self: *Font, tag: FontTableTag) ?*foundation.Data { - return @constCast(@ptrCast(c.CTFontCopyTable( + return @ptrCast(@constCast(c.CTFontCopyTable( @ptrCast(self), @intFromEnum(tag), c.kCTFontTableOptionNoOptions, @@ -90,7 +90,7 @@ pub const Font = opaque { } pub fn createPathForGlyph(self: *Font, glyph: graphics.Glyph) ?*graphics.Path { - return @constCast(@ptrCast(c.CTFontCreatePathForGlyph( + return @ptrCast(@constCast(c.CTFontCreatePathForGlyph( @ptrCast(self), glyph, null, diff --git a/pkg/macos/text/line.zig b/pkg/macos/text/line.zig index 135fd8558..248f8e645 100644 --- a/pkg/macos/text/line.zig +++ b/pkg/macos/text/line.zig @@ -51,7 +51,7 @@ pub const Line = opaque { } pub fn getGlyphRuns(self: *Line) *foundation.Array { - return @constCast(@ptrCast(c.CTLineGetGlyphRuns(@ptrCast(self)))); + return @ptrCast(@constCast(c.CTLineGetGlyphRuns(@ptrCast(self)))); } }; diff --git a/pkg/macos/text/run.zig b/pkg/macos/text/run.zig index 9d40de81f..2895bfe34 100644 --- a/pkg/macos/text/run.zig +++ b/pkg/macos/text/run.zig @@ -15,10 +15,13 @@ pub const Run = opaque { return @intCast(c.CTRunGetGlyphCount(@ptrCast(self))); } - pub fn getGlyphsPtr(self: *Run) []const graphics.Glyph { + pub fn getGlyphsPtr(self: *Run) ?[]const graphics.Glyph { const len = self.getGlyphCount(); if (len == 0) return &.{}; - const ptr = c.CTRunGetGlyphsPtr(@ptrCast(self)) orelse &.{}; + const ptr: [*c]const graphics.Glyph = @ptrCast( + c.CTRunGetGlyphsPtr(@ptrCast(self)), + ); + if (ptr == null) return null; return ptr[0..len]; } @@ -34,10 +37,13 @@ pub const Run = opaque { return ptr; } - pub fn getPositionsPtr(self: *Run) []const graphics.Point { + pub fn getPositionsPtr(self: *Run) ?[]const graphics.Point { const len = self.getGlyphCount(); if (len == 0) return &.{}; - const ptr = c.CTRunGetPositionsPtr(@ptrCast(self)) orelse &.{}; + const ptr: [*c]const graphics.Point = @ptrCast( + c.CTRunGetPositionsPtr(@ptrCast(self)), + ); + if (ptr == null) return null; return ptr[0..len]; } @@ -53,10 +59,13 @@ pub const Run = opaque { return ptr; } - pub fn getAdvancesPtr(self: *Run) []const graphics.Size { + pub fn getAdvancesPtr(self: *Run) ?[]const graphics.Size { const len = self.getGlyphCount(); if (len == 0) return &.{}; - const ptr = c.CTRunGetAdvancesPtr(@ptrCast(self)) orelse &.{}; + const ptr: [*c]const graphics.Size = @ptrCast( + c.CTRunGetAdvancesPtr(@ptrCast(self)), + ); + if (ptr == null) return null; return ptr[0..len]; } @@ -72,10 +81,13 @@ pub const Run = opaque { return ptr; } - pub fn getStringIndicesPtr(self: *Run) []const usize { + pub fn getStringIndicesPtr(self: *Run) ?[]const usize { const len = self.getGlyphCount(); if (len == 0) return &.{}; - const ptr = c.CTRunGetStringIndicesPtr(@ptrCast(self)) orelse &.{}; + const ptr: [*c]const usize = @ptrCast( + c.CTRunGetStringIndicesPtr(@ptrCast(self)), + ); + if (ptr == null) return null; return ptr[0..len]; } @@ -90,4 +102,16 @@ pub const Run = opaque { ); return ptr; } + + pub fn getStatus(self: *Run) Status { + return @bitCast(c.CTRunGetStatus(@ptrCast(self))); + } +}; + +/// https://developer.apple.com/documentation/coretext/ctrunstatus?language=objc +pub const Status = packed struct(u32) { + right_to_left: bool, + non_monotonic: bool, + has_non_identity_matrix: bool, + _pad: u29 = 0, }; diff --git a/pkg/macos/text/typesetter.zig b/pkg/macos/text/typesetter.zig new file mode 100644 index 000000000..dc07df980 --- /dev/null +++ b/pkg/macos/text/typesetter.zig @@ -0,0 +1,36 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const foundation = @import("../foundation.zig"); +const graphics = @import("../graphics.zig"); +const text = @import("../text.zig"); +const c = @import("c.zig").c; + +pub const Typesetter = opaque { + pub fn createWithAttributedStringAndOptions( + str: *foundation.AttributedString, + opts: *foundation.Dictionary, + ) Allocator.Error!*Typesetter { + return @as( + ?*Typesetter, + @ptrFromInt(@intFromPtr(c.CTTypesetterCreateWithAttributedStringAndOptions( + @ptrCast(str), + @ptrCast(opts), + ))), + ) orelse Allocator.Error.OutOfMemory; + } + + pub fn release(self: *Typesetter) void { + foundation.CFRelease(self); + } + + pub fn createLine( + self: *Typesetter, + range: foundation.c.CFRange, + ) *text.Line { + return @ptrFromInt(@intFromPtr(c.CTTypesetterCreateLine( + @ptrCast(self), + range, + ))); + } +}; diff --git a/pkg/macos/video/display_link.zig b/pkg/macos/video/display_link.zig index 4bbf58a0c..7d6b437f9 100644 --- a/pkg/macos/video/display_link.zig +++ b/pkg/macos/video/display_link.zig @@ -74,7 +74,7 @@ pub const DisplayLink = opaque { callbackFn( displayLink, - @alignCast(@ptrCast(inner_userinfo)), + @ptrCast(@alignCast(inner_userinfo)), ); return c.kCVReturnSuccess; } diff --git a/pkg/oniguruma/build.zig b/pkg/oniguruma/build.zig index 77e3b6f65..ea39b4814 100644 --- a/pkg/oniguruma/build.zig +++ b/pkg/oniguruma/build.zig @@ -100,9 +100,8 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu .SIZEOF_VOIDP = t.ptrBitWidth() / t.cTypeBitSize(.char), })); - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{}); + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); lib.addCSourceFiles(.{ .root = upstream.path(""), .flags = flags.items, diff --git a/pkg/oniguruma/init.zig b/pkg/oniguruma/init.zig index 933e50b5a..ea64724c2 100644 --- a/pkg/oniguruma/init.zig +++ b/pkg/oniguruma/init.zig @@ -6,7 +6,7 @@ const errors = @import("errors.zig"); /// the encodings that the program will use. pub fn init(encs: []const *Encoding) !void { _ = try errors.convertError(c.onig_initialize( - @constCast(@ptrCast(@alignCast(encs.ptr))), + @ptrCast(@alignCast(@constCast(encs.ptr))), @intCast(encs.len), )); } diff --git a/pkg/sentry/build.zig b/pkg/sentry/build.zig index 95900ae8f..3c88df56d 100644 --- a/pkg/sentry/build.zig +++ b/pkg/sentry/build.zig @@ -26,22 +26,21 @@ pub fn build(b: *std.Build) !void { try apple_sdk.addPaths(b, lib); } - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{}); + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); if (target.result.os.tag == .windows) { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DSENTRY_WITH_UNWINDER_DBGHELP", }); } else { - try flags.appendSlice(&.{ + try flags.appendSlice(b.allocator, &.{ "-DSENTRY_WITH_UNWINDER_LIBBACKTRACE", }); } switch (backend) { - .crashpad => try flags.append("-DSENTRY_BACKEND_CRASHPAD"), - .breakpad => try flags.append("-DSENTRY_BACKEND_BREAKPAD"), - .inproc => try flags.append("-DSENTRY_BACKEND_INPROC"), + .crashpad => try flags.append(b.allocator, "-DSENTRY_BACKEND_CRASHPAD"), + .breakpad => try flags.append(b.allocator, "-DSENTRY_BACKEND_BREAKPAD"), + .inproc => try flags.append(b.allocator, "-DSENTRY_BACKEND_INPROC"), .none => {}, } diff --git a/pkg/simdutf/build.zig b/pkg/simdutf/build.zig index f96eeae45..f2ddfeba4 100644 --- a/pkg/simdutf/build.zig +++ b/pkg/simdutf/build.zig @@ -20,11 +20,11 @@ pub fn build(b: *std.Build) !void { try apple_sdk.addPaths(b, lib); } - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); // Zig 0.13 bug: https://github.com/ziglang/zig/issues/20414 // (See root Ghostty build.zig on why we do this) - try flags.appendSlice(&.{"-DSIMDUTF_IMPLEMENTATION_ICELAKE=0"}); + try flags.appendSlice(b.allocator, &.{"-DSIMDUTF_IMPLEMENTATION_ICELAKE=0"}); lib.addCSourceFiles(.{ .flags = flags.items, diff --git a/pkg/spirv-cross/build.zig b/pkg/spirv-cross/build.zig index ff7f15c94..003ec43cf 100644 --- a/pkg/spirv-cross/build.zig +++ b/pkg/spirv-cross/build.zig @@ -64,9 +64,9 @@ fn buildSpirvCross( try apple_sdk.addPaths(b, lib); } - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ "-DSPIRV_CROSS_C_API_GLSL=1", "-DSPIRV_CROSS_C_API_MSL=1", diff --git a/pkg/utfcpp/build.zig b/pkg/utfcpp/build.zig index 341b35578..e06813b83 100644 --- a/pkg/utfcpp/build.zig +++ b/pkg/utfcpp/build.zig @@ -19,9 +19,8 @@ pub fn build(b: *std.Build) !void { try apple_sdk.addPaths(b, lib); } - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{}); + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); lib.addCSourceFiles(.{ .flags = flags.items, diff --git a/pkg/wuffs/build.zig b/pkg/wuffs/build.zig index 57d89e6b6..3d9f83daa 100644 --- a/pkg/wuffs/build.zig +++ b/pkg/wuffs/build.zig @@ -17,11 +17,11 @@ pub fn build(b: *std.Build) !void { }); unit_tests.linkLibC(); - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.append("-DWUFFS_IMPLEMENTATION"); + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.append(b.allocator, "-DWUFFS_IMPLEMENTATION"); inline for (@import("src/c.zig").defines) |key| { - try flags.append("-D" ++ key); + try flags.append(b.allocator, "-D" ++ key); } if (b.lazyDependency("wuffs", .{})) |wuffs_dep| { diff --git a/pkg/wuffs/src/jpeg.zig b/pkg/wuffs/src/jpeg.zig index c07278eed..700ba01b9 100644 --- a/pkg/wuffs/src/jpeg.zig +++ b/pkg/wuffs/src/jpeg.zig @@ -31,7 +31,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData { } var source_buffer: c.wuffs_base__io_buffer = .{ - .data = .{ .ptr = @constCast(@ptrCast(data.ptr)), .len = data.len }, + .data = .{ .ptr = @ptrCast(@constCast(data.ptr)), .len = data.len }, .meta = .{ .wi = data.len, .ri = 0, diff --git a/pkg/wuffs/src/png.zig b/pkg/wuffs/src/png.zig index 1f37bb375..d79ae5b56 100644 --- a/pkg/wuffs/src/png.zig +++ b/pkg/wuffs/src/png.zig @@ -31,7 +31,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData { } var source_buffer: c.wuffs_base__io_buffer = .{ - .data = .{ .ptr = @constCast(@ptrCast(data.ptr)), .len = data.len }, + .data = .{ .ptr = @ptrCast(@constCast(data.ptr)), .len = data.len }, .meta = .{ .wi = data.len, .ri = 0, diff --git a/pkg/zlib/build.zig b/pkg/zlib/build.zig index caa557454..246ab1bcb 100644 --- a/pkg/zlib/build.zig +++ b/pkg/zlib/build.zig @@ -26,9 +26,9 @@ pub fn build(b: *std.Build) !void { .{ .include_extensions = &.{".h"} }, ); - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); - try flags.appendSlice(&.{ + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ "-DHAVE_SYS_TYPES_H", "-DHAVE_STDINT_H", "-DHAVE_STDDEF_H", diff --git a/po/lt_LT.UTF-8.po b/po/lt_LT.UTF-8.po new file mode 100644 index 000000000..0c466d3a4 --- /dev/null +++ b/po/lt_LT.UTF-8.po @@ -0,0 +1,318 @@ +# Language LT translations for com.mitchellh.ghostty package. +# Copyright (C) 2025 "Mitchell Hashimoto, Ghostty contributors" +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Tadas Lotuzas , 2025. +# +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-09-17 13:27+0200\n" +"Last-Translator: Tadas Lotuzas \n" +"Language-Team: Language LT\n" +"Language: LT\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "Keisti terminalo pavadinimą" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "Palikite tuščią, kad atkurtumėte numatytąjį pavadinimą." + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "Atšaukti" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "Gerai" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Konfigūracijos klaidos" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Rasta viena ar daugiau konfigūracijos klaidų. Peržiūrėkite žemiau esančias klaidas " +"ir arba iš naujo įkelkite konfigūraciją, arba ignoruokite šias klaidas." + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "Ignoruoti" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Reload Configuration" +msgstr "Iš naujo įkelti konfigūraciją" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "Padalinti aukštyn" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "Padalinti žemyn" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "Padalinti kairėn" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "Padalinti dešinėn" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "Vykdyti komandą…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "Kopijuoti" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 +msgid "Paste" +msgstr "Įklijuoti" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "Išvalyti" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "Atstatyti" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "Padalinti" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "Keisti pavadinimą…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "Kortelė" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:265 +msgid "New Tab" +msgstr "Nauja kortelė" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "Uždaryti kortelę" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Langas" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "Naujas langas" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "Uždaryti langą" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "Konfigūracija" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Open Configuration" +msgstr "Atidaryti konfigūraciją" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "Komandų paletė" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Terminal Inspector" +msgstr "Terminalo inspektorius" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 +msgid "About Ghostty" +msgstr "Apie Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 +msgid "Quit" +msgstr "Išeiti" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "Leisti prieigą prie iškarpinės" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Programa bando skaityti iš iškarpinės. Žemiau rodomas dabartinis " +"iškarpinės turinys." + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "Drausti" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "Leisti" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "Prisiminti pasirinkimą šiam padalijimui" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "Iš naujo įkelkite konfigūraciją, kad vėl būtų rodoma ši užuomina" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Programa bando rašyti į iškarpinę. Žemiau rodomas dabartinis " +"iškarpinės turinys." + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Įspėjimas: galimai nesaugus įklijavimas" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Šio teksto įklijavimas į terminalą gali būti pavojingas, nes panašu, kad " +"gali būti vykdomos tam tikros komandos." + +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2531 +msgid "Close" +msgstr "Uždaryti" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Išeiti iš Ghostty?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Uždaryti langą?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Uždaryti kortelę?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Uždaryti padalijimą?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Visos terminalo sesijos bus nutrauktos." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Visos terminalo sesijos šiame lange bus nutrauktos." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Visos terminalo sesijos šioje kortelėje bus nutrauktos." + +#: src/apprt/gtk/CloseDialog.zig:99 +msgid "The currently running process in this split will be terminated." +msgstr "Šiuo metu vykdomas procesas šiame padalijime bus nutrauktas." + +#: src/apprt/gtk/Surface.zig:1266 +msgid "Copied to clipboard" +msgstr "Nukopijuota į iškarpinę" + +#: src/apprt/gtk/Surface.zig:1268 +msgid "Cleared clipboard" +msgstr "Iškarpinė išvalyta" + +#: src/apprt/gtk/Surface.zig:2525 +msgid "Command succeeded" +msgstr "Komanda sėkminga" + +#: src/apprt/gtk/Surface.zig:2527 +msgid "Command failed" +msgstr "Komanda nepavyko" + +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Pagrindinis meniu" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Peržiūrėti atidarytas korteles" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "Naujas padalijimas" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Naudojate Ghostty derinimo versiją! Našumas bus sumažintas." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "Konfigūracija įkelta iš naujo" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Ghostty kūrėjai" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: terminalo inspektorius" diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index e48fa93c8..7bdbc9b48 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -52,15 +52,15 @@ parts: rm -rf $CRAFT_PART_SRC/* if [[ -n $arch ]]; then - curl -LO --retry-connrefused --retry 10 https://ziglang.org/download/0.14.0/zig-linux-$arch-0.14.0.tar.xz + curl -LO --retry-connrefused --retry 10 https://ziglang.org/download/0.15.2/zig-$arch-linux-0.15.2.tar.xz else echo "Unsupported arch" exit 1 fi - tar xf zig-lin*xz + tar xf zig-$arch-lin*xz rm -f *xz - mv zig-linux*/* . + mv zig-$arch-linux*/* . prime: - -* diff --git a/src/Command.zig b/src/Command.zig index b0d804327..f28d8bb9d 100644 --- a/src/Command.zig +++ b/src/Command.zig @@ -194,7 +194,9 @@ fn startPosix(self: *Command, arena: Allocator) !void { // child process so there isn't much we can do. We try to output // something reasonable. Its important to note we MUST NOT return // any other error condition from here on out. - const stderr = std.io.getStdErr().writer(); + var stderr_buf: [1024]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&stderr_buf); + const stderr = &stderr_writer.interface; switch (err) { error.FileNotFound => stderr.print( \\Requested executable not found. Please verify the command is on @@ -211,6 +213,7 @@ fn startPosix(self: *Command, arena: Allocator) !void { .{err}, ) catch {}, } + stderr.flush() catch {}; // We return a very specific error that can be detected to determine // we're in the child. @@ -464,34 +467,35 @@ fn createWindowsEnvBlock(allocator: mem.Allocator, env_map: *const EnvMap) ![]u1 /// Copied from Zig. This function could be made public in child_process.zig instead. fn windowsCreateCommandLine(allocator: mem.Allocator, argv: []const []const u8) ![:0]u8 { - var buf = std.ArrayList(u8).init(allocator); + var buf: std.Io.Writer.Allocating = .init(allocator); defer buf.deinit(); + const writer = &buf.writer; for (argv, 0..) |arg, arg_i| { - if (arg_i != 0) try buf.append(' '); + if (arg_i != 0) try writer.writeByte(' '); if (mem.indexOfAny(u8, arg, " \t\n\"") == null) { - try buf.appendSlice(arg); + try writer.writeAll(arg); continue; } - try buf.append('"'); + try writer.writeByte('"'); var backslash_count: usize = 0; for (arg) |byte| { switch (byte) { '\\' => backslash_count += 1, '"' => { - try buf.appendNTimes('\\', backslash_count * 2 + 1); - try buf.append('"'); + try writer.splatByteAll('\\', backslash_count * 2 + 1); + try writer.writeByte('"'); backslash_count = 0; }, else => { - try buf.appendNTimes('\\', backslash_count); - try buf.append(byte); + try writer.splatByteAll('\\', backslash_count); + try writer.writeByte(byte); backslash_count = 0; }, } } - try buf.appendNTimes('\\', backslash_count * 2); - try buf.append('"'); + try writer.splatByteAll('\\', backslash_count * 2); + try writer.writeByte('"'); } return buf.toOwnedSliceSentinel(0); diff --git a/src/Surface.zig b/src/Surface.zig index 8edeadf83..c9c40f466 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -33,6 +33,7 @@ const font = @import("font/main.zig"); const Command = @import("Command.zig"); const terminal = @import("terminal/main.zig"); const configpkg = @import("config.zig"); +const Duration = configpkg.Config.Duration; const input = @import("input.zig"); const App = @import("App.zig"); const internal_os = @import("os/main.zig"); @@ -147,6 +148,13 @@ focused: bool = true, /// Used to determine whether to continuously scroll. selection_scroll_active: 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 +/// actual time, but we must be able to compare two subsequent timestamps to get +/// the wall clock time that has elapsed between timestamps. +command_timer: ?std.time.Instant = null, + /// The effect of an input event. This can be used by callers to take /// the appropriate action after an input event. For example, key /// input can be forwarded to the OS for further processing if it @@ -260,10 +268,11 @@ const DerivedConfig = struct { font: font.SharedGridSet.DerivedConfig, mouse_interval: u64, mouse_hide_while_typing: bool, - mouse_scroll_multiplier: f64, + mouse_reporting: bool, + mouse_scroll_multiplier: configpkg.MouseScrollMultiplier, mouse_shift_capture: configpkg.MouseShiftCapture, macos_non_native_fullscreen: configpkg.NonNativeFullscreen, - macos_option_as_alt: ?configpkg.OptionAsAlt, + macos_option_as_alt: ?input.OptionAsAlt, selection_clear_on_copy: bool, selection_clear_on_typing: bool, vt_kam_allowed: bool, @@ -280,6 +289,9 @@ const DerivedConfig = struct { links: []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, const Link = struct { regex: oni.Regex, @@ -294,19 +306,19 @@ const DerivedConfig = struct { // Build all of our links const links = links: { - var links = std.ArrayList(Link).init(alloc); - defer links.deinit(); + var links: std.ArrayList(Link) = .empty; + defer links.deinit(alloc); for (config.link.links.items) |link| { var regex = try link.oniRegex(); errdefer regex.deinit(); - try links.append(.{ + try links.append(alloc, .{ .regex = regex, .action = link.action, .highlight = link.highlight, }); } - break :links try links.toOwnedSlice(); + break :links try links.toOwnedSlice(alloc); }; errdefer { for (links) |*link| link.regex.deinit(); @@ -330,6 +342,7 @@ const DerivedConfig = struct { .font = try font.SharedGridSet.DerivedConfig.init(alloc, config), .mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms .mouse_hide_while_typing = config.@"mouse-hide-while-typing", + .mouse_reporting = config.@"mouse-reporting", .mouse_scroll_multiplier = config.@"mouse-scroll-multiplier", .mouse_shift_capture = config.@"mouse-shift-capture", .macos_non_native_fullscreen = config.@"macos-non-native-fullscreen", @@ -350,6 +363,9 @@ const DerivedConfig = struct { .links = links, .link_previews = config.@"link-previews", .scroll_to_bottom = config.@"scroll-to-bottom", + .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", // Assignments happen sequentially so we have to do this last // so that the memory is captured from allocs above. @@ -688,7 +704,22 @@ pub fn init( .set_title, .{ .title = title }, ); - } + } else if (command) |cmd| switch (cmd) { + // If a user specifies a command it is appropriate to set the title as argv[0] + // we know in the case of a direct command it has been supplied by the user + .direct => |cmd_str| if (cmd_str.len != 0) { + _ = try rt_app.performAction( + .{ .surface = self }, + .set_title, + .{ .title = cmd_str[0] }, + ); + }, + + // We won't set the title in the case the shell expands the command + // as that should typically be used to launch a shell which should + // set its own titles + .shell => {}, + }; // We are no longer the first surface app.first = false; @@ -954,6 +985,8 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { .renderer_health => |health| self.updateRendererHealth(health), + .scrollbar => |scrollbar| self.updateScrollbar(scrollbar), + .report_color_scheme => |force| self.reportColorScheme(force), .present_surface => try self.presentSurface(), @@ -984,6 +1017,30 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { self.selection_scroll_active = active; try self.selectionScrollTick(); }, + + .start_command => { + self.command_timer = try .now(); + }, + + .stop_command => |v| timer: { + const end: std.time.Instant = try .now(); + const start = self.command_timer orelse break :timer; + self.command_timer = null; + + const duration: Duration = .{ .duration = end.since(start) }; + log.debug("command took {f}", .{duration}); + + _ = self.rt_app.performAction( + .{ .surface = self }, + .command_finished, + .{ + .exit_code = v, + .duration = duration, + }, + ) catch |err| { + log.warn("apprt failed to notify command finish={}", .{err}); + }; + }, } } @@ -1004,6 +1061,16 @@ fn selectionScrollTick(self: *Surface) !void { defer self.renderer_state.mutex.unlock(); const t: *terminal.Terminal = self.renderer_state.terminal; + // If our screen changed while this is happening, we stop our + // selection scroll. + if (self.mouse.left_click_screen != t.active_screen) { + self.io.queueMessage( + .{ .selection_scroll = false }, + .locked, + ); + return; + } + // Scroll the viewport as required try t.scrollViewport(.{ .delta = delta }); @@ -1092,7 +1159,7 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void { // so that we can close the terminal. We close the terminal on // any key press that encodes a character. t.modes.set(.disable_keyboard, false); - t.screen.kitty_keyboard.set(.set, .{}); + t.screen.kitty_keyboard.set(.set, .disabled); } // Waiting after command we stop here. The terminal is updated, our @@ -1396,6 +1463,17 @@ fn updateRendererHealth(self: *Surface, health: rendererpkg.Health) void { }; } +/// Called when the scrollbar state changes. +fn updateScrollbar(self: *Surface, scrollbar: terminal.Scrollbar) void { + _ = self.rt_app.performAction( + .{ .surface = self }, + .scrollbar, + scrollbar, + ) catch |err| { + log.warn("failed to notify app of scrollbar change err={}", .{err}); + }; +} + /// This should be called anytime `config_conditional_state` changes /// so that the apprt can reload the configuration. fn notifyConfigConditionalState(self: *Surface) void { @@ -2455,7 +2533,7 @@ fn maybeHandleBinding( self.keyboard.bindings = null; // Attempt to perform the action - log.debug("key event binding flags={} action={}", .{ + log.debug("key event binding flags={} action={f}", .{ leaf.flags, action, }); @@ -2573,56 +2651,32 @@ fn encodeKey( event: input.KeyEvent, insp_ev: ?*inspectorpkg.key.Event, ) !?termio.Message.WriteReq { - // Build up our encoder. Under different modes and - // inputs there are many keybindings that result in no encoding - // whatsoever. - const enc: input.KeyEncoder = enc: { - const option_as_alt: configpkg.OptionAsAlt = self.config.macos_option_as_alt orelse detect: { - // Non-macOS doesn't use this value so ignore. - if (comptime builtin.os.tag != .macos) break :detect .false; - - // If we don't have alt pressed, it doesn't matter what this - // config is so we can just say "false" and break out and avoid - // more expensive checks below. - if (!event.mods.alt) break :detect .false; - - // Alt is pressed, we're on macOS. We break some encapsulation - // here and assume libghostty for ease... - break :detect self.rt_app.keyboardLayout().detectOptionAsAlt(); - }; - - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); - const t = &self.io.terminal; - break :enc .{ - .event = event, - .macos_option_as_alt = option_as_alt, - .alt_esc_prefix = t.modes.get(.alt_esc_prefix), - .cursor_key_application = t.modes.get(.cursor_keys), - .keypad_key_application = t.modes.get(.keypad_keys), - .ignore_keypad_with_numlock = t.modes.get(.ignore_keypad_with_numlock), - .modify_other_keys_state_2 = t.flags.modify_other_keys_2, - .kitty_flags = t.screen.kitty_keyboard.current(), - }; - }; - const write_req: termio.Message.WriteReq = req: { + // Build our encoding options, which requires the lock. + const encoding_opts = self.encodeKeyOpts(); + // Try to write the input into a small array. This fits almost // every scenario. Larger situations can happen due to long // pre-edits. var data: termio.Message.WriteReq.Small.Array = undefined; - if (enc.encode(&data)) |seq| { + var writer: std.Io.Writer = .fixed(&data); + if (input.key_encode.encode( + &writer, + event, + encoding_opts, + )) { + const written = writer.buffered(); + // Special-case: we did nothing. - if (seq.len == 0) return null; + if (written.len == 0) return null; break :req .{ .small = .{ .data = data, - .len = @intCast(seq.len), + .len = @intCast(written.len), } }; } else |err| switch (err) { // Means we need to allocate - error.OutOfMemory => {}, - else => return err, + error.WriteFailed => {}, } // We need to allocate. We allocate double the UTF-8 length @@ -2631,16 +2685,23 @@ fn encodeKey( // typing this where we don't have enough space is a long preedit, // and in that case the size we need is exactly the UTF-8 length, // so the double is being safe. - const buf = try self.alloc.alloc(u8, @max( - event.utf8.len * 2, - data.len * 2, - )); - defer self.alloc.free(buf); + var alloc_writer: std.Io.Writer.Allocating = try .initCapacity( + self.alloc, + @max(event.utf8.len * 2, data.len * 2), + ); + defer alloc_writer.deinit(); // This results in a double allocation but this is such an unlikely // path the performance impact is unimportant. - const seq = try enc.encode(buf); - break :req try termio.Message.WriteReq.init(self.alloc, seq); + try input.key_encode.encode( + &alloc_writer.writer, + event, + encoding_opts, + ); + break :req try termio.Message.WriteReq.init( + self.alloc, + alloc_writer.writer.buffered(), + ); }; // Copy the encoded data into the inspector event if we have one. @@ -2660,6 +2721,28 @@ fn encodeKey( return write_req; } +fn encodeKeyOpts(self: *const Surface) input.key_encode.Options { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + const t = &self.io.terminal; + + var opts: input.key_encode.Options = .fromTerminal(t); + if (comptime builtin.os.tag != .macos) return opts; + + opts.macos_option_as_alt = self.config.macos_option_as_alt orelse detect: { + // If we don't have alt pressed, it doesn't matter what this + // config is so we can just say "false" and break out and avoid + // more expensive checks below. + if (!self.mouse.mods.alt) break :detect .false; + + // Alt is pressed, we're on macOS. We break some encapsulation + // here and assume libghostty for ease... + break :detect self.rt_app.keyboardLayout().detectOptionAsAlt(); + }; + + return opts; +} + /// Sends text as-is to the terminal without triggering any keyboard /// protocol. This will treat the input text as if it was pasted /// from the clipboard so the same logic will be applied. Namely, @@ -2829,7 +2912,7 @@ pub fn scrollCallback( // scroll events to pixels by multiplying the wheel tick value and the cell size. This means // that a wheel tick of 1 results in single scroll event. const yoff_adjusted: f64 = if (scroll_mods.precision) - yoff + yoff * self.config.mouse_scroll_multiplier.precision else yoff_adjusted: { // Round out the yoff to an absolute minimum of 1. macos tries to // simulate precision scrolling with non precision events by @@ -2843,7 +2926,7 @@ pub fn scrollCallback( else @min(yoff, -1); - break :yoff_adjusted yoff_max * cell_size * self.config.mouse_scroll_multiplier; + break :yoff_adjusted yoff_max * cell_size * self.config.mouse_scroll_multiplier.discrete; }; // Add our previously saved pending amount to the offset to get the @@ -2903,7 +2986,7 @@ pub fn scrollCallback( // If we have an active mouse reporting mode, clear the selection. // The selection can occur if the user uses the shift mod key to // override mouse grabbing from the window. - if (self.io.terminal.flags.mouse_event != .none) { + if (self.isMouseReporting()) { try self.setSelection(null); } @@ -2946,7 +3029,7 @@ pub fn scrollCallback( // the normal logic. // If we're scrolling up or down, then send a mouse event. - if (self.io.terminal.flags.mouse_event != .none) { + if (self.isMouseReporting()) { for (0..@abs(y.delta)) |_| { const pos = try self.rt_surface.getCursorPos(); try self.mouseReport(switch (y.direction()) { @@ -3019,6 +3102,13 @@ pub fn contentScaleCallback(self: *Surface, content_scale: apprt.ContentScale) ! /// The type of action to report for a mouse event. const MouseReportAction = enum { press, release, motion }; +/// Returns true if mouse reporting is enabled both in the config and +/// the terminal state. +fn isMouseReporting(self: *const Surface) bool { + return self.config.mouse_reporting and + self.io.terminal.flags.mouse_event != .none; +} + fn mouseReport( self: *Surface, button: ?input.MouseButton, @@ -3026,9 +3116,13 @@ fn mouseReport( mods: input.Mods, pos: apprt.CursorPos, ) !void { + // Mouse reporting must be enabled by both config and terminal state + assert(self.config.mouse_reporting); + assert(self.io.terminal.flags.mouse_event != .none); + // Depending on the event, we may do nothing at all. switch (self.io.terminal.flags.mouse_event) { - .none => return, + .none => unreachable, // checked by assert above // X10 only reports clicks with mouse button 1, 2, 3. We verify // the button later. @@ -3423,7 +3517,7 @@ pub fn mouseButtonCallback( { self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); - if (self.io.terminal.flags.mouse_event != .none) report: { + if (self.isMouseReporting()) report: { // If we have shift-pressed and we aren't allowed to capture it, // then we do not do a mouse report. if (mods.shift and !shift_capture) break :report; @@ -3960,6 +4054,8 @@ pub fn cursorPosCallback( crash.sentry.thread_state = self.crashThreadState(); defer crash.sentry.thread_state = null; + // log.debug("cursor pos x={} y={} mods={?}", .{ pos.x, pos.y, mods }); + // If the position is negative, it is outside our viewport and // we need to clear any hover states. if (pos.x < 0 or pos.y < 0) { @@ -4059,7 +4155,7 @@ pub fn cursorPosCallback( } // Do a mouse report - if (self.io.terminal.flags.mouse_event != .none) report: { + if (self.isMouseReporting()) report: { // Shift overrides mouse "grabbing" in the window, taken from Kitty. // This only applies if there is a mouse button pressed so that // movement reports are not affected. @@ -4091,6 +4187,12 @@ pub fn cursorPosCallback( // count because we don't want to handle selection. if (self.mouse.left_click_count == 0) break :select; + // If our terminal screen changed then we don't process this. We don't + // invalidate our pin or mouse state because if the screen switches + // back then we can continue our selection. + const t: *terminal.Terminal = self.renderer_state.terminal; + if (self.mouse.left_click_screen != t.active_screen) break :select; + // All roads lead to requiring a re-render at this point. try self.queueRender(); @@ -4114,7 +4216,7 @@ pub fn cursorPosCallback( } // Convert to points - const screen = &self.renderer_state.terminal.screen; + const screen = &t.screen; const pin = screen.pages.pin(.{ .viewport = .{ .x = pos_vp.x, @@ -4738,12 +4840,27 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool }, .unlocked); }, + .scroll_to_row => |n| { + { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + const t: *terminal.Terminal = self.renderer_state.terminal; + t.screen.scroll(.{ .row = n }); + } + + try self.queueRender(); + }, + .scroll_to_selection => { - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); - const sel = self.io.terminal.screen.selection orelse return false; - const tl = sel.topLeft(&self.io.terminal.screen); - self.io.terminal.screen.scroll(.{ .pin = tl }); + { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + const sel = self.io.terminal.screen.selection orelse return false; + const tl = sel.topLeft(&self.io.terminal.screen); + self.io.terminal.screen.scroll(.{ .pin = tl }); + } + + try self.queueRender(); }, .scroll_page_up => { @@ -4931,6 +5048,11 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .toggle, ), + .toggle_mouse_reporting => { + self.config.mouse_reporting = !self.config.mouse_reporting; + log.debug("mouse reporting toggled: {}", .{self.config.mouse_reporting}); + }, + .toggle_command_palette => return try self.rt_app.performAction( .{ .surface = self }, .toggle_command_palette, @@ -5081,7 +5203,9 @@ fn writeScreenFile( defer file.close(); // Screen.dumpString writes byte-by-byte, so buffer it - var buf_writer = std.io.bufferedWriter(file.writer()); + var buf: [4096]u8 = undefined; + var file_writer = file.writer(&buf); + var buf_writer = &file_writer.interface; // Write the scrollback contents. This requires a lock. { @@ -5131,7 +5255,7 @@ fn writeScreenFile( const br = sel.bottomRight(&self.io.terminal.screen); try self.io.terminal.screen.dumpString( - buf_writer.writer(), + buf_writer, .{ .tl = tl, .br = br, @@ -5228,13 +5352,10 @@ fn completeClipboardPaste( ) !void { if (data.len == 0) return; - const critical: struct { - bracketed: bool, - } = critical: { + const encode_opts: input.paste.Options = encode_opts: { self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); - - const bracketed = self.io.terminal.modes.get(.bracketed_paste); + const opts: input.paste.Options = .fromTerminal(&self.io.terminal); // If we have paste protection enabled, we detect unsafe pastes and return // an error. The error approach allows apprt to attempt to complete the paste @@ -5250,7 +5371,7 @@ fn completeClipboardPaste( // This is set during confirmation usually. if (allow_unsafe) break :unsafe false; - if (bracketed) { + if (opts.bracketed) { // If we're bracketed and the paste contains and ending // bracket then something naughty might be going on and we // never trust it. @@ -5261,7 +5382,7 @@ fn completeClipboardPaste( if (self.config.clipboard_paste_bracketed_safe) break :unsafe false; } - break :unsafe !terminal.isSafePaste(data); + break :unsafe !input.paste.isSafe(data); }; if (unsafe) { @@ -5275,55 +5396,32 @@ fn completeClipboardPaste( log.warn("error scrolling to bottom err={}", .{err}); }; - break :critical .{ - .bracketed = bracketed, - }; + break :encode_opts opts; }; - if (critical.bracketed) { - // If we're bracketd we write the data as-is to the terminal with - // the bracketed paste escape codes around it. - self.io.queueMessage(.{ - .write_stable = "\x1B[200~", - }, .unlocked); + // Encode the data. In most cases this doesn't require any + // copies, so we optimize for that case. + var data_duped: ?[]u8 = null; + const vecs = input.paste.encode(data, encode_opts) catch |err| switch (err) { + error.MutableRequired => vecs: { + const buf: []u8 = try self.alloc.dupe(u8, data); + errdefer self.alloc.free(buf); + data_duped = buf; + break :vecs input.paste.encode(buf, encode_opts); + }, + }; + defer if (data_duped) |v| { + // This code path means the data did require a copy and mutation. + // We must free it. + self.alloc.free(v); + }; + + for (vecs) |vec| if (vec.len > 0) { self.io.queueMessage(try termio.Message.writeReq( self.alloc, - data, + vec, ), .unlocked); - self.io.queueMessage(.{ - .write_stable = "\x1B[201~", - }, .unlocked); - } else { - // If its not bracketed the input bytes are indistinguishable from - // keystrokes, so we must be careful. For example, we must replace - // any newlines with '\r'. - - // We just do a heap allocation here because its easy and I don't think - // worth the optimization of using small messages. - var buf = try self.alloc.alloc(u8, data.len); - defer self.alloc.free(buf); - - // This is super, super suboptimal. We can easily make use of SIMD - // here, but maybe LLVM in release mode is smart enough to figure - // out something clever. Either way, large non-bracketed pastes are - // increasingly rare for modern applications. - var len: usize = 0; - for (data, 0..) |ch, i| { - const dch = switch (ch) { - '\n' => '\r', - '\r' => if (i + 1 < data.len and data[i + 1] == '\n') continue else ch, - else => ch, - }; - - buf[len] = dch; - len += 1; - } - - self.io.queueMessage(try termio.Message.writeReq( - self.alloc, - buf[0..len], - ), .unlocked); - } + }; } fn completeClipboardReadOSC52( diff --git a/src/apprt/action.zig b/src/apprt/action.zig index b7dc80e03..e593d4bce 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -164,6 +164,9 @@ pub const Action = union(Key) { /// The cell size has changed to the given dimensions in pixels. cell_size: CellSize, + /// The scrollbar is updating. + scrollbar: terminal.Scrollbar, + /// The target should be re-rendered. This usually has a specific /// surface target but if the app is targeted then all active /// surfaces should be redrawn. @@ -295,6 +298,9 @@ pub const Action = union(Key) { /// Show the on-screen keyboard. show_on_screen_keyboard, + /// A command has finished, + command_finished: CommandFinished, + /// Sync with: ghostty_action_tag_e pub const Key = enum(c_int) { quit, @@ -321,6 +327,7 @@ pub const Action = union(Key) { reset_window_size, initial_size, cell_size, + scrollbar, render, inspector, show_gtk_inspector, @@ -350,6 +357,7 @@ pub const Action = union(Key) { show_child_exited, progress_report, show_on_screen_keyboard, + command_finished, }; /// Sync with: ghostty_action_u @@ -574,7 +582,7 @@ pub const SetTitle = struct { value: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { try writer.print("{s}{{ {s} }}", .{ @typeName(@This()), value.title }); } @@ -598,7 +606,7 @@ pub const Pwd = struct { value: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { try writer.print("{s}{{ {s} }}", .{ @typeName(@This()), value.pwd }); } @@ -626,7 +634,7 @@ pub const DesktopNotification = struct { value: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { try writer.print("{s}{{ title: {s}, body: {s} }}", .{ @typeName(@This()), @@ -741,3 +749,21 @@ pub const CloseTabMode = enum(c_int) { /// Close all other tabs. other, }; + +pub const CommandFinished = struct { + exit_code: ?u8, + duration: configpkg.Config.Duration, + + /// sync with ghostty_action_command_finished_s in ghostty.h + pub const C = extern struct { + exit_code: i16, + duration: u64, + }; + + pub fn cval(self: CommandFinished) C { + return .{ + .exit_code = self.exit_code orelse -1, + .duration = self.duration.duration, + }; + } +}; diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 08d8291ef..617557995 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -266,8 +266,8 @@ pub const App = struct { // embedded apprt. self.performPreAction(target, action, value); - log.debug("dispatching action target={s} action={} value={}", .{ - @tagName(target), + log.debug("dispatching action target={t} action={} value={any}", .{ + target, action, value, }); @@ -1910,7 +1910,7 @@ pub const CAPI = struct { }; return ptr.core_surface.performBindingAction(action) catch |err| { - log.err("error performing binding action action={} err={}", .{ action, err }); + log.err("error performing binding action action={f} err={}", .{ action, err }); return false; }; } diff --git a/src/apprt/gtk/adw_version.zig b/src/apprt/gtk/adw_version.zig index 7ce88f585..6f7be52da 100644 --- a/src/apprt/gtk/adw_version.zig +++ b/src/apprt/gtk/adw_version.zig @@ -27,7 +27,7 @@ pub fn getRuntimeVersion() std.SemanticVersion { } pub fn logVersion() void { - log.info("libadwaita version build={} runtime={}", .{ + log.info("libadwaita version build={f} runtime={f}", .{ comptime_version, getRuntimeVersion(), }); diff --git a/src/apprt/gtk/build/blueprint.zig b/src/apprt/gtk/build/blueprint.zig index 1e614f972..f25e7e1f9 100644 --- a/src/apprt/gtk/build/blueprint.zig +++ b/src/apprt/gtk/build/blueprint.zig @@ -45,7 +45,7 @@ pub fn main() !void { std.debug.print( \\`libadwaita` is too old. \\ - \\Ghostty requires a version {} or newer of `libadwaita` to + \\Ghostty requires a version {f} or newer of `libadwaita` to \\compile this blueprint. Please install it, ensure that it is \\available on your PATH, and then retry building Ghostty. , .{required_adwaita_version}); @@ -80,7 +80,7 @@ pub fn main() !void { std.debug.print( \\`blueprint-compiler` not found. \\ - \\Ghostty requires version {} or newer of + \\Ghostty requires version {f} or newer of \\`blueprint-compiler` as a build-time dependency starting \\from version 1.2. Please install it, ensure that it is \\available on your PATH, and then retry building Ghostty. @@ -104,7 +104,7 @@ pub fn main() !void { std.debug.print( \\`blueprint-compiler` is the wrong version. \\ - \\Ghostty requires version {} or newer of + \\Ghostty requires version {f} or newer of \\`blueprint-compiler` as a build-time dependency starting \\from version 1.2. Please install it, ensure that it is \\available on your PATH, and then retry building Ghostty. @@ -145,7 +145,7 @@ pub fn main() !void { std.debug.print( \\`blueprint-compiler` not found. \\ - \\Ghostty requires version {} or newer of + \\Ghostty requires version {f} or newer of \\`blueprint-compiler` as a build-time dependency starting \\from version 1.2. Please install it, ensure that it is \\available on your PATH, and then retry building Ghostty. diff --git a/src/apprt/gtk/build/gresource.zig b/src/apprt/gtk/build/gresource.zig index 1f253fd5e..cc701d7c2 100644 --- a/src/apprt/gtk/build/gresource.zig +++ b/src/apprt/gtk/build/gresource.zig @@ -46,6 +46,7 @@ pub const blueprints: []const Blueprint = &.{ .{ .major = 1, .minor = 5, .name = "split-tree" }, .{ .major = 1, .minor = 5, .name = "split-tree-split" }, .{ .major = 1, .minor = 2, .name = "surface" }, + .{ .major = 1, .minor = 5, .name = "surface-scrolled-window" }, .{ .major = 1, .minor = 5, .name = "surface-title-dialog" }, .{ .major = 1, .minor = 3, .name = "surface-child-exited" }, .{ .major = 1, .minor = 5, .name = "tab" }, @@ -142,7 +143,9 @@ pub fn main() !void { ); } - const writer = std.io.getStdOut().writer(); + var buf: [4096]u8 = undefined; + var stdout = std.fs.File.stdout().writer(&buf); + const writer = &stdout.interface; try writer.writeAll( \\ \\ @@ -157,12 +160,14 @@ pub fn main() !void { \\ \\ ); + + try stdout.end(); } /// Generate the icon resources. This works by looking up all the icons /// specified by `icon_sizes` in `images/icons/`. They are asserted to exist /// by trying to access the file. -fn genIcons(writer: anytype) !void { +fn genIcons(writer: *std.Io.Writer) !void { try writer.print( \\ \\ @@ -204,7 +209,7 @@ fn genIcons(writer: anytype) !void { } /// Generate the resources at the root prefix. -fn genRoot(writer: anytype) !void { +fn genRoot(writer: *std.Io.Writer) !void { try writer.print( \\ \\ @@ -236,7 +241,7 @@ fn genRoot(writer: anytype) !void { /// assuming these will be fn genUi( alloc: Allocator, - writer: anytype, + writer: *std.Io.Writer, files: *const std.ArrayListUnmanaged([]const u8), ) !void { try writer.print( diff --git a/src/apprt/gtk/cgroup.zig b/src/apprt/gtk/cgroup.zig index 23c4d545e..697126798 100644 --- a/src/apprt/gtk/cgroup.zig +++ b/src/apprt/gtk/cgroup.zig @@ -50,7 +50,7 @@ pub fn init( ) orelse ""; if (!std.mem.eql(u8, original, current)) break :transient current; alloc.free(current); - std.time.sleep(25 * std.time.ns_per_ms); + std.Thread.sleep(25 * std.time.ns_per_ms); }; errdefer alloc.free(transient); log.info("transient scope created cgroup={s}", .{transient}); @@ -101,21 +101,21 @@ fn enableControllers(alloc: Allocator, cgroup: []const u8) !void { defer alloc.free(raw); // Build our string builder for enabling all controllers - var builder = std.ArrayList(u8).init(alloc); + var builder: std.Io.Writer.Allocating = .init(alloc); defer builder.deinit(); // Controllers are space-separated var it = std.mem.splitScalar(u8, raw, ' '); while (it.next()) |controller| { - try builder.append('+'); - try builder.appendSlice(controller); - if (it.rest().len > 0) try builder.append(' '); + try builder.writer.writeByte('+'); + try builder.writer.writeAll(controller); + if (it.rest().len > 0) try builder.writer.writeByte(' '); } // Enable them all try internal_os.cgroup.configureControllers( cgroup, - builder.items, + builder.written(), ); } diff --git a/src/apprt/gtk/class.zig b/src/apprt/gtk/class.zig index 4b46f8365..942666cf4 100644 --- a/src/apprt/gtk/class.zig +++ b/src/apprt/gtk/class.zig @@ -282,7 +282,7 @@ pub fn Common( fn setter(self: *Self, value: ?[:0]const u8) void { const priv = private(self); if (@field(priv, name)) |v| { - glib.free(@constCast(@ptrCast(v))); + glib.free(@ptrCast(@constCast(v))); } // We don't need to copy this because it was already diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 6ab3ad282..ceea6fee5 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -394,6 +394,14 @@ pub const Application = extern struct { .{ .detail = "config" }, ); + _ = gtk.CssProvider.signals.parsing_error.connect( + css_provider, + *Self, + signalCssParsingError, + self, + .{}, + ); + // Trigger initial config changes self.as(gobject.Object).notifyByPspec(properties.config.impl.param_spec); @@ -524,13 +532,23 @@ pub const Application = extern struct { if (!config.@"quit-after-last-window-closed") break :q false; // If the quit timer has expired, quit. - if (priv.quit_timer == .expired) break :q true; + if (priv.quit_timer == .expired) { + log.debug("must_quit due to quit timer expired", .{}); + break :q true; + } // If we have no windows attached to our app, also quit. - if (priv.requested_window and @as( - ?*glib.List, - self.as(gtk.Application).getWindows(), - ) == null) break :q true; + // We only do this if we don't have the closed delay set, + // because with the closed delay set we'll exit eventually. + if (config.@"quit-after-last-window-closed-delay" == null) { + if (priv.requested_window and @as( + ?*glib.List, + self.as(gtk.Application).getWindows(), + ) == null) { + log.debug("must_quit due to no app windows", .{}); + break :q true; + } + } // No quit conditions met break :q false; @@ -691,6 +709,8 @@ pub const Application = extern struct { .ring_bell => Action.ringBell(target), + .scrollbar => Action.scrollbar(target, value), + .set_title => Action.setTitle(target, value), .show_child_exited => return Action.showChildExited(target, value), @@ -707,6 +727,7 @@ pub const Application = extern struct { .toggle_command_palette => return Action.toggleCommandPalette(target), .toggle_split_zoom => return Action.toggleSplitZoom(target), .show_on_screen_keyboard => return Action.showOnScreenKeyboard(target), + .command_finished => return Action.commandFinished(target, value), // Unimplemented .secure_input, @@ -799,19 +820,19 @@ pub const Application = extern struct { } } - fn loadRuntimeCss(self: *Self) Allocator.Error!void { + fn loadRuntimeCss(self: *Self) (Allocator.Error || std.Io.Writer.Error)!void { const alloc = self.allocator(); + const priv: *Private = self.private(); + const config = priv.config.get(); - const config = self.private().config.get(); + var buf: std.Io.Writer.Allocating = try .initCapacity(alloc, 2048); + defer buf.deinit(); - var buf: std.ArrayListUnmanaged(u8) = try .initCapacity(alloc, 2048); - defer buf.deinit(alloc); - - const writer = buf.writer(alloc); + const writer = &buf.writer; // Load standard css first as it can override some of the user configured styling. - try loadRuntimeCss414(config, &writer); - try loadRuntimeCss416(config, &writer); + try loadRuntimeCss414(config, writer); + try loadRuntimeCss416(config, writer); const unfocused_fill: CoreConfig.Color = config.@"unfocused-split-fill" orelse config.background; @@ -851,25 +872,22 @@ pub const Application = extern struct { , .{ .font_family = font_family }); } - // ensure that we have a sentinel - try writer.writeByte(0); + const contents = buf.written(); - const data = buf.items[0 .. buf.items.len - 1 :0]; + log.debug("runtime CSS is {d} bytes", .{contents.len}); - log.debug("runtime CSS is {d} bytes", .{data.len + 1}); + const bytes = glib.Bytes.new(contents.ptr, contents.len); + defer bytes.unref(); // Clears any previously loaded CSS from this provider - loadCssProviderFromData( - self.private().css_provider, - data, - ); + priv.css_provider.loadFromBytes(bytes); } /// Load runtime CSS for older than GTK 4.16 fn loadRuntimeCss414( config: *const CoreConfig, - writer: *const std.ArrayListUnmanaged(u8).Writer, - ) Allocator.Error!void { + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { if (gtk_version.runtimeAtLeast(4, 16, 0)) return; const window_theme = config.@"window-theme"; @@ -904,8 +922,8 @@ pub const Application = extern struct { /// Load runtime for GTK 4.16 and newer fn loadRuntimeCss416( config: *const CoreConfig, - writer: *const std.ArrayListUnmanaged(u8).Writer, - ) Allocator.Error!void { + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { if (gtk_version.runtimeUntil(4, 16, 0)) return; const window_theme = config.@"window-theme"; @@ -1001,8 +1019,8 @@ pub const Application = extern struct { } } - fn loadCustomCss(self: *Self) !void { - const priv = self.private(); + fn loadCustomCss(self: *Self) (std.fs.File.ReadError || Allocator.Error)!void { + const priv: *Private = self.private(); const alloc = self.allocator(); const display = gdk.Display.getDefault() orelse { log.warn("unable to get display", .{}); @@ -1019,7 +1037,7 @@ pub const Application = extern struct { } priv.custom_css_providers.clearRetainingCapacity(); - const config = priv.config.getMut(); + const config = priv.config.get(); for (config.@"gtk-custom-css".value.items) |p| { const path, const optional = switch (p) { .optional => |path| .{ path, true }, @@ -1036,23 +1054,42 @@ pub const Application = extern struct { }; defer file.close(); + const css_file_size_limit = 5 * 1024 * 1024; // 5MB + log.info("loading gtk-custom-css path={s}", .{path}); - const contents = try file.reader().readAllAlloc( + const contents = file.readToEndAlloc( alloc, - 5 * 1024 * 1024, // 5MB, - ); + css_file_size_limit, + ) catch |err| switch (err) { + error.FileTooBig => { + log.warn("gtk-custom-css file {s} was larger than {Bi}", .{ path, css_file_size_limit }); + continue; + }, + else => |e| return e, + }; defer alloc.free(contents); - const data = try alloc.dupeZ(u8, contents); - defer alloc.free(data); + const bytes = glib.Bytes.new(contents.ptr, contents.len); + defer bytes.unref(); + + const css_provider = gtk.CssProvider.new(); + errdefer css_provider.unref(); + + _ = gtk.CssProvider.signals.parsing_error.connect( + css_provider, + *Self, + signalCssParsingError, + self, + .{}, + ); + + try priv.custom_css_providers.append(alloc, css_provider); + + css_provider.loadFromBytes(bytes); - const provider = gtk.CssProvider.new(); - errdefer provider.unref(); - try priv.custom_css_providers.append(alloc, provider); - loadCssProviderFromData(provider, data); gtk.StyleContext.addProviderForDisplay( display, - provider.as(gtk.StyleProvider), + css_provider.as(gtk.StyleProvider), gtk.STYLE_PROVIDER_PRIORITY_USER, ); } @@ -1108,8 +1145,8 @@ pub const Application = extern struct { // This should really never, never happen. Its not critical enough // to actually crash, but this is a bug somewhere. An accelerator // for a trigger can't possibly be more than 1024 bytes. - error.NoSpaceLeft => { - log.warn("accelerator somehow longer than 1024 bytes: {}", .{trigger}); + error.WriteFailed => { + log.warn("accelerator somehow longer than 1024 bytes: {f}", .{trigger}); return; }, }; @@ -1155,7 +1192,7 @@ pub const Application = extern struct { // just stuck with the old CSS but we don't want to fail the entire // config change operation. self.loadRuntimeCss() catch |err| switch (err) { - error.OutOfMemory => log.warn( + error.WriteFailed, error.OutOfMemory => log.warn( "out of memory loading runtime CSS, no runtime CSS applied", .{}, ), @@ -1168,6 +1205,37 @@ pub const Application = extern struct { }; } + /// Log CSS parsing error + fn signalCssParsingError( + _: *gtk.CssProvider, + css_section: *gtk.CssSection, + err: *glib.Error, + _: *Self, + ) callconv(.c) void { + const location = css_section.toString(); + defer glib.free(location); + if (comptime gtk_version.atLeast(4, 16, 0)) bytes: { + const bytes = css_section.getBytes() orelse break :bytes; + var len: usize = undefined; + const ptr = bytes.getData(&len) orelse break :bytes; + const data = ptr[0..len]; + log.warn("css parsing failed at {s}: {s} {d} {s}\n{s}", .{ + location, + glib.quarkToString(err.f_domain), + err.f_code, + err.f_message orelse "«unknown»", + data, + }); + return; + } + log.warn("css parsing failed at {s}: {s} {d} {s}", .{ + location, + glib.quarkToString(err.f_domain), + err.f_code, + err.f_message orelse "«unknown»", + }); + } + //--------------------------------------------------------------- // Libghostty Callbacks @@ -1818,13 +1886,13 @@ const Action = struct { target: apprt.Target, n: apprt.action.DesktopNotification, ) void { - // TODO: We should move the surface target to a function call - // on Surface and emit a signal that embedders can connect to. This - // will let us handle notifications differently depending on where - // a surface is presented. At the time of writing this, we always - // want to show the notification AND the logic below was directly - // ported from "legacy" GTK so this is fine, but I want to leave this - // note so we can do it one day. + switch (target) { + .app => {}, + .surface => |v| { + v.rt_surface.gobj().sendDesktopNotification(n.title, n.body); + return; + }, + } // Set a default title if we don't already have one const t = switch (n.title.len) { @@ -1839,14 +1907,9 @@ const Action = struct { const icon = gio.ThemedIcon.new("com.mitchellh.ghostty"); defer icon.unref(); notification.setIcon(icon.as(gio.Icon)); - - const pointer = glib.Variant.newUint64(switch (target) { - .app => 0, - .surface => |v| @intFromPtr(v), - }); notification.setDefaultActionAndTargetValue( "app.present-surface", - pointer, + glib.Variant.newUint64(0), ); // We set the notification ID to the body content. If the content is the @@ -2266,6 +2329,16 @@ const Action = struct { } } + pub fn scrollbar( + target: apprt.Target, + value: apprt.Action.Value(.scrollbar), + ) void { + switch (target) { + .app => {}, + .surface => |v| v.rt_surface.surface.setScrollbar(value), + } + } + pub fn setTitle( target: apprt.Target, value: apprt.action.SetTitle, @@ -2451,6 +2524,15 @@ const Action = struct { }, } } + + pub fn commandFinished(target: apprt.Target, value: apprt.Action.Value(.command_finished)) bool { + switch (target) { + .app => return false, + .surface => |surface| { + return surface.rt_surface.gobj().commandFinished(value); + }, + } + } }; /// This sets various GTK-related environment variables as necessary @@ -2567,8 +2649,3 @@ fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c) // Abusing integers to be enums and booleans is a terrible idea, C. return if (window.isActive() != 0) 0 else -1; } - -fn loadCssProviderFromData(provider: *gtk.CssProvider, data: [:0]const u8) void { - assert(gtk_version.runtimeAtLeast(4, 12, 0)); - provider.loadFromString(data); -} diff --git a/src/apprt/gtk/class/command_palette.zig b/src/apprt/gtk/class/command_palette.zig index 8b7bb328c..6da49115e 100644 --- a/src/apprt/gtk/class/command_palette.zig +++ b/src/apprt/gtk/class/command_palette.zig @@ -485,10 +485,11 @@ const Command = extern struct { const command = priv.command orelse return null; - priv.action_key = std.fmt.allocPrintZ( + priv.action_key = std.fmt.allocPrintSentinel( priv.arena.allocator(), - "{}", + "{f}", .{command.action}, + 0, ) catch null; return priv.action_key; diff --git a/src/apprt/gtk/class/config.zig b/src/apprt/gtk/class/config.zig index 2b98c68b5..eadd3b7b8 100644 --- a/src/apprt/gtk/class/config.zig +++ b/src/apprt/gtk/class/config.zig @@ -117,10 +117,10 @@ pub const Config = extern struct { errdefer text_buf.unref(); var buf: [4095:0]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); + var writer: std.Io.Writer = .fixed(&buf); for (config._diagnostics.items()) |diag| { - fbs.reset(); - diag.write(fbs.writer()) catch |err| { + writer.end = 0; + diag.format(&writer) catch |err| { log.warn( "error writing diagnostic to buffer err={}", .{err}, @@ -128,7 +128,7 @@ pub const Config = extern struct { continue; }; - text_buf.insertAtCursor(&buf, @intCast(fbs.pos)); + text_buf.insertAtCursor(&buf, @intCast(writer.end)); text_buf.insertAtCursor("\n", 1); } diff --git a/src/apprt/gtk/class/global_shortcuts.zig b/src/apprt/gtk/class/global_shortcuts.zig index 18280cfe9..9c67be7c1 100644 --- a/src/apprt/gtk/class/global_shortcuts.zig +++ b/src/apprt/gtk/class/global_shortcuts.zig @@ -188,9 +188,9 @@ pub const GlobalShortcuts = extern struct { // If there isn't space to translate the trigger, then our // buffer might be too small (but 1024 is insane!). In any case // we don't want to stop registering globals. - error.NoSpaceLeft => { + error.WriteFailed => { log.warn( - "buffer too small to translate trigger, ignoring={}", + "buffer too small to translate trigger, ignoring={f}", .{entry.key_ptr.*}, ); continue; @@ -257,7 +257,7 @@ pub const GlobalShortcuts = extern struct { const trigger = entry.key_ptr.*.ptr; const action = std.fmt.bufPrintZ( &action_buf, - "{}", + "{f}", .{entry.value_ptr.*}, ) catch continue; diff --git a/src/apprt/gtk/class/resize_overlay.zig b/src/apprt/gtk/class/resize_overlay.zig index 9bb9a0a7c..f6e0c1442 100644 --- a/src/apprt/gtk/class/resize_overlay.zig +++ b/src/apprt/gtk/class/resize_overlay.zig @@ -172,7 +172,7 @@ pub const ResizeOverlay = extern struct { /// overlay if it is currently hidden; you must call schedule. pub fn setLabel(self: *Self, label: ?[:0]const u8) void { const priv = self.private(); - if (priv.label_text) |v| glib.free(@constCast(@ptrCast(v))); + if (priv.label_text) |v| glib.free(@ptrCast(@constCast(v))); priv.label_text = null; if (label) |v| priv.label_text = glib.ext.dupeZ(u8, v); self.as(gobject.Object).notifyByPspec(properties.label.impl.param_spec); @@ -285,7 +285,7 @@ pub const ResizeOverlay = extern struct { fn finalize(self: *Self) callconv(.c) void { const priv = self.private(); if (priv.label_text) |v| { - glib.free(@constCast(@ptrCast(v))); + glib.free(@ptrCast(@constCast(v))); priv.label_text = null; } diff --git a/src/apprt/gtk/class/split_tree.zig b/src/apprt/gtk/class/split_tree.zig index 755b51e9a..1c901b1bb 100644 --- a/src/apprt/gtk/class/split_tree.zig +++ b/src/apprt/gtk/class/split_tree.zig @@ -22,6 +22,7 @@ const Config = @import("config.zig").Config; const Application = @import("application.zig").Application; const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog; const Surface = @import("surface.zig").Surface; +const SurfaceScrolledWindow = @import("surface_scrolled_window.zig").SurfaceScrolledWindow; const log = std.log.scoped(.gtk_ghostty_split_tree); @@ -198,7 +199,7 @@ pub const SplitTree = extern struct { .init("zoom", actionZoom, null), }; - ext.actions.addAsGroup(Self, self, "split-tree", &actions); + _ = ext.actions.addAsGroup(Self, self, "split-tree", &actions); } /// Create a new split in the given direction from the currently @@ -268,7 +269,7 @@ pub const SplitTree = extern struct { ); defer new_tree.deinit(); log.debug( - "new split at={} direction={} old_tree={} new_tree={}", + "new split at={} direction={} old_tree={f} new_tree={f}", .{ handle, direction, old_tree, &new_tree }, ); @@ -874,7 +875,9 @@ pub const SplitTree = extern struct { current: Surface.Tree.Node.Handle, ) *gtk.Widget { return switch (tree.nodes[current.idx()]) { - .leaf => |v| v.as(gtk.Widget), + .leaf => |v| gobject.ext.newInstance(SurfaceScrolledWindow, .{ + .surface = v, + }).as(gtk.Widget), .split => |s| SplitTreeSplit.new( current, &s, diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index fb933073c..646ad5dbd 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -32,6 +32,7 @@ const TitleDialog = @import("surface_title_dialog.zig").SurfaceTitleDialog; const Window = @import("window.zig").Window; const WeakRef = @import("../weak_ref.zig").WeakRef; const InspectorWindow = @import("inspector_window.zig").InspectorWindow; +const i18n = @import("../../../os/i18n.zig"); const log = std.log.scoped(.gtk_ghostty_surface); @@ -39,18 +40,29 @@ pub const Surface = extern struct { const Self = @This(); parent_instance: Parent, pub const Parent = adw.Bin; + pub const Implements = [_]type{gtk.Scrollable}; pub const getGObjectType = gobject.ext.defineClass(Self, .{ .name = "GhosttySurface", .instanceInit = &init, .classInit = &Class.init, .parent_class = &Class.parent, .private = .{ .Type = Private, .offset = &Private.offset }, + .implements = &.{ + gobject.ext.implement(gtk.Scrollable, .{}), + }, }); /// A SplitTree implementation that stores surfaces. pub const Tree = datastruct.SplitTree(Self); pub const properties = struct { + /// This property is set to true when the bell is ringing. Note that + /// this property will only emit a changed signal when there is a + /// full state change. If a bell is ringing and another bell event + /// comes through, the change notification will NOT be emitted. + /// + /// If you need to know every scenario the bell is triggered, + /// listen to the `bell` signal instead. pub const @"bell-ringing" = struct { pub const name = "bell-ringing"; const impl = gobject.ext.defineProperty( @@ -293,9 +305,78 @@ pub const Surface = extern struct { }, ); }; + + pub const hadjustment = struct { + pub const name = "hadjustment"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*gtk.Adjustment, + .{ + .accessor = .{ + .getter = getHAdjustmentValue, + .setter = setHAdjustmentValue, + }, + }, + ); + }; + + pub const vadjustment = struct { + pub const name = "vadjustment"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*gtk.Adjustment, + .{ + .accessor = .{ + .getter = getVAdjustmentValue, + .setter = setVAdjustmentValue, + }, + }, + ); + }; + + pub const @"hscroll-policy" = struct { + pub const name = "hscroll-policy"; + const impl = gobject.ext.defineProperty( + name, + Self, + gtk.ScrollablePolicy, + .{ + .default = .natural, + .accessor = C.privateShallowFieldAccessor("hscroll_policy"), + }, + ); + }; + + pub const @"vscroll-policy" = struct { + pub const name = "vscroll-policy"; + const impl = gobject.ext.defineProperty( + name, + Self, + gtk.ScrollablePolicy, + .{ + .default = .natural, + .accessor = C.privateShallowFieldAccessor("vscroll_policy"), + }, + ); + }; }; pub const signals = struct { + /// Emitted whenever the bell event is received. Unlike the + /// `bell-ringing` property, this is emitted every time the event + /// is received and not just on state changes. + pub const bell = struct { + pub const name = "bell"; + pub const connect = impl.connect; + const impl = gobject.ext.defineSignal( + name, + Self, + &.{}, + void, + ); + }; /// Emitted whenever the surface would like to be closed for any /// reason. /// @@ -525,6 +606,15 @@ pub const Surface = extern struct { // unfocused-split-* options is_split: bool = false, + action_group: ?*gio.SimpleActionGroup = null, + + // Gtk.Scrollable interface adjustments + hadj: ?*gtk.Adjustment = null, + vadj: ?*gtk.Adjustment = null, + hscroll_policy: gtk.ScrollablePolicy = .natural, + vscroll_policy: gtk.ScrollablePolicy = .natural, + vadj_signal_group: ?*gobject.SignalGroup = null, + // Template binds child_exited_overlay: *ChildExited, context_menu: *gtk.PopoverMenu, @@ -691,6 +781,47 @@ pub const Surface = extern struct { return priv.im_context.as(gtk.IMContext).activateOsk(event) != 0; } + /// Set the scrollbar state for this surface. This will setup the + /// properties for our Gtk.Scrollable interface properly. + pub fn setScrollbar(self: *Self, scrollbar: terminal.Scrollbar) void { + // Update existing adjustment in-place. If we don't have an + // adjustment then we do nothing because we're not part of a + // scrolled window. + const vadj = self.getVAdjustment() orelse return; + + // Check if values match existing adjustment and skip update if so + const value: f64 = @floatFromInt(scrollbar.offset); + const upper: f64 = @floatFromInt(scrollbar.total); + const page_size: f64 = @floatFromInt(scrollbar.len); + + if (std.math.approxEqAbs(f64, vadj.getValue(), value, 0.001) and + std.math.approxEqAbs(f64, vadj.getUpper(), upper, 0.001) and + std.math.approxEqAbs(f64, vadj.getPageSize(), page_size, 0.001)) + { + return; + } + + // If we have a vadjustment we MUST have the signal group since + // it is setup in the prop handler. + const priv = self.private(); + const group = priv.vadj_signal_group.?; + + // During manual scrollbar changes from Ghostty core we don't + // want to emit value-changed signals so we block them. This would + // cause a waste of resources at best and infinite loops at worst. + group.block(); + defer group.unblock(); + + vadj.configure( + value, // value: current scroll position + 0, // lower: minimum value + upper, // upper: maximum value (total scrollable area) + 1, // step_increment: amount to scroll on arrow click + page_size, // page_increment: amount to scroll on page up/down + page_size, // page_size: size of visible area + ); + } + /// Set the current progress report state. pub fn setProgressReport( self: *Self, @@ -789,6 +920,67 @@ pub const Surface = extern struct { ); } + pub fn commandFinished(self: *Self, value: apprt.Action.Value(.command_finished)) bool { + const app = Application.default(); + const alloc = app.allocator(); + const priv: *Private = self.private(); + + const notify_next_command_finish = notify: { + const simple_action_group = priv.action_group orelse break :notify false; + const action_group = simple_action_group.as(gio.ActionGroup); + const state = action_group.getActionState("notify-on-next-command-finish") orelse break :notify false; + const bool_variant_type = glib.ext.VariantType.newFor(bool); + defer bool_variant_type.free(); + if (state.isOfType(bool_variant_type) == 0) break :notify false; + const notify = state.getBoolean() != 0; + action_group.changeActionState("notify-on-next-command-finish", glib.Variant.newBoolean(@intFromBool(false))); + break :notify notify; + }; + + const config = priv.config orelse return false; + + const cfg = config.get(); + + if (!notify_next_command_finish) { + if (cfg.@"notify-on-command-finish" == .never) return true; + if (cfg.@"notify-on-command-finish" == .unfocused and self.getFocused()) return true; + } + + if (value.duration.lte(cfg.@"notify-on-command-finish-after")) return true; + + const action = cfg.@"notify-on-command-finish-action"; + + if (action.bell) self.setBellRinging(true); + + if (action.notify) notify: { + const title_ = title: { + const exit_code = value.exit_code orelse break :title i18n._("Command Finished"); + if (exit_code == 0) break :title i18n._("Command Succeeded"); + break :title i18n._("Command Failed"); + }; + const title = std.mem.span(title_); + const body = body: { + const exit_code = value.exit_code orelse break :body std.fmt.allocPrintSentinel( + alloc, + "Command took {f}.", + .{value.duration.round(std.time.ns_per_ms)}, + 0, + ) catch break :notify; + break :body std.fmt.allocPrintSentinel( + alloc, + "Command took {f} and exited with code {d}.", + .{ value.duration.round(std.time.ns_per_ms), exit_code }, + 0, + ) catch break :notify; + }; + defer alloc.free(body); + + self.sendDesktopNotification(title, body); + } + + return true; + } + /// Key press event (press or release). /// /// At a high level, we want to construct an `input.KeyEvent` and @@ -1295,11 +1487,11 @@ pub const Surface = extern struct { defer arena.deinit(); const alloc = arena.allocator(); - var env_to_remove = std.ArrayList([]const u8).init(alloc); - var env_to_update = std.ArrayList(struct { + var env_to_remove: std.ArrayList([]const u8) = .empty; + var env_to_update: std.ArrayList(struct { key: []const u8, value: []const u8, - }).init(alloc); + }) = .empty; var it = env_map.iterator(); while (it.next()) |entry| { @@ -1312,13 +1504,11 @@ pub const Surface = extern struct { // Any env var starting with SNAP must be removed if (std.mem.startsWith(u8, key, "SNAP_")) { - try env_to_remove.append(key); + try env_to_remove.append(alloc, key); continue; } - var filtered_paths = std.ArrayList([]const u8).init(alloc); - defer filtered_paths.deinit(); - + var filtered_paths: std.ArrayList([]const u8) = .empty; var modified = false; var paths = std.mem.splitAny(u8, value, ":"); while (paths.next()) |path| { @@ -1331,15 +1521,15 @@ pub const Surface = extern struct { break; } }; - if (include) try filtered_paths.append(path); + if (include) try filtered_paths.append(alloc, path); } if (modified) { if (filtered_paths.items.len > 0) { const new_value = try std.mem.join(alloc, ":", filtered_paths.items); - try env_to_update.append(.{ .key = key, .value = new_value }); + try env_to_update.append(alloc, .{ .key = key, .value = new_value }); } else { - try env_to_remove.append(key); + try env_to_remove.append(alloc, key); } } } @@ -1384,6 +1574,40 @@ pub const Surface = extern struct { _ = priv.gl_area.as(gtk.Widget).grabFocus(); } + pub fn sendDesktopNotification(self: *Self, title: [:0]const u8, body: [:0]const u8) void { + const app = Application.default(); + const priv: *Private = self.private(); + + const core_surface = priv.core_surface orelse { + log.warn("can't send notification because there is no core surface", .{}); + return; + }; + + const t = switch (title.len) { + 0 => "Ghostty", + else => title, + }; + + const notification = gio.Notification.new(t); + defer notification.unref(); + notification.setBody(body); + + const icon = gio.ThemedIcon.new("com.mitchellh.ghostty"); + defer icon.unref(); + notification.setIcon(icon.as(gio.Icon)); + + const pointer = glib.Variant.newUint64(@intFromPtr(core_surface)); + notification.setDefaultActionAndTargetValue( + "app.present-surface", + pointer, + ); + + // We set the notification ID to the body content. If the content is the + // same, this notification may replace a previous notification + const gio_app = app.as(gio.Application); + gio_app.sendNotification(body, notification); + } + //--------------------------------------------------------------- // Virtual Methods @@ -1403,6 +1627,7 @@ pub const Surface = extern struct { priv.mouse_hidden = false; priv.focused = true; priv.size = .{ .width = 0, .height = 0 }; + priv.vadj_signal_group = null; // If our configuration is null then we get the configuration // from the application. @@ -1440,11 +1665,23 @@ pub const Surface = extern struct { } fn initActionMap(self: *Self) void { + const priv: *Private = self.private(); + const actions = [_]ext.actions.Action(Self){ - .init("prompt-title", actionPromptTitle, null), + .init( + "prompt-title", + actionPromptTitle, + null, + ), + .initStateful( + "notify-on-next-command-finish", + actionNotifyOnNextCommandFinish, + null, + glib.Variant.newBoolean(@intFromBool(false)), + ), }; - ext.actions.addAsGroup(Self, self, "surface", &actions); + priv.action_group = ext.actions.addAsGroup(Self, self, "surface", &actions); } fn dispose(self: *Self) callconv(.c) void { @@ -1455,6 +1692,22 @@ pub const Surface = extern struct { priv.config = null; } + if (priv.vadj_signal_group) |group| { + group.setTarget(null); + group.as(gobject.Object).unref(); + priv.vadj_signal_group = null; + } + + if (priv.hadj) |v| { + v.as(gobject.Object).unref(); + priv.hadj = null; + } + + if (priv.vadj) |v| { + v.as(gobject.Object).unref(); + priv.vadj = null; + } + if (priv.progress_bar_timer) |timer| { if (glib.Source.remove(timer) == 0) { log.warn("unable to remove progress bar timer", .{}); @@ -1506,7 +1759,7 @@ pub const Surface = extern struct { priv.core_surface = null; } if (priv.mouse_hover_url) |v| { - glib.free(@constCast(@ptrCast(v))); + glib.free(@ptrCast(@constCast(v))); priv.mouse_hover_url = null; } if (priv.default_size) |v| { @@ -1522,15 +1775,15 @@ pub const Surface = extern struct { priv.min_size = null; } if (priv.pwd) |v| { - glib.free(@constCast(@ptrCast(v))); + glib.free(@ptrCast(@constCast(v))); priv.pwd = null; } if (priv.title) |v| { - glib.free(@constCast(@ptrCast(v))); + glib.free(@ptrCast(@constCast(v))); priv.title = null; } if (priv.title_override) |v| { - glib.free(@constCast(@ptrCast(v))); + glib.free(@ptrCast(@constCast(v))); priv.title_override = null; } self.clearCgroup(); @@ -1554,7 +1807,7 @@ pub const Surface = extern struct { /// title. For manually set titles see `setTitleOverride`. pub fn setTitle(self: *Self, title: ?[:0]const u8) void { const priv = self.private(); - if (priv.title) |v| glib.free(@constCast(@ptrCast(v))); + if (priv.title) |v| glib.free(@ptrCast(@constCast(v))); priv.title = null; if (title) |v| priv.title = glib.ext.dupeZ(u8, v); self.as(gobject.Object).notifyByPspec(properties.title.impl.param_spec); @@ -1564,7 +1817,7 @@ pub const Surface = extern struct { /// unless this is unset (null). pub fn setTitleOverride(self: *Self, title: ?[:0]const u8) void { const priv = self.private(); - if (priv.title_override) |v| glib.free(@constCast(@ptrCast(v))); + if (priv.title_override) |v| glib.free(@ptrCast(@constCast(v))); priv.title_override = null; if (title) |v| priv.title_override = glib.ext.dupeZ(u8, v); self.as(gobject.Object).notifyByPspec(properties.@"title-override".impl.param_spec); @@ -1578,7 +1831,7 @@ pub const Surface = extern struct { /// Set the pwd for this surface, copies the value. pub fn setPwd(self: *Self, pwd: ?[:0]const u8) void { const priv = self.private(); - if (priv.pwd) |v| glib.free(@constCast(@ptrCast(v))); + if (priv.pwd) |v| glib.free(@ptrCast(@constCast(v))); priv.pwd = null; if (pwd) |v| priv.pwd = glib.ext.dupeZ(u8, v); self.as(gobject.Object).notifyByPspec(properties.pwd.impl.param_spec); @@ -1663,7 +1916,7 @@ pub const Surface = extern struct { pub fn setMouseHoverUrl(self: *Self, url: ?[:0]const u8) void { const priv = self.private(); - if (priv.mouse_hover_url) |v| glib.free(@constCast(@ptrCast(v))); + if (priv.mouse_hover_url) |v| glib.free(@ptrCast(@constCast(v))); priv.mouse_hover_url = null; if (url) |v| priv.mouse_hover_url = glib.ext.dupeZ(u8, v); self.as(gobject.Object).notifyByPspec(properties.@"mouse-hover-url".impl.param_spec); @@ -1674,6 +1927,16 @@ pub const Surface = extern struct { } pub fn setBellRinging(self: *Self, ringing: bool) void { + // Prevent duplicate change notifications if the signals we emit + // in this function cause this state to change again. + self.as(gobject.Object).freezeNotify(); + defer self.as(gobject.Object).thawNotify(); + + // Logic around bell reaction happens on every event even if we're + // already in the ringing state. + if (ringing) self.ringBell(); + + // Property change only happens on actual state change const priv = self.private(); if (priv.bell_ringing == ringing) return; priv.bell_ringing = ringing; @@ -1858,20 +2121,63 @@ pub const Surface = extern struct { self.as(gtk.Widget).setCursorFromName(name.ptr); } - fn propBellRinging( + fn vadjValueChanged(adj: *gtk.Adjustment, self: *Self) callconv(.c) void { + // This will trigger for every single pixel change in the adjustment, + // but our core surface handles the noise from this so that identical + // rows are cheap. + const core_surface = self.core() orelse return; + const row: usize = @intFromFloat(@round(adj.getValue())); + _ = core_surface.performBindingAction(.{ .scroll_to_row = row }) catch |err| { + log.err("error performing scroll_to_row action err={}", .{err}); + }; + } + + fn propVAdjustment( self: *Self, _: *gobject.ParamSpec, _: ?*anyopaque, ) callconv(.c) void { const priv = self.private(); - if (!priv.bell_ringing) return; + + // When vadjustment is first set, we setup the signal group lazily. + // This makes it so that if we don't use scrollbars, we never + // pay the memory cost of this. + const group: *gobject.SignalGroup = priv.vadj_signal_group orelse group: { + const group = gobject.SignalGroup.new(gtk.Adjustment.getGObjectType()); + group.connect( + "value-changed", + @ptrCast(&vadjValueChanged), + self, + ); + + priv.vadj_signal_group = group; + break :group group; + }; + + // Setup our signal group target + group.setTarget(if (priv.vadj) |v| v.as(gobject.Object) else null); + } + + /// Handle bell features that need to happen every time a BEL is received + /// Currently this is audio and system but this could change in the future. + fn ringBell(self: *Self) void { + const priv = self.private(); + + // Emit the signal + signals.bell.impl.emit( + self, + null, + .{}, + null, + ); // Activate actions if they exist _ = self.as(gtk.Widget).activateAction("tab.ring-bell", null); _ = self.as(gtk.Widget).activateAction("win.ring-bell", null); - // Do our sound const config = if (priv.config) |c| c.get() else return; + + // Do our sound if (config.@"bell-features".audio) audio: { const config_path = config.@"bell-audio-path" orelse break :audio; const path, const required = switch (config_path) { @@ -1916,6 +2222,66 @@ pub const Surface = extern struct { } } + //--------------------------------------------------------------- + // Gtk.Scrollable interface implementation + + pub fn getHAdjustment(self: *Self) ?*gtk.Adjustment { + return self.private().hadj; + } + + pub fn setHAdjustment(self: *Self, adj_: ?*gtk.Adjustment) void { + self.as(gobject.Object).freezeNotify(); + defer self.as(gobject.Object).thawNotify(); + self.as(gobject.Object).notifyByPspec(properties.hadjustment.impl.param_spec); + + const priv = self.private(); + if (priv.hadj) |old| { + old.as(gobject.Object).unref(); + priv.hadj = null; + } + + const adj = adj_ orelse return; + _ = adj.as(gobject.Object).ref(); + priv.hadj = adj; + } + + fn getHAdjustmentValue(self: *Self, value: *gobject.Value) void { + gobject.ext.Value.set(value, self.getHAdjustment()); + } + + fn setHAdjustmentValue(self: *Self, value: *const gobject.Value) void { + self.setHAdjustment(gobject.ext.Value.get(value, ?*gtk.Adjustment)); + } + + pub fn getVAdjustment(self: *Self) ?*gtk.Adjustment { + return self.private().vadj; + } + + pub fn setVAdjustment(self: *Self, adj_: ?*gtk.Adjustment) void { + self.as(gobject.Object).freezeNotify(); + defer self.as(gobject.Object).thawNotify(); + self.as(gobject.Object).notifyByPspec(properties.vadjustment.impl.param_spec); + + const priv = self.private(); + + if (priv.vadj) |old| { + old.as(gobject.Object).unref(); + priv.vadj = null; + } + + const adj = adj_ orelse return; + _ = adj.as(gobject.Object).ref(); + priv.vadj = adj; + } + + fn getVAdjustmentValue(self: *Self, value: *gobject.Value) void { + gobject.ext.Value.set(value, self.getVAdjustment()); + } + + fn setVAdjustmentValue(self: *Self, value: *const gobject.Value) void { + self.setVAdjustment(gobject.ext.Value.get(value, ?*gtk.Adjustment)); + } + //--------------------------------------------------------------- // Signal Handlers @@ -1930,6 +2296,20 @@ pub const Surface = extern struct { }; } + pub fn actionNotifyOnNextCommandFinish( + action: *gio.SimpleAction, + _: ?*glib.Variant, + _: *Self, + ) callconv(.c) void { + const state = action.as(gio.Action).getState() orelse glib.Variant.newBoolean(@intFromBool(false)); + defer state.unref(); + const bool_variant_type = glib.ext.VariantType.newFor(bool); + defer bool_variant_type.free(); + if (state.isOfType(bool_variant_type) == 0) return; + const value = state.getBoolean() != 0; + action.setState(glib.Variant.newBoolean(@intFromBool(!value))); + } + fn childExitedClose( _: *ChildExited, self: *Self, @@ -1967,13 +2347,11 @@ pub const Surface = extern struct { const alloc = Application.default().allocator(); if (ext.gValueHolds(value, gdk.FileList.getGObjectType())) { - var data = std.ArrayList(u8).init(alloc); - defer data.deinit(); + var stream: std.Io.Writer.Allocating = .init(alloc); + defer stream.deinit(); - var shell_escape_writer: internal_os.ShellEscapeWriter(std.ArrayList(u8).Writer) = .{ - .child_writer = data.writer(), - }; - const writer = shell_escape_writer.writer(); + var shell_escape_writer: internal_os.ShellEscapeWriter = .init(&stream.writer); + const writer = &shell_escape_writer.writer; const list: ?*glib.SList = list: { const unboxed = value.getBoxed() orelse return 0; @@ -2001,7 +2379,7 @@ pub const Surface = extern struct { } } - const string = data.toOwnedSliceSentinel(0) catch |err| { + const string = stream.toOwnedSliceSentinel(0) catch |err| { log.err("unable to convert to a slice: {}", .{err}); return 0; }; @@ -2014,13 +2392,11 @@ pub const Surface = extern struct { const object = value.getObject() orelse return 0; const file = gobject.ext.cast(gio.File, object) orelse return 0; const path = file.getPath() orelse return 0; - var data = std.ArrayList(u8).init(alloc); - defer data.deinit(); + var stream: std.Io.Writer.Allocating = .init(alloc); + defer stream.deinit(); - var shell_escape_writer: internal_os.ShellEscapeWriter(std.ArrayList(u8).Writer) = .{ - .child_writer = data.writer(), - }; - const writer = shell_escape_writer.writer(); + var shell_escape_writer: internal_os.ShellEscapeWriter = .init(&stream.writer); + const writer = &shell_escape_writer.writer; writer.writeAll(std.mem.span(path)) catch |err| { log.err("unable to write path to buffer: {}", .{err}); return 0; @@ -2030,7 +2406,7 @@ pub const Surface = extern struct { return 0; }; - const string = data.toOwnedSliceSentinel(0) catch |err| { + const string = stream.toOwnedSliceSentinel(0) catch |err| { log.err("unable to convert to a slice: {}", .{err}); return 0; }; @@ -2859,7 +3235,7 @@ pub const Surface = extern struct { class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl); class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden); class.bindTemplateCallback("notify_mouse_shape", &propMouseShape); - class.bindTemplateCallback("notify_bell_ringing", &propBellRinging); + class.bindTemplateCallback("notify_vadjustment", &propVAdjustment); class.bindTemplateCallback("should_border_be_shown", &closureShouldBorderBeShown); class.bindTemplateCallback("should_unfocused_split_be_shown", &closureShouldUnfocusedSplitBeShown); @@ -2881,9 +3257,16 @@ pub const Surface = extern struct { properties.@"title-override".impl, properties.zoom.impl, properties.@"is-split".impl, + + // For Gtk.Scrollable + properties.hadjustment.impl, + properties.vadjustment.impl, + properties.@"hscroll-policy".impl, + properties.@"vscroll-policy".impl, }); // Signals + signals.bell.impl.register(.{}); signals.@"close-request".impl.register(.{}); signals.@"clipboard-read".impl.register(.{}); signals.@"clipboard-write".impl.register(.{}); diff --git a/src/apprt/gtk/class/surface_scrolled_window.zig b/src/apprt/gtk/class/surface_scrolled_window.zig new file mode 100644 index 000000000..3095b4c78 --- /dev/null +++ b/src/apprt/gtk/class/surface_scrolled_window.zig @@ -0,0 +1,209 @@ +const std = @import("std"); +const assert = std.debug.assert; +const adw = @import("adw"); +const gobject = @import("gobject"); +const gtk = @import("gtk"); + +const gresource = @import("../build/gresource.zig"); +const Common = @import("../class.zig").Common; +const Surface = @import("surface.zig").Surface; +const Config = @import("config.zig").Config; + +const log = std.log.scoped(.gtk_ghostty_surface_scrolled_window); + +/// A wrapper widget that embeds a Surface inside a GtkScrolledWindow. +/// This provides scrollbar functionality for the terminal surface. +/// The surface property can be set during initialization or changed +/// dynamically via the surface property. +pub const SurfaceScrolledWindow = extern struct { + const Self = @This(); + parent_instance: Parent, + pub const Parent = adw.Bin; + pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .name = "GhostttySurfaceScrolledWindow", + .instanceInit = &init, + .classInit = &Class.init, + .parent_class = &Class.parent, + .private = .{ .Type = Private, .offset = &Private.offset }, + }); + + pub const properties = struct { + pub const config = struct { + pub const name = "config"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*Config, + .{ + .accessor = C.privateObjFieldAccessor("config"), + }, + ); + }; + + pub const surface = struct { + pub const name = "surface"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*Surface, + .{ + .accessor = .{ + .getter = getSurfaceValue, + .setter = setSurfaceValue, + }, + }, + ); + }; + }; + + const Private = struct { + config: ?*Config = null, + config_binding: ?*gobject.Binding = null, + surface: ?*Surface = null, + pub var offset: c_int = 0; + }; + + fn init(self: *Self, _: *Class) callconv(.c) void { + gtk.Widget.initTemplate(self.as(gtk.Widget)); + } + + fn dispose(self: *Self) callconv(.c) void { + const priv = self.private(); + + if (priv.config_binding) |binding| { + binding.as(gobject.Object).unref(); + priv.config_binding = null; + } + + if (priv.config) |v| { + v.unref(); + priv.config = null; + } + + 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 { + gobject.Object.virtual_methods.finalize.call( + Class.parent, + self.as(Parent), + ); + } + + fn getSurfaceValue(self: *Self, value: *gobject.Value) void { + gobject.ext.Value.set( + value, + self.private().surface, + ); + } + + fn setSurfaceValue(self: *Self, value: *const gobject.Value) void { + self.setSurface(gobject.ext.Value.get( + value, + ?*Surface, + )); + } + + pub fn getSurface(self: *Self) ?*Surface { + return self.private().surface; + } + + pub fn setSurface(self: *Self, surface_: ?*Surface) void { + const priv = self.private(); + + if (surface_ == priv.surface) return; + + self.as(gobject.Object).freezeNotify(); + defer self.as(gobject.Object).thawNotify(); + self.as(gobject.Object).notifyByPspec(properties.surface.impl.param_spec); + + priv.surface = surface_; + } + + fn closureScrollbarPolicy( + _: *Self, + config_: ?*Config, + ) callconv(.c) gtk.PolicyType { + const config = if (config_) |c| c.get() else return .automatic; + return switch (config.scrollbar) { + .never => .never, + .system => .automatic, + }; + } + + fn propSurface( + self: *Self, + _: *gobject.ParamSpec, + _: ?*anyopaque, + ) callconv(.c) void { + const priv = self.private(); + const child: *gtk.Widget = self.as(Parent).getChild().?; + const scrolled_window = gobject.ext.cast(gtk.ScrolledWindow, child).?; + scrolled_window.setChild(if (priv.surface) |s| s.as(gtk.Widget) else null); + + // Unbind old config binding if it exists + if (priv.config_binding) |binding| { + binding.as(gobject.Object).unref(); + priv.config_binding = null; + } + + // Bind config from surface to our config property + if (priv.surface) |surface| { + priv.config_binding = surface.as(gobject.Object).bindProperty( + properties.config.name, + self.as(gobject.Object), + properties.config.name, + .{ .sync_create = true }, + ); + } + } + + 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 = 5, + .name = "surface-scrolled-window", + }), + ); + + // Bindings + class.bindTemplateCallback("scrollbar_policy", &closureScrollbarPolicy); + class.bindTemplateCallback("notify_surface", &propSurface); + + // Properties + gobject.ext.registerProperties(class, &.{ + properties.config.impl, + properties.surface.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/surface_title_dialog.zig b/src/apprt/gtk/class/surface_title_dialog.zig index de36f3090..6d3bf33de 100644 --- a/src/apprt/gtk/class/surface_title_dialog.zig +++ b/src/apprt/gtk/class/surface_title_dialog.zig @@ -136,7 +136,7 @@ pub const SurfaceTitleDialog = extern struct { fn finalize(self: *Self) callconv(.c) void { const priv = self.private(); if (priv.initial_value) |v| { - glib.free(@constCast(@ptrCast(v))); + glib.free(@ptrCast(@constCast(v))); priv.initial_value = null; } diff --git a/src/apprt/gtk/class/tab.zig b/src/apprt/gtk/class/tab.zig index d8f9b97f8..c9928be8b 100644 --- a/src/apprt/gtk/class/tab.zig +++ b/src/apprt/gtk/class/tab.zig @@ -206,7 +206,7 @@ pub const Tab = extern struct { .init("ring-bell", actionRingBell, null), }; - ext.actions.addAsGroup(Self, self, "tab", &actions); + _ = ext.actions.addAsGroup(Self, self, "tab", &actions); } //--------------------------------------------------------------- @@ -270,11 +270,11 @@ pub const Tab = extern struct { fn finalize(self: *Self) callconv(.c) void { const priv = self.private(); if (priv.tooltip) |v| { - glib.free(@constCast(@ptrCast(v))); + glib.free(@ptrCast(@constCast(v))); priv.tooltip = null; } if (priv.title) |v| { - glib.free(@constCast(@ptrCast(v))); + glib.free(@ptrCast(@constCast(v))); priv.title = null; } @@ -389,8 +389,14 @@ pub const Tab = extern struct { // the terminal title if it exists, otherwise a default string. const plain = plain: { const default = "Ghostty"; + const config_title: ?[*:0]const u8 = title: { + const config = config_ orelse break :title null; + break :title config.get().title orelse null; + }; + const plain = override_ orelse terminal_ orelse + config_title orelse break :plain default; break :plain std.mem.span(plain); }; @@ -405,22 +411,21 @@ pub const Tab = extern struct { }; // Use an allocator to build up our string as we write it. - var buf: std.ArrayList(u8) = .init(Application.default().allocator()); + var buf: std.Io.Writer.Allocating = .init(Application.default().allocator()); defer buf.deinit(); - const writer = buf.writer(); // If our bell is ringing, then we prefix the bell icon to the title. if (bell_ringing and config.@"bell-features".title) { - writer.writeAll("🔔 ") catch {}; + buf.writer.writeAll("🔔 ") catch {}; } // If we're zoomed, prefix with the magnifying glass emoji. if (zoomed) { - writer.writeAll("🔍 ") catch {}; + buf.writer.writeAll("🔍 ") catch {}; } - writer.writeAll(plain) catch return glib.ext.dupeZ(u8, plain); - return glib.ext.dupeZ(u8, buf.items); + buf.writer.writeAll(plain) catch return glib.ext.dupeZ(u8, plain); + return glib.ext.dupeZ(u8, buf.written()); } const C = Common(Self, Private); diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index df6ea647f..4febebfc6 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -697,6 +697,19 @@ pub const Window = extern struct { var it = tree.iterator(); while (it.next()) |entry| { const surface = entry.view; + // Before adding any new signal handlers, disconnect any that we may + // have added before. Otherwise we may get multiple handlers for the + // same signal. + _ = gobject.signalHandlersDisconnectMatched( + surface.as(gobject.Object), + .{ .data = true }, + 0, + 0, + null, + null, + self, + ); + _ = Surface.signals.@"present-request".connect( surface, *Self, @@ -1002,6 +1015,15 @@ pub const Window = extern struct { _: *gobject.ParamSpec, self: *Self, ) callconv(.c) void { + // Hide quick-terminal if set to autohide + if (self.isQuickTerminal()) { + if (self.getConfig()) |cfg| { + if (cfg.get().@"quick-terminal-autohide" and self.as(gtk.Window).isActive() == 0) { + self.toggleVisibility(); + } + } + } + // Don't change urgency if we're not the active window. if (self.as(gtk.Window).isActive() == 0) return; @@ -1489,6 +1511,13 @@ pub const Window = extern struct { const priv = self.private(); if (priv.tab_view.getNPages() == 0) { // If we have no pages left then we want to close window. + + // If the tab overview is open, then we don't close the window + // because its a rather abrupt experience. This also fixes an + // issue where dragging out the last tab in the tab overview + // won't cause Ghostty to exit. + if (priv.tab_overview.getOpen() != 0) return; + self.as(gtk.Window).close(); } } @@ -1565,6 +1594,9 @@ pub const Window = extern struct { // Grab focus surface.grabFocus(); + + // Bring the window to the front. + self.as(gtk.Window).present(); } fn surfaceToggleFullscreen( diff --git a/src/apprt/gtk/ext/actions.zig b/src/apprt/gtk/ext/actions.zig index 8499e7de8..344c08e05 100644 --- a/src/apprt/gtk/ext/actions.zig +++ b/src/apprt/gtk/ext/actions.zig @@ -40,14 +40,21 @@ test "gActionNameIsValid" { /// Function to create a structure for describing an action. pub fn Action(comptime T: type) type { return struct { + const Self = @This(); pub const Callback = *const fn (*gio.SimpleAction, ?*glib.Variant, *T) callconv(.c) void; name: [:0]const u8, callback: Callback, parameter_type: ?*const glib.VariantType, + state: ?*glib.Variant = null, - /// Function to initialize a new action so that we can comptime check the name. - pub fn init(comptime name: [:0]const u8, callback: Callback, parameter_type: ?*const glib.VariantType) @This() { + /// Function to initialize a new action so that we can comptime check + /// the name. + pub fn init( + comptime name: [:0]const u8, + callback: Callback, + parameter_type: ?*const glib.VariantType, + ) Self { comptime assert(gActionNameIsValid(name)); return .{ @@ -56,6 +63,23 @@ pub fn Action(comptime T: type) type { .parameter_type = parameter_type, }; } + + /// Function to initialize a new stateful action so that we can comptime + /// check the name. + pub fn initStateful( + comptime name: [:0]const u8, + callback: Callback, + parameter_type: ?*const glib.VariantType, + state: *glib.Variant, + ) Self { + comptime assert(gActionNameIsValid(name)); + return .{ + .name = name, + .callback = callback, + .parameter_type = parameter_type, + .state = state, + }; + } }; } @@ -68,10 +92,19 @@ pub fn add(comptime T: type, self: *T, actions: []const Action(T)) void { pub fn addToMap(comptime T: type, self: *T, map: *gio.ActionMap, actions: []const Action(T)) void { for (actions) |entry| { assert(gActionNameIsValid(entry.name)); - const action = gio.SimpleAction.new( - entry.name, - entry.parameter_type, - ); + const action = action: { + if (entry.state) |state| { + break :action gio.SimpleAction.newStateful( + entry.name, + entry.parameter_type, + state, + ); + } + break :action gio.SimpleAction.new( + entry.name, + entry.parameter_type, + ); + }; defer action.unref(); _ = gio.SimpleAction.signals.activate.connect( action, @@ -85,7 +118,7 @@ pub fn addToMap(comptime T: type, self: *T, map: *gio.ActionMap, actions: []cons } /// Add actions to a widget that doesn't implement ActionGroup directly. -pub fn addAsGroup(comptime T: type, self: *T, comptime name: [:0]const u8, actions: []const Action(T)) void { +pub fn addAsGroup(comptime T: type, self: *T, comptime name: [:0]const u8, actions: []const Action(T)) *gio.SimpleActionGroup { comptime assert(gActionNameIsValid(name)); // Collect our actions into a group since we're just a plain widget that @@ -99,6 +132,8 @@ pub fn addAsGroup(comptime T: type, self: *T, comptime name: [:0]const u8, actio name, group.as(gio.ActionGroup), ); + + return group; } test "adding actions to an object" { @@ -138,7 +173,7 @@ test "adding actions to an object" { .init("test", callbacks.callback, i32_variant_type), }; - addAsGroup(gtk.Box, box, "test", &actions); + _ = addAsGroup(gtk.Box, box, "test", &actions); } const expected = std.crypto.random.intRangeAtMost(i32, 1, std.math.maxInt(u31)); diff --git a/src/apprt/gtk/gtk_version.zig b/src/apprt/gtk/gtk_version.zig index 6f3d733a5..71edb076d 100644 --- a/src/apprt/gtk/gtk_version.zig +++ b/src/apprt/gtk/gtk_version.zig @@ -26,7 +26,7 @@ pub fn getRuntimeVersion() std.SemanticVersion { } pub fn logVersion() void { - log.info("GTK version build={} runtime={}", .{ + log.info("GTK version build={f} runtime={f}", .{ comptime_version, getRuntimeVersion(), }); diff --git a/src/apprt/gtk/ipc/DBus.zig b/src/apprt/gtk/ipc/DBus.zig index d14d86ce6..fa4a6723e 100644 --- a/src/apprt/gtk/ipc/DBus.zig +++ b/src/apprt/gtk/ipc/DBus.zig @@ -29,7 +29,10 @@ payload_builder: *glib.VariantBuilder, parameters_builder: *glib.VariantBuilder, /// Initialize the helper. -pub fn init(alloc: Allocator, target: apprt.ipc.Target, action: [:0]const u8) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!Self { +pub fn init(alloc: Allocator, target: apprt.ipc.Target, action: [:0]const u8) (Allocator.Error || std.Io.Writer.Error || apprt.ipc.Errors)!Self { + var buf: [256]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&buf); + const stderr = &stderr_writer.interface; // Get the appropriate bus name and object path for contacting the // Ghostty instance we're interested in. @@ -37,7 +40,7 @@ pub fn init(alloc: Allocator, target: apprt.ipc.Target, action: [:0]const u8) (A .class => |class| result: { // Force the usage of the class specified on the CLI to determine the // bus name and object path. - const object_path = try std.fmt.allocPrintZ(alloc, "/{s}", .{class}); + const object_path = try std.fmt.allocPrintSentinel(alloc, "/{s}", .{class}, 0); std.mem.replaceScalar(u8, object_path, '.', '/'); std.mem.replaceScalar(u8, object_path, '-', '_'); @@ -54,14 +57,14 @@ pub fn init(alloc: Allocator, target: apprt.ipc.Target, action: [:0]const u8) (A } if (gio.Application.idIsValid(bus_name.ptr) == 0) { - const stderr = std.io.getStdErr().writer(); try stderr.print("D-Bus bus name is not valid: {s}\n", .{bus_name}); + try stderr.flush(); return error.IPCFailed; } if (glib.Variant.isObjectPath(object_path.ptr) == 0) { - const stderr = std.io.getStdErr().writer(); try stderr.print("D-Bus object path is not valid: {s}\n", .{object_path}); + try stderr.flush(); return error.IPCFailed; } @@ -72,17 +75,17 @@ pub fn init(alloc: Allocator, target: apprt.ipc.Target, action: [:0]const u8) (A const dbus_ = gio.busGetSync(.session, null, &err_); if (err_) |err| { - const stderr = std.io.getStdErr().writer(); try stderr.print( "Unable to establish connection to D-Bus session bus: {s}\n", .{err.f_message orelse "(unknown)"}, ); + try stderr.flush(); return error.IPCFailed; } break :dbus dbus_ orelse { - const stderr = std.io.getStdErr().writer(); try stderr.print("gio.busGetSync returned null\n", .{}); + try stderr.flush(); return error.IPCFailed; }; }; @@ -128,7 +131,11 @@ pub fn addParameter(self: *Self, variant: *glib.Variant) void { /// Send the IPC to the remote Ghostty. Once it completes, nothing further /// should be done with this object other than call `deinit`. -pub fn send(self: *Self) (std.posix.WriteError || apprt.ipc.Errors)!void { +pub fn send(self: *Self) (std.Io.Writer.Error || apprt.ipc.Errors)!void { + var buf: [256]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&buf); + const stderr = &stderr_writer.interface; + // finish building the parameters const parameters = self.parameters_builder.end(); @@ -167,11 +174,11 @@ pub fn send(self: *Self) (std.posix.WriteError || apprt.ipc.Errors)!void { defer if (result_) |result| result.unref(); if (err_) |err| { - const stderr = std.io.getStdErr().writer(); try stderr.print( "D-Bus method call returned an error err={s}\n", .{err.f_message orelse "(unknown)"}, ); + try stderr.flush(); return error.IPCFailed; } } diff --git a/src/apprt/gtk/ipc/new_window.zig b/src/apprt/gtk/ipc/new_window.zig index 55e2e0e01..19c46e3aa 100644 --- a/src/apprt/gtk/ipc/new_window.zig +++ b/src/apprt/gtk/ipc/new_window.zig @@ -20,7 +20,7 @@ const DBus = @import("DBus.zig"); // ``` // gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["echo" "hello"]>]' [] // ``` -pub fn newWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Action.NewWindow) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool { +pub fn newWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Action.NewWindow) (Allocator.Error || std.Io.Writer.Error || apprt.ipc.Errors)!bool { var dbus = try DBus.init( alloc, target, diff --git a/src/apprt/gtk/key.zig b/src/apprt/gtk/key.zig index a00b0312e..bf0f0e2f6 100644 --- a/src/apprt/gtk/key.zig +++ b/src/apprt/gtk/key.zig @@ -12,9 +12,8 @@ const winproto = @import("winproto.zig"); pub fn accelFromTrigger( buf: []u8, trigger: input.Binding.Trigger, -) error{NoSpaceLeft}!?[:0]const u8 { - var buf_stream = std.io.fixedBufferStream(buf); - const writer = buf_stream.writer(); +) error{WriteFailed}!?[:0]const u8 { + var writer: std.Io.Writer = .fixed(buf); // Modifiers if (trigger.mods.shift) try writer.writeAll(""); @@ -23,11 +22,11 @@ pub fn accelFromTrigger( if (trigger.mods.super) try writer.writeAll(""); // Write our key - if (!try writeTriggerKey(writer, trigger)) return null; + if (!try writeTriggerKey(&writer, trigger)) return null; // We need to make the string null terminated. try writer.writeByte(0); - const slice = buf_stream.getWritten(); + const slice = writer.buffered(); return slice[0 .. slice.len - 1 :0]; } @@ -36,9 +35,8 @@ pub fn accelFromTrigger( pub fn xdgShortcutFromTrigger( buf: []u8, trigger: input.Binding.Trigger, -) error{NoSpaceLeft}!?[:0]const u8 { - var buf_stream = std.io.fixedBufferStream(buf); - const writer = buf_stream.writer(); +) error{WriteFailed}!?[:0]const u8 { + var writer: std.Io.Writer = .fixed(buf); // Modifiers if (trigger.mods.shift) try writer.writeAll("SHIFT+"); @@ -52,15 +50,18 @@ pub fn xdgShortcutFromTrigger( // to *X11's* keysyms (which I assume is a subset of libxkbcommon's). // I haven't been able to any evidence to back up that assumption but // this works for now - if (!try writeTriggerKey(writer, trigger)) return null; + if (!try writeTriggerKey(&writer, trigger)) return null; // We need to make the string null terminated. try writer.writeByte(0); - const slice = buf_stream.getWritten(); + const slice = writer.buffered(); return slice[0 .. slice.len - 1 :0]; } -fn writeTriggerKey(writer: anytype, trigger: input.Binding.Trigger) error{NoSpaceLeft}!bool { +fn writeTriggerKey( + writer: *std.Io.Writer, + trigger: input.Binding.Trigger, +) error{WriteFailed}!bool { switch (trigger.key) { .physical => |k| { const keyval = keyvalFromKey(k) orelse return false; diff --git a/src/apprt/gtk/ui/1.2/surface.blp b/src/apprt/gtk/ui/1.2/surface.blp index ad971e991..0596bf15d 100644 --- a/src/apprt/gtk/ui/1.2/surface.blp +++ b/src/apprt/gtk/ui/1.2/surface.blp @@ -169,12 +169,12 @@ template $GhosttySurface: Adw.Bin { "surface", ] - notify::bell-ringing => $notify_bell_ringing(); notify::config => $notify_config(); notify::error => $notify_error(); notify::mouse-hover-url => $notify_mouse_hover_url(); notify::mouse-hidden => $notify_mouse_hidden(); notify::mouse-shape => $notify_mouse_shape(); + notify::vadjustment => $notify_vadjustment(); // Some history: we used to use a Stack here and swap between the // terminal and error pages as needed. But a Stack doesn't play nice // with our SplitTree and Gtk.Paned usage[^1]. Replacing this with @@ -204,6 +204,11 @@ menu context_menu_model { label: _("Paste"); action: "win.paste"; } + + item { + label: _("Notify on Next Command Finish"); + action: "surface.notify-on-next-command-finish"; + } } section { diff --git a/src/apprt/gtk/ui/1.5/surface-scrolled-window.blp b/src/apprt/gtk/ui/1.5/surface-scrolled-window.blp new file mode 100644 index 000000000..722c4427b --- /dev/null +++ b/src/apprt/gtk/ui/1.5/surface-scrolled-window.blp @@ -0,0 +1,11 @@ +using Gtk 4.0; +using Adw 1; + +template $GhostttySurfaceScrolledWindow: Adw.Bin { + notify::surface => $notify_surface(); + + Gtk.ScrolledWindow { + hscrollbar-policy: never; + vscrollbar-policy: bind $scrollbar_policy(template.config) as ; + } +} diff --git a/src/apprt/gtk/ui/1.5/surface-title-dialog.blp b/src/apprt/gtk/ui/1.5/surface-title-dialog.blp index 24ae26f37..90d9f9c0b 100644 --- a/src/apprt/gtk/ui/1.5/surface-title-dialog.blp +++ b/src/apprt/gtk/ui/1.5/surface-title-dialog.blp @@ -6,11 +6,14 @@ template $GhosttySurfaceTitleDialog: Adw.AlertDialog { body: _("Leave blank to restore the default title."); responses [ - cancel: _("Cancel") suggested, - ok: _("OK") destructive, + cancel: _("Cancel"), + ok: _("OK") suggested, ] + default-response: "ok"; focus-widget: entry; - extra-child: Entry entry {}; + extra-child: Entry entry { + activates-default: true; + }; } diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index e4effe128..b71bf1e6e 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -96,6 +96,17 @@ pub const Message = union(enum) { /// Report the progress of an action using a GUI element progress_report: terminal.osc.Command.ProgressReport, + /// A command has started in the shell, start a timer. + start_command, + + /// A command has finished in the shell, stop the timer and send out + /// notifications as appropriate. The optional u8 is the exit code + /// of the command. + stop_command: ?u8, + + /// The scrollbar state changed for the surface. + scrollbar: terminal.Scrollbar, + pub const ReportTitleStyle = enum { csi_21_t, diff --git a/src/benchmark/CodepointWidth.zig b/src/benchmark/CodepointWidth.zig index a6945c8f1..552df8d1f 100644 --- a/src/benchmark/CodepointWidth.zig +++ b/src/benchmark/CodepointWidth.zig @@ -107,12 +107,15 @@ fn stepWcwidth(ptr: *anyopaque) Benchmark.Error!void { const self: *CodepointWidth = @ptrCast(@alignCast(ptr)); const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); + var read_buf: [4096]u8 = undefined; + var f_reader = f.reader(&read_buf); + var r = &f_reader.interface; + var d: UTF8Decoder = .{}; var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached @@ -131,12 +134,15 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void { const self: *CodepointWidth = @ptrCast(@alignCast(ptr)); const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); + var read_buf: [4096]u8 = undefined; + var f_reader = f.reader(&read_buf); + var r = &f_reader.interface; + var d: UTF8Decoder = .{}; var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached @@ -160,12 +166,15 @@ fn stepSimd(ptr: *anyopaque) Benchmark.Error!void { const self: *CodepointWidth = @ptrCast(@alignCast(ptr)); const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); + var read_buf: [4096]u8 = undefined; + var f_reader = f.reader(&read_buf); + var r = &f_reader.interface; + var d: UTF8Decoder = .{}; var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached diff --git a/src/benchmark/GraphemeBreak.zig b/src/benchmark/GraphemeBreak.zig index 8516f86fa..a1b3380f0 100644 --- a/src/benchmark/GraphemeBreak.zig +++ b/src/benchmark/GraphemeBreak.zig @@ -90,12 +90,15 @@ fn stepNoop(ptr: *anyopaque) Benchmark.Error!void { const self: *GraphemeBreak = @ptrCast(@alignCast(ptr)); const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); + var read_buf: [4096]u8 = undefined; + var f_reader = f.reader(&read_buf); + var r = &f_reader.interface; + var d: UTF8Decoder = .{}; var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached @@ -110,14 +113,17 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void { const self: *GraphemeBreak = @ptrCast(@alignCast(ptr)); const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); + var read_buf: [4096]u8 = undefined; + var f_reader = f.reader(&read_buf); + var r = &f_reader.interface; + var d: UTF8Decoder = .{}; var state: unicode.GraphemeBreakState = .{}; var cp1: u21 = 0; var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached diff --git a/src/benchmark/IsSymbol.zig b/src/benchmark/IsSymbol.zig index ce635626a..c4667b333 100644 --- a/src/benchmark/IsSymbol.zig +++ b/src/benchmark/IsSymbol.zig @@ -10,7 +10,7 @@ const Allocator = std.mem.Allocator; const Benchmark = @import("Benchmark.zig"); const options = @import("options.zig"); const UTF8Decoder = @import("../terminal/UTF8Decoder.zig"); -const symbols = @import("../unicode/symbols_ziglyph.zig"); +const uucode = @import("uucode"); const symbols_table = @import("../unicode/symbols_table.zig").table; const log = std.log.scoped(.@"is-symbol-bench"); @@ -22,7 +22,7 @@ data_f: ?std.fs.File = null, pub const Options = struct { /// Which test to run. - mode: Mode = .ziglyph, + mode: Mode = .uucode, /// The data to read as a filepath. If this is "-" then /// we will read stdin. If this is unset, then we will @@ -33,8 +33,8 @@ pub const Options = struct { }; pub const Mode = enum { - /// "Naive" ziglyph implementation. - ziglyph, + /// uucode implementation + uucode, /// Ghostty's table-based approach. table, @@ -58,7 +58,7 @@ pub fn destroy(self: *IsSymbol, alloc: Allocator) void { pub fn benchmark(self: *IsSymbol) Benchmark { return .init(self, .{ .stepFn = switch (self.opts.mode) { - .ziglyph => stepZiglyph, + .uucode => stepUucode, .table => stepTable, }, .setupFn = setup, @@ -86,16 +86,19 @@ fn teardown(ptr: *anyopaque) void { } } -fn stepZiglyph(ptr: *anyopaque) Benchmark.Error!void { +fn stepUucode(ptr: *anyopaque) Benchmark.Error!void { const self: *IsSymbol = @ptrCast(@alignCast(ptr)); const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); + var read_buf: [4096]u8 = undefined; + var f_reader = f.reader(&read_buf); + var r = &f_reader.interface; + var d: UTF8Decoder = .{}; var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached @@ -104,7 +107,7 @@ fn stepZiglyph(ptr: *anyopaque) Benchmark.Error!void { const cp_, const consumed = d.next(c); assert(consumed); if (cp_) |cp| { - std.mem.doNotOptimizeAway(symbols.isSymbol(cp)); + std.mem.doNotOptimizeAway(uucode.get(.is_symbol, cp)); } } } @@ -114,12 +117,15 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void { const self: *IsSymbol = @ptrCast(@alignCast(ptr)); const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); + var read_buf: [4096]u8 = undefined; + var f_reader = f.reader(&read_buf); + var r = &f_reader.interface; + var d: UTF8Decoder = .{}; var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached diff --git a/src/benchmark/TerminalParser.zig b/src/benchmark/TerminalParser.zig index 002af4831..f13b44552 100644 --- a/src/benchmark/TerminalParser.zig +++ b/src/benchmark/TerminalParser.zig @@ -75,14 +75,16 @@ fn step(ptr: *anyopaque) Benchmark.Error!void { // the benchmark results and... I know writing this that we // aren't currently IO bound. const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); + var read_buf: [4096]u8 = undefined; + var f_reader = f.reader(&read_buf); + var r = &f_reader.interface; var p: terminalpkg.Parser = .init(); var buf: [4096]u8 = undefined; while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached diff --git a/src/benchmark/TerminalStream.zig b/src/benchmark/TerminalStream.zig index 28a95226c..ecce509f3 100644 --- a/src/benchmark/TerminalStream.zig +++ b/src/benchmark/TerminalStream.zig @@ -113,17 +113,19 @@ fn step(ptr: *anyopaque) Benchmark.Error!void { // the benchmark results and... I know writing this that we // aren't currently IO bound. const f = self.data_f orelse return; - var r = std.io.bufferedReader(f.reader()); + + var read_buf: [4096]u8 = undefined; + var f_reader = f.reader(&read_buf); + const r = &f_reader.interface; var buf: [4096]u8 = undefined; while (true) { - const n = r.read(&buf) catch |err| { - log.warn("error reading data file err={}", .{err}); + const n = r.readSliceShort(&buf) catch { + log.warn("error reading data file err={?}", .{f_reader.err}); return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached - const chunk = buf[0..n]; - self.stream.nextSlice(chunk) catch |err| { + self.stream.nextSlice(buf[0..n]) catch |err| { log.warn("error processing data file chunk err={}", .{err}); return error.BenchmarkFailed; }; diff --git a/src/benchmark/options.zig b/src/benchmark/options.zig index 867be6afc..049e80f48 100644 --- a/src/benchmark/options.zig +++ b/src/benchmark/options.zig @@ -10,7 +10,7 @@ pub fn dataFile(path_: ?[]const u8) !?std.fs.File { const path = path_ orelse return null; // Stdin - if (std.mem.eql(u8, path, "-")) return std.io.getStdIn(); + if (std.mem.eql(u8, path, "-")) return .stdin(); // Normal file const file = try std.fs.cwd().openFile(path, .{}); diff --git a/src/build/Config.zig b/src/build/Config.zig index 474674d3a..e88213d71 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -9,24 +9,17 @@ const ApprtRuntime = @import("../apprt/runtime.zig").Runtime; const FontBackend = @import("../font/backend.zig").Backend; const RendererBackend = @import("../renderer/backend.zig").Backend; const TerminalBuildOptions = @import("../terminal/build_options.zig").Options; -const XCFramework = @import("GhosttyXCFramework.zig"); +const XCFrameworkTarget = @import("xcframework.zig").Target; const WasmTarget = @import("../os/wasm/target.zig").Target; const expandPath = @import("../os/path.zig").expand; const gtk = @import("gtk.zig"); const GitVersion = @import("GitVersion.zig"); -/// The version of the next release. -/// -/// TODO: When Zig 0.14 is released, derive this from build.zig.zon directly. -/// Until then this MUST match build.zig.zon and should always be the -/// _next_ version to release. -const app_version: std.SemanticVersion = .{ .major = 1, .minor = 2, .patch = 1 }; - /// Standard build configuration options. optimize: std.builtin.OptimizeMode, target: std.Build.ResolvedTarget, -xcframework_target: XCFramework.Target = .universal, +xcframework_target: XCFrameworkTarget = .universal, wasm_target: WasmTarget, /// Comptime interfaces @@ -62,6 +55,7 @@ emit_macos_app: bool = false, emit_terminfo: bool = false, emit_termcap: bool = false, emit_test_exe: bool = false, +emit_themes: bool = false, emit_xcframework: bool = false, emit_webdata: bool = false, emit_unicode_table_gen: bool = false, @@ -69,7 +63,7 @@ emit_unicode_table_gen: bool = false, /// Environmental properties env: std.process.EnvMap, -pub fn init(b: *std.Build) !Config { +pub fn init(b: *std.Build, appVersion: []const u8) !Config { // Setup our standard Zig target and optimize options, i.e. // `-Doptimize` and `-Dtarget`. const optimize = b.standardOptimizeOption(.{}); @@ -121,7 +115,7 @@ pub fn init(b: *std.Build) !Config { //--------------------------------------------------------------- // Target-specific properties config.xcframework_target = b.option( - XCFramework.Target, + XCFrameworkTarget, "xcframework-target", "The target for the xcframework.", ) orelse .universal; @@ -179,7 +173,13 @@ pub fn init(b: *std.Build) !Config { bool, "simd", "Build with SIMD-accelerated code paths. Results in significant performance improvements.", - ) orelse true; + ) orelse simd: { + // We can't build our SIMD dependencies for Wasm. Note that we may + // still use SIMD features in the Wasm-builds. + if (target.result.cpu.arch.isWasm()) break :simd false; + + break :simd true; + }; config.wayland = b.option( bool, @@ -217,6 +217,7 @@ pub fn init(b: *std.Build) !Config { // If an explicit version is given, we always use it. try std.SemanticVersion.parse(v) else version: { + const app_version = try std.SemanticVersion.parse(appVersion); // 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. @@ -374,6 +375,12 @@ pub fn init(b: *std.Build) !Config { .ReleaseSafe, .ReleaseFast, .ReleaseSmall => false, }; + config.emit_themes = b.option( + bool, + "emit-themes", + "Install bundled iTerm2-Color-Schemes Ghostty themes", + ) orelse true; + config.emit_webdata = b.option( bool, "emit-webdata", @@ -477,7 +484,7 @@ pub fn addOptions(self: *const Config, step: *std.Build.Step.Options) !void { step.addOption(std.SemanticVersion, "app_version", self.version); step.addOption([:0]const u8, "app_version_string", try std.fmt.bufPrintZ( &buf, - "{}", + "{f}", .{self.version}, )); step.addOption( @@ -498,6 +505,7 @@ pub fn terminalOptions(self: *const Config) TerminalBuildOptions { .artifact = .ghostty, .simd = self.simd, .oniguruma = true, + .c_abi = false, .slow_runtime_safety = switch (self.optimize) { .Debug => true, .ReleaseSafe, diff --git a/src/build/GhosttyBench.zig b/src/build/GhosttyBench.zig index 5859a8bcf..c9cd5dd33 100644 --- a/src/build/GhosttyBench.zig +++ b/src/build/GhosttyBench.zig @@ -11,8 +11,8 @@ pub fn init( b: *std.Build, deps: *const SharedDeps, ) !GhosttyBench { - var steps = std.ArrayList(*std.Build.Step.Compile).init(b.allocator); - errdefer steps.deinit(); + var steps: std.ArrayList(*std.Build.Step.Compile) = .empty; + errdefer steps.deinit(b.allocator); // Our synthetic data generator { @@ -28,7 +28,7 @@ pub fn init( }); exe.linkLibC(); _ = try deps.add(exe); - try steps.append(exe); + try steps.append(b.allocator, exe); } // Our benchmarking application. @@ -44,7 +44,7 @@ pub fn init( }); exe.linkLibC(); _ = try deps.add(exe); - try steps.append(exe); + try steps.append(b.allocator, exe); } return .{ .steps = steps.items }; diff --git a/src/build/GhosttyDist.zig b/src/build/GhosttyDist.zig index d889f2350..092322689 100644 --- a/src/build/GhosttyDist.zig +++ b/src/build/GhosttyDist.zig @@ -3,6 +3,7 @@ const GhosttyDist = @This(); const std = @import("std"); const Config = @import("Config.zig"); const SharedDeps = @import("SharedDeps.zig"); +const GhosttyFrameData = @import("GhosttyFrameData.zig"); /// The final source tarball. archive: std.Build.LazyPath, @@ -25,6 +26,10 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { try resources.append(alloc, gtk.resources_c); try resources.append(alloc, gtk.resources_h); } + { + const framedata = GhosttyFrameData.distResources(b); + try resources.append(alloc, framedata.framedata); + } // git archive to create the final tarball. "git archive" is the // easiest way I can find to create a tarball that ignores stuff @@ -38,10 +43,10 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { // embed the Ghostty version in the tarball { - const version = b.addWriteFiles().add("VERSION", b.fmt("{}", .{cfg.version})); + const version = b.addWriteFiles().add("VERSION", b.fmt("{f}", .{cfg.version})); // --add-file uses the most recent --prefix to determine the path // in the archive to copy the file (the directory only). - git_archive.addArg(b.fmt("--prefix=ghostty-{}/", .{ + git_archive.addArg(b.fmt("--prefix=ghostty-{f}/", .{ cfg.version, })); git_archive.addPrefixedFileArg("--add-file=", version); @@ -60,7 +65,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { // --add-file uses the most recent --prefix to determine the path // in the archive to copy the file (the directory only). - git_archive.addArg(b.fmt("--prefix=ghostty-{}/{s}/", .{ + git_archive.addArg(b.fmt("--prefix=ghostty-{f}/{s}/", .{ cfg.version, std.fs.path.dirname(resource.dist).?, })); @@ -72,11 +77,11 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { // This is important. Standard source tarballs extract into // a directory named `project-version`. This is expected by // standard tooling such as debhelper and rpmbuild. - b.fmt("--prefix=ghostty-{}/", .{cfg.version}), + b.fmt("--prefix=ghostty-{f}/", .{cfg.version}), "-o", }); const output = git_archive.addOutputFileArg(b.fmt( - "ghostty-{}.tar.gz", + "ghostty-{f}.tar.gz", .{cfg.version}, )); git_archive.addArg("HEAD"); @@ -84,7 +89,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { // The install step to put the dist into the build directory. const install = b.addInstallFile( output, - b.fmt("dist/ghostty-{}.tar.gz", .{cfg.version}), + b.fmt("dist/ghostty-{f}.tar.gz", .{cfg.version}), ); // The check step to ensure the archive works. @@ -96,7 +101,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { // i.e. this is way `build.zig` is. const extract_dir = check .addOutputDirectoryArg("ghostty") - .path(b, b.fmt("ghostty-{}", .{cfg.version})); + .path(b, b.fmt("ghostty-{f}", .{cfg.version})); // Check that tests pass within the extracted directory. This isn't // a fully hermetic test because we're sharing the Zig cache. In diff --git a/src/build/GhosttyDocs.zig b/src/build/GhosttyDocs.zig index b95b56f74..cd75fc061 100644 --- a/src/build/GhosttyDocs.zig +++ b/src/build/GhosttyDocs.zig @@ -12,8 +12,8 @@ pub fn init( b: *std.Build, deps: *const SharedDeps, ) !GhosttyDocs { - var steps = std.ArrayList(*std.Build.Step).init(b.allocator); - errdefer steps.deinit(); + var steps: std.ArrayList(*std.Build.Step) = .empty; + errdefer steps.deinit(b.allocator); const manpages = [_]struct { name: []const u8, @@ -52,7 +52,7 @@ pub fn init( const generate_markdown_step = b.addRunArtifact(generate_markdown); const markdown_output = generate_markdown_step.captureStdOut(); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( markdown_output, "share/ghostty/doc/" ++ manpage.name ++ "." ++ manpage.section ++ ".md", ).step); @@ -67,7 +67,7 @@ pub fn init( }); generate_html.addFileArg(markdown_output); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( generate_html.captureStdOut(), "share/ghostty/doc/" ++ manpage.name ++ "." ++ manpage.section ++ ".html", ).step); @@ -82,7 +82,7 @@ pub fn init( }); generate_manpage.addFileArg(markdown_output); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( generate_manpage.captureStdOut(), "share/man/man" ++ manpage.section ++ "/" ++ manpage.name ++ "." ++ manpage.section, ).step); diff --git a/src/build/GhosttyExe.zig b/src/build/GhosttyExe.zig index 083aecdb5..3e63b6026 100644 --- a/src/build/GhosttyExe.zig +++ b/src/build/GhosttyExe.zig @@ -21,6 +21,8 @@ pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty .omit_frame_pointer = cfg.strip, .unwind_tables = if (cfg.strip) .none else .sync, }), + // Crashes on x86_64 self-hosted on 0.15.1 + .use_llvm = true, }); const install_step = b.addInstallArtifact(exe, .{}); diff --git a/src/build/GhosttyFrameData.zig b/src/build/GhosttyFrameData.zig index 1644388bc..7193162bd 100644 --- a/src/build/GhosttyFrameData.zig +++ b/src/build/GhosttyFrameData.zig @@ -5,35 +5,25 @@ const GhosttyFrameData = @This(); const std = @import("std"); const Config = @import("Config.zig"); const SharedDeps = @import("SharedDeps.zig"); - -/// The exe. -exe: *std.Build.Step.Compile, +const DistResource = @import("GhosttyDist.zig").Resource; /// The output path for the compressed framedata zig file output: std.Build.LazyPath, pub fn init(b: *std.Build) !GhosttyFrameData { - const exe = b.addExecutable(.{ - .name = "framegen", - .root_module = b.createModule(.{ - .root_source_file = b.path("src/build/framegen/main.zig"), - .target = b.graph.host, - .strip = false, - .omit_frame_pointer = false, - .unwind_tables = .sync, - }), - }); + const dist = distResources(b); - const run = b.addRunArtifact(exe); - // Both the compressed framedata and the Zig source file - // have to be put in the same directory, since the compressed file - // has to be within the source file's include path. - const dir = run.addOutputDirectoryArg("framedata"); + // Generate the Zig source file that embeds the compressed data + const wf = b.addWriteFiles(); + _ = wf.addCopyFile(dist.framedata.path(b), "framedata.compressed"); + const zig_file = wf.add("framedata.zig", + \\//! This file is auto-generated. Do not edit. + \\ + \\pub const compressed = @embedFile("framedata.compressed"); + \\ + ); - return .{ - .exe = exe, - .output = dir.path(b, "framedata.zig"), - }; + return .{ .output = zig_file }; } /// Add the "framedata" import. @@ -43,3 +33,45 @@ pub fn addImport(self: *const GhosttyFrameData, step: *std.Build.Step.Compile) v .root_source_file = self.output, }); } + +/// Creates the framedata resources that can be prebuilt for our dist build. +pub fn distResources(b: *std.Build) struct { + framedata: DistResource, +} { + const exe = b.addExecutable(.{ + .name = "framegen", + .root_module = b.createModule(.{ + .target = b.graph.host, + }), + }); + exe.addCSourceFile(.{ + .file = b.path("src/build/framegen/main.c"), + .flags = &.{}, + }); + exe.linkLibC(); + + if (b.systemIntegrationOption("zlib", .{})) { + exe.linkSystemLibrary2("zlib", .{ + .preferred_link_mode = .dynamic, + .search_strategy = .mode_first, + }); + } else { + if (b.lazyDependency("zlib", .{ + .target = b.graph.host, + .optimize = .ReleaseFast, + })) |zlib_dep| { + exe.linkLibrary(zlib_dep.artifact("z")); + } + } + + const run = b.addRunArtifact(exe); + run.addDirectoryArg(b.path("src/build/framegen/frames")); + const compressed_file = run.addOutputFileArg("framedata.compressed"); + + return .{ + .framedata = .{ + .dist = "src/build/framegen/framedata.compressed", + .generated = compressed_file, + }, + }; +} diff --git a/src/build/GhosttyI18n.zig b/src/build/GhosttyI18n.zig index b99e60426..8e31f61b3 100644 --- a/src/build/GhosttyI18n.zig +++ b/src/build/GhosttyI18n.zig @@ -18,8 +18,8 @@ update_step: *std.Build.Step, pub fn init(b: *std.Build, cfg: *const Config) !GhosttyI18n { _ = cfg; - var steps = std.ArrayList(*std.Build.Step).init(b.allocator); - defer steps.deinit(); + var steps: std.ArrayList(*std.Build.Step) = .empty; + defer steps.deinit(b.allocator); inline for (locales) |locale| { // There is no encoding suffix in the LC_MESSAGES path on FreeBSD, @@ -33,7 +33,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyI18n { const msgfmt = b.addSystemCommand(&.{ "msgfmt", "-o", "-" }); msgfmt.addFileArg(b.path("po/" ++ locale ++ ".po")); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( msgfmt.captureStdOut(), std.fmt.comptimePrint( "share/locale/{s}/LC_MESSAGES/{s}.mo", @@ -45,7 +45,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyI18n { return .{ .owner = b, .update_step = try createUpdateStep(b), - .steps = try steps.toOwnedSlice(), + .steps = try steps.toOwnedSlice(b.allocator), }; } diff --git a/src/build/GhosttyLib.zig b/src/build/GhosttyLib.zig index b244a72c5..2ac383544 100644 --- a/src/build/GhosttyLib.zig +++ b/src/build/GhosttyLib.zig @@ -28,7 +28,9 @@ pub fn initStatic( .omit_frame_pointer = deps.config.strip, .unwind_tables = if (deps.config.strip) .none else .sync, }), - .linkage = .static, + + // Fails on self-hosted x86_64 on macOS + .use_llvm = true, }); lib.linkLibC(); @@ -40,7 +42,7 @@ pub fn initStatic( // Add our dependencies. Get the list of all static deps so we can // build a combined archive if necessary. var lib_list = try deps.add(lib); - try lib_list.append(lib.getEmittedBin()); + try lib_list.append(b.allocator, lib.getEmittedBin()); if (!deps.config.target.result.os.tag.isDarwin()) return .{ .step = &lib.step, @@ -69,8 +71,9 @@ pub fn initShared( b: *std.Build, deps: *const SharedDeps, ) !GhosttyLib { - const lib = b.addSharedLibrary(.{ + const lib = b.addLibrary(.{ .name = "ghostty", + .linkage = .dynamic, .root_module = b.createModule(.{ .root_source_file = b.path("src/main_c.zig"), .target = deps.config.target, @@ -79,6 +82,9 @@ pub fn initShared( .omit_frame_pointer = deps.config.strip, .unwind_tables = if (deps.config.strip) .none else .sync, }), + + // Fails on self-hosted x86_64 + .use_llvm = true, }); _ = try deps.add(lib); diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 0029d6756..d1ab5d1ba 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -1,6 +1,7 @@ const GhosttyLibVt = @This(); const std = @import("std"); +const assert = std.debug.assert; const RunStep = std.Build.Step.Run; const Config = @import("Config.zig"); const GhosttyZig = @import("GhosttyZig.zig"); @@ -17,20 +18,51 @@ artifact: *std.Build.Step.InstallArtifact, /// The final library file output: std.Build.LazyPath, dsym: ?std.Build.LazyPath, -pkg_config: std.Build.LazyPath, +pkg_config: ?std.Build.LazyPath, + +pub fn initWasm( + b: *std.Build, + zig: *const GhosttyZig, +) !GhosttyLibVt { + const target = zig.vt.resolved_target.?; + assert(target.result.cpu.arch.isWasm()); + + const exe = b.addExecutable(.{ + .name = "ghostty-vt", + .root_module = zig.vt_c, + .version = std.SemanticVersion{ .major = 0, .minor = 1, .patch = 0 }, + }); + + // Allow exported symbols to actually be exported. + exe.rdynamic = true; + + // There is no entrypoint for this wasm module. + exe.entry = .disabled; + + return .{ + .step = &exe.step, + .artifact = b.addInstallArtifact(exe, .{}), + .output = exe.getEmittedBin(), + .dsym = null, + .pkg_config = null, + }; +} pub fn initShared( b: *std.Build, zig: *const GhosttyZig, ) !GhosttyLibVt { const target = zig.vt.resolved_target.?; - const lib = b.addSharedLibrary(.{ + const lib = b.addLibrary(.{ .name = "ghostty-vt", - .root_module = zig.vt, + .linkage = .dynamic, + .root_module = zig.vt_c, + .version = std.SemanticVersion{ .major = 0, .minor = 1, .patch = 0 }, }); - lib.installHeader( - b.path("include/ghostty/vt.h"), - "ghostty/vt.h", + lib.installHeadersDirectory( + b.path("include/ghostty"), + "ghostty", + .{ .include_extensions = &.{".h"} }, ); // Get our debug symbols @@ -79,9 +111,11 @@ pub fn install( ) void { const b = step.owner; step.dependOn(&self.artifact.step); - step.dependOn(&b.addInstallFileWithDir( - self.pkg_config, - .prefix, - "share/pkgconfig/libghostty-vt.pc", - ).step); + if (self.pkg_config) |pkg_config| { + step.dependOn(&b.addInstallFileWithDir( + pkg_config, + .prefix, + "share/pkgconfig/libghostty-vt.pc", + ).step); + } } diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig index 0db1fd418..1ac8fe2a9 100644 --- a/src/build/GhosttyResources.zig +++ b/src/build/GhosttyResources.zig @@ -10,8 +10,8 @@ const RunStep = std.Build.Step.Run; steps: []*std.Build.Step, pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { - var steps = std.ArrayList(*std.Build.Step).init(b.allocator); - errdefer steps.deinit(); + var steps: std.ArrayList(*std.Build.Step) = .empty; + errdefer steps.deinit(b.allocator); // This is the exe used to generate some build data. const build_data_exe = b.addExecutable(.{ @@ -49,7 +49,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { "share/terminfo/ghostty.terminfo", ); - try steps.append(&source_install.step); + try steps.append(b.allocator, &source_install.step); } // Windows doesn't have the binaries below. @@ -73,7 +73,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { "share/terminfo/ghostty.termcap", ); - try steps.append(&cap_install.step); + try steps.append(b.allocator, &cap_install.step); } // Compile the terminfo source into a terminfo database @@ -99,7 +99,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { .{ b.install_path, terminfo_share_dir }, )); - try steps.append(&mkdir_step.step); + try steps.append(b.allocator, &mkdir_step.step); // Use cp -R instead of Step.InstallDir because we need to preserve // symlinks in the terminfo database. Zig's InstallDir step doesn't @@ -109,7 +109,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { copy_step.addFileArg(path); copy_step.addArg(b.fmt("{s}/share", .{b.install_path})); copy_step.step.dependOn(&mkdir_step.step); - try steps.append(©_step.step); + try steps.append(b.allocator, ©_step.step); } } @@ -121,18 +121,20 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { .install_subdir = b.pathJoin(&.{ "ghostty", "shell-integration" }), .exclude_extensions = &.{".md"}, }); - try steps.append(&install_step.step); + try steps.append(b.allocator, &install_step.step); } // Themes - if (b.lazyDependency("iterm2_themes", .{})) |upstream| { - const install_step = b.addInstallDirectory(.{ - .source_dir = upstream.path(""), - .install_dir = .{ .custom = "share" }, - .install_subdir = b.pathJoin(&.{ "ghostty", "themes" }), - .exclude_extensions = &.{".md"}, - }); - try steps.append(&install_step.step); + if (cfg.emit_themes) { + if (b.lazyDependency("iterm2_themes", .{})) |upstream| { + const install_step = b.addInstallDirectory(.{ + .source_dir = upstream.path(""), + .install_dir = .{ .custom = "share" }, + .install_subdir = b.pathJoin(&.{ "ghostty", "themes" }), + .exclude_extensions = &.{".md"}, + }); + try steps.append(b.allocator, &install_step.step); + } } // Fish shell completions @@ -147,7 +149,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { .install_dir = .prefix, .install_subdir = "share/fish/vendor_completions.d", }); - try steps.append(&install_step.step); + try steps.append(b.allocator, &install_step.step); } // zsh shell completions @@ -162,7 +164,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { .install_dir = .prefix, .install_subdir = "share/zsh/site-functions", }); - try steps.append(&install_step.step); + try steps.append(b.allocator, &install_step.step); } // bash shell completions @@ -177,7 +179,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { .install_dir = .prefix, .install_subdir = "share/bash-completion/completions", }); - try steps.append(&install_step.step); + try steps.append(b.allocator, &install_step.step); } // Vim and Neovim plugin @@ -210,14 +212,14 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { .install_dir = .prefix, .install_subdir = "share/vim/vimfiles", }); - try steps.append(&vim_step.step); + try steps.append(b.allocator, &vim_step.step); const neovim_step = b.addInstallDirectory(.{ .source_dir = wf.getDirectory(), .install_dir = .prefix, .install_subdir = "share/nvim/site", }); - try steps.append(&neovim_step.step); + try steps.append(b.allocator, &neovim_step.step); } // Sublime syntax highlighting for bat cli tool @@ -225,7 +227,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { // 'ghostty.sublime-syntax' file from zig-out to the '~.config/bat/syntaxes' // directory. The syntax then needs to be mapped to the correct language in // the config file within the '~.config/bat' directory - // (ex: --map-syntax "/Users/user/.config/ghostty/config:Ghostty Config"). + // (ex: --map-syntax "/Users/user/.config/ghostty/config.ghostty:Ghostty Config"). { const run = b.addRunArtifact(build_data_exe); run.addArg("+sublime"); @@ -237,7 +239,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { .install_dir = .prefix, .install_subdir = "share/bat/syntaxes", }); - try steps.append(&install_step.step); + try steps.append(b.allocator, &install_step.step); } // App (Linux) @@ -286,16 +288,17 @@ fn addLinuxAppResources( // second element of the tuple. const Template = struct { std.Build.LazyPath, []const u8 }; const templates: []const Template = templates: { - var ts: std.ArrayList(Template) = .init(b.allocator); + var ts: std.ArrayList(Template) = .empty; + defer ts.deinit(b.allocator); // Desktop file so that we have an icon and other metadata - try ts.append(.{ + try ts.append(b.allocator, .{ b.path("dist/linux/app.desktop.in"), b.fmt("share/applications/{s}.desktop", .{app_id}), }); // Service for DBus activation. - try ts.append(.{ + try ts.append(b.allocator, .{ if (cfg.flatpak) b.path("dist/linux/dbus.service.flatpak.in") else @@ -320,7 +323,7 @@ fn addLinuxAppResources( // See the following code: // // https://github.com/flatpak/xdg-desktop-portal/blob/7d4d48cf079147c8887da17ec6c3954acd5a285c/src/xdp-utils.c#L152-L220 - if (!cfg.flatpak) try ts.append(.{ + if (!cfg.flatpak) try ts.append(b.allocator, .{ b.path("dist/linux/systemd.service.in"), b.fmt( "{s}/systemd/user/app-{s}.service", @@ -333,12 +336,12 @@ fn addLinuxAppResources( // AppStream metainfo so that application has rich metadata // within app stores - try ts.append(.{ + try ts.append(b.allocator, .{ b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml.in"), b.fmt("share/metainfo/{s}.metainfo.xml", .{app_id}), }); - break :templates ts.items; + break :templates try ts.toOwnedSlice(b.allocator); }; // Process all our templates @@ -361,65 +364,65 @@ fn addLinuxAppResources( template[1], ); - try steps.append(©.step); + try steps.append(b.allocator, ©.step); } // Right click menu action for Plasma desktop - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("dist/linux/ghostty_dolphin.desktop"), "share/kio/servicemenus/com.mitchellh.ghostty.desktop", ).step); // Right click menu action for Nautilus. Note that this _must_ be named // `ghostty.py`. Using the full app id causes problems (see #5468). - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("dist/linux/ghostty_nautilus.py"), "share/nautilus-python/extensions/ghostty.py", ).step); // Various icons that our application can use, including the icon // that will be used for the desktop. - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/16.png"), "share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png", ).step); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/32.png"), "share/icons/hicolor/32x32/apps/com.mitchellh.ghostty.png", ).step); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/128.png"), "share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png", ).step); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/256.png"), "share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png", ).step); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/512.png"), "share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png", ).step); // Flatpaks only support icons up to 512x512. if (!cfg.flatpak) { - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/1024.png"), "share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png", ).step); } - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/32.png"), "share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png", ).step); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/64.png"), "share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png", ).step); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/256.png"), "share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png", ).step); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( b.path("images/gnome/512.png"), "share/icons/hicolor/256x256@2/apps/com.mitchellh.ghostty.png", ).step); diff --git a/src/build/GhosttyWebdata.zig b/src/build/GhosttyWebdata.zig index b0201c3ff..145bb91fa 100644 --- a/src/build/GhosttyWebdata.zig +++ b/src/build/GhosttyWebdata.zig @@ -12,8 +12,8 @@ pub fn init( b: *std.Build, deps: *const SharedDeps, ) !GhosttyWebdata { - var steps = std.ArrayList(*std.Build.Step).init(b.allocator); - errdefer steps.deinit(); + var steps: std.ArrayList(*std.Build.Step) = .empty; + errdefer steps.deinit(b.allocator); { const webgen_config = b.addExecutable(.{ @@ -43,7 +43,7 @@ pub fn init( const webgen_config_step = b.addRunArtifact(webgen_config); const webgen_config_out = webgen_config_step.captureStdOut(); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( webgen_config_out, "share/ghostty/webdata/config.mdx", ).step); @@ -52,8 +52,10 @@ pub fn init( { const webgen_actions = b.addExecutable(.{ .name = "webgen_actions", - .root_source_file = b.path("src/main.zig"), - .target = b.graph.host, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = b.graph.host, + }), }); deps.help_strings.addImport(webgen_actions); @@ -72,7 +74,7 @@ pub fn init( const webgen_actions_step = b.addRunArtifact(webgen_actions); const webgen_actions_out = webgen_actions_step.captureStdOut(); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( webgen_actions_out, "share/ghostty/webdata/actions.mdx", ).step); @@ -81,8 +83,10 @@ pub fn init( { const webgen_commands = b.addExecutable(.{ .name = "webgen_commands", - .root_source_file = b.path("src/main.zig"), - .target = b.graph.host, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = b.graph.host, + }), }); deps.help_strings.addImport(webgen_commands); @@ -101,7 +105,7 @@ pub fn init( const webgen_commands_step = b.addRunArtifact(webgen_commands); const webgen_commands_out = webgen_commands_step.captureStdOut(); - try steps.append(&b.addInstallFile( + try steps.append(b.allocator, &b.addInstallFile( webgen_commands_out, "share/ghostty/webdata/commands.mdx", ).step); diff --git a/src/build/GhosttyXCFramework.zig b/src/build/GhosttyXCFramework.zig index d036e7020..3afeb9073 100644 --- a/src/build/GhosttyXCFramework.zig +++ b/src/build/GhosttyXCFramework.zig @@ -5,12 +5,11 @@ const Config = @import("Config.zig"); const SharedDeps = @import("SharedDeps.zig"); const GhosttyLib = @import("GhosttyLib.zig"); const XCFrameworkStep = @import("XCFrameworkStep.zig"); +const Target = @import("xcframework.zig").Target; xcframework: *XCFrameworkStep, target: Target, -pub const Target = enum { native, universal }; - pub fn init( b: *std.Build, deps: *const SharedDeps, diff --git a/src/build/GhosttyXcodebuild.zig b/src/build/GhosttyXcodebuild.zig index 0afb64007..27691d744 100644 --- a/src/build/GhosttyXcodebuild.zig +++ b/src/build/GhosttyXcodebuild.zig @@ -31,7 +31,7 @@ pub fn init( .ReleaseSafe, .ReleaseSmall, .ReleaseFast, - => "Release", + => "ReleaseLocal", }; const xc_arch: ?[]const u8 = switch (deps.xcframework.target) { diff --git a/src/build/GhosttyZig.zig b/src/build/GhosttyZig.zig index f175eb957..a8d2726bc 100644 --- a/src/build/GhosttyZig.zig +++ b/src/build/GhosttyZig.zig @@ -5,18 +5,17 @@ const GhosttyZig = @This(); const std = @import("std"); const Config = @import("Config.zig"); const SharedDeps = @import("SharedDeps.zig"); +const TerminalBuildOptions = @import("../terminal/build_options.zig").Options; +/// The `_c`-suffixed modules are built with the C ABI enabled. vt: *std.Build.Module, +vt_c: *std.Build.Module, pub fn init( b: *std.Build, cfg: *const Config, deps: *const SharedDeps, ) !GhosttyZig { - // General build options - const general_options = b.addOptions(); - try cfg.addOptions(general_options); - // Terminal module build options var vt_options = cfg.terminalOptions(); vt_options.artifact = .lib; @@ -25,7 +24,41 @@ pub fn init( // conditionally do this. vt_options.oniguruma = false; - const vt = b.addModule("ghostty-vt", .{ + return .{ + .vt = try initVt( + "ghostty-vt", + b, + cfg, + deps, + vt_options, + ), + + .vt_c = try initVt( + "ghostty-vt-c", + b, + cfg, + deps, + options: { + var dup = vt_options; + dup.c_abi = true; + break :options dup; + }, + ), + }; +} + +fn initVt( + name: []const u8, + b: *std.Build, + cfg: *const Config, + deps: *const SharedDeps, + vt_options: TerminalBuildOptions, +) !*std.Build.Module { + // General build options + const general_options = b.addOptions(); + try cfg.addOptions(general_options); + + const vt = b.addModule(name, .{ .root_source_file = b.path("src/lib_vt.zig"), .target = cfg.target, .optimize = cfg.optimize, @@ -45,5 +78,5 @@ pub fn init( try SharedDeps.addSimd(b, vt, null); } - return .{ .vt = vt }; + return vt; } diff --git a/src/build/MetallibStep.zig b/src/build/MetallibStep.zig index 6999f8f31..fcf3055f8 100644 --- a/src/build/MetallibStep.zig +++ b/src/build/MetallibStep.zig @@ -44,7 +44,7 @@ pub fn create(b: *std.Build, opts: Options) ?*MetallibStep { const self = b.allocator.create(MetallibStep) catch @panic("OOM"); const min_version = if (opts.target.query.os_version_min) |v| - b.fmt("{}", .{v.semver}) + b.fmt("{f}", .{v.semver}) else switch (opts.target.result.os.tag) { .macos => "10.14", .ios => "11.0", diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index b3fe860d1..dfa676bba 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -17,16 +17,26 @@ help_strings: HelpStrings, metallib: ?*MetallibStep, unicode_tables: UnicodeTables, framedata: GhosttyFrameData, +uucode_tables: std.Build.LazyPath, /// Used to keep track of a list of file sources. pub const LazyPathList = std.ArrayList(std.Build.LazyPath); pub fn init(b: *std.Build, cfg: *const Config) !SharedDeps { + const uucode_tables = blk: { + const uucode = b.dependency("uucode", .{ + .build_config_path = b.path("src/build/uucode_config.zig"), + }); + + break :blk uucode.namedLazyPath("tables.zig"); + }; + var result: SharedDeps = .{ .config = cfg, .help_strings = try .init(b, cfg), - .unicode_tables = try .init(b), + .unicode_tables = try .init(b, uucode_tables), .framedata = try .init(b), + .uucode_tables = uucode_tables, // Setup by retarget .options = undefined, @@ -103,8 +113,8 @@ pub fn add( // We maintain a list of our static libraries and return it so that // we can build a single fat static library for the final app. - var static_libs = LazyPathList.init(b.allocator); - errdefer static_libs.deinit(); + var static_libs: LazyPathList = .empty; + errdefer static_libs.deinit(b.allocator); // WARNING: This is a hack! // If we're cross-compiling to Darwin then we don't add any deps. @@ -144,6 +154,7 @@ pub fn add( } else { step.linkLibrary(freetype_dep.artifact("freetype")); try static_libs.append( + b.allocator, freetype_dep.artifact("freetype").getEmittedBin(), ); } @@ -168,6 +179,7 @@ pub fn add( } else { step.linkLibrary(harfbuzz_dep.artifact("harfbuzz")); try static_libs.append( + b.allocator, harfbuzz_dep.artifact("harfbuzz").getEmittedBin(), ); } @@ -191,6 +203,7 @@ pub fn add( } else { step.linkLibrary(fontconfig_dep.artifact("fontconfig")); try static_libs.append( + b.allocator, fontconfig_dep.artifact("fontconfig").getEmittedBin(), ); } @@ -208,6 +221,7 @@ pub fn add( })) |libpng_dep| { step.linkLibrary(libpng_dep.artifact("png")); try static_libs.append( + b.allocator, libpng_dep.artifact("png").getEmittedBin(), ); } @@ -221,6 +235,7 @@ pub fn add( })) |zlib_dep| { step.linkLibrary(zlib_dep.artifact("z")); try static_libs.append( + b.allocator, zlib_dep.artifact("z").getEmittedBin(), ); } @@ -240,6 +255,7 @@ pub fn add( } else { step.linkLibrary(oniguruma_dep.artifact("oniguruma")); try static_libs.append( + b.allocator, oniguruma_dep.artifact("oniguruma").getEmittedBin(), ); } @@ -260,6 +276,7 @@ pub fn add( } else { step.linkLibrary(glslang_dep.artifact("glslang")); try static_libs.append( + b.allocator, glslang_dep.artifact("glslang").getEmittedBin(), ); } @@ -279,6 +296,7 @@ pub fn add( } else { step.linkLibrary(spirv_cross_dep.artifact("spirv_cross")); try static_libs.append( + b.allocator, spirv_cross_dep.artifact("spirv_cross").getEmittedBin(), ); } @@ -297,6 +315,7 @@ pub fn add( ); step.linkLibrary(sentry_dep.artifact("sentry")); try static_libs.append( + b.allocator, sentry_dep.artifact("sentry").getEmittedBin(), ); @@ -306,6 +325,7 @@ pub fn add( .optimize = optimize, })) |breakpad_dep| { try static_libs.append( + b.allocator, breakpad_dep.artifact("breakpad").getEmittedBin(), ); } @@ -393,11 +413,13 @@ pub fn add( })) |dep| { step.root_module.addImport("z2d", dep.module("z2d")); } - if (b.lazyDependency("ziglyph", .{ + 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("ziglyph", dep.module("ziglyph")); + step.root_module.addImport("uucode", dep.module("uucode")); } if (b.lazyDependency("zf", .{ .target = target, @@ -431,6 +453,7 @@ pub fn add( macos_dep.artifact("macos"), ); try static_libs.append( + b.allocator, macos_dep.artifact("macos").getEmittedBin(), ); } @@ -449,6 +472,7 @@ pub fn add( })) |libintl_dep| { step.linkLibrary(libintl_dep.artifact("intl")); try static_libs.append( + b.allocator, libintl_dep.artifact("intl").getEmittedBin(), ); } @@ -461,7 +485,10 @@ pub fn add( })) |cimgui_dep| { step.root_module.addImport("cimgui", cimgui_dep.module("cimgui")); step.linkLibrary(cimgui_dep.artifact("cimgui")); - try static_libs.append(cimgui_dep.artifact("cimgui").getEmittedBin()); + try static_libs.append( + b.allocator, + cimgui_dep.artifact("cimgui").getEmittedBin(), + ); } // Fonts @@ -665,7 +692,7 @@ fn addGtkNg( } } -/// Add only the dependencies required for `Config.simd` enbled. This also +/// Add only the dependencies required for `Config.simd` enabled. This also /// adds all the simd source files for compilation. pub fn addSimd( b: *std.Build, @@ -685,6 +712,7 @@ pub fn addSimd( })) |simdutf_dep| { m.linkLibrary(simdutf_dep.artifact("simdutf")); if (static_libs) |v| try v.append( + b.allocator, simdutf_dep.artifact("simdutf").getEmittedBin(), ); } @@ -696,7 +724,10 @@ pub fn addSimd( .optimize = optimize, })) |highway_dep| { m.linkLibrary(highway_dep.artifact("highway")); - if (static_libs) |v| try v.append(highway_dep.artifact("highway").getEmittedBin()); + 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 @@ -705,7 +736,10 @@ pub fn addSimd( .optimize = optimize, })) |utfcpp_dep| { m.linkLibrary(utfcpp_dep.artifact("utfcpp")); - if (static_libs) |v| try v.append(utfcpp_dep.artifact("utfcpp").getEmittedBin()); + if (static_libs) |v| try v.append( + b.allocator, + utfcpp_dep.artifact("utfcpp").getEmittedBin(), + ); } // SIMD C++ files @@ -749,16 +783,20 @@ pub fn gtkNgDistResources( const gresource_xml = gresource_xml: { const xml_exe = b.addExecutable(.{ .name = "generate_gresource_xml", - .root_source_file = b.path("src/apprt/gtk/build/gresource.zig"), - .target = b.graph.host, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/apprt/gtk/build/gresource.zig"), + .target = b.graph.host, + }), }); const xml_run = b.addRunArtifact(xml_exe); // Run our blueprint compiler across all of our blueprint files. const blueprint_exe = b.addExecutable(.{ .name = "gtk_blueprint_compiler", - .root_source_file = b.path("src/apprt/gtk/build/blueprint.zig"), - .target = b.graph.host, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/apprt/gtk/build/blueprint.zig"), + .target = b.graph.host, + }), }); blueprint_exe.linkLibC(); blueprint_exe.linkSystemLibrary2("gtk4", dynamic_link_opts); diff --git a/src/build/UnicodeTables.zig b/src/build/UnicodeTables.zig index 0f558b708..aba3e8f24 100644 --- a/src/build/UnicodeTables.zig +++ b/src/build/UnicodeTables.zig @@ -11,37 +11,42 @@ symbols_exe: *std.Build.Step.Compile, props_output: std.Build.LazyPath, symbols_output: std.Build.LazyPath, -pub fn init(b: *std.Build) !UnicodeTables { +pub fn init(b: *std.Build, uucode_tables: std.Build.LazyPath) !UnicodeTables { const props_exe = b.addExecutable(.{ .name = "props-unigen", .root_module = b.createModule(.{ - .root_source_file = b.path("src/unicode/props_ziglyph.zig"), + .root_source_file = b.path("src/unicode/props_uucode.zig"), .target = b.graph.host, .strip = false, .omit_frame_pointer = false, .unwind_tables = .sync, }), + + // TODO: x86_64 self-hosted crashes + .use_llvm = true, }); const symbols_exe = b.addExecutable(.{ .name = "symbols-unigen", .root_module = b.createModule(.{ - .root_source_file = b.path("src/unicode/symbols_ziglyph.zig"), + .root_source_file = b.path("src/unicode/symbols_uucode.zig"), .target = b.graph.host, .strip = false, .omit_frame_pointer = false, .unwind_tables = .sync, }), + + // TODO: x86_64 self-hosted crashes + .use_llvm = true, }); - if (b.lazyDependency("ziglyph", .{ + if (b.lazyDependency("uucode", .{ .target = b.graph.host, - })) |ziglyph_dep| { + .tables_path = uucode_tables, + .build_config_path = b.path("src/build/uucode_config.zig"), + })) |dep| { inline for (&.{ props_exe, symbols_exe }) |exe| { - exe.root_module.addImport( - "ziglyph", - ziglyph_dep.module("ziglyph"), - ); + exe.root_module.addImport("uucode", dep.module("uucode")); } } diff --git a/src/build/docker/debian/Dockerfile b/src/build/docker/debian/Dockerfile index 73c7da7c8..ffeef3d6a 100644 --- a/src/build/docker/debian/Dockerfile +++ b/src/build/docker/debian/Dockerfile @@ -24,15 +24,15 @@ RUN DEBIAN_FRONTEND="noninteractive" apt-get -qq update && \ WORKDIR /src -COPY ./build.zig /src +COPY ./build.zig ./build.zig.zon /src/ # Install zig # https://ziglang.org/download/ -RUN export ZIG_VERSION=$(sed -n -e 's/^.*requireZig("\(.*\)").*$/\1/p' build.zig) && curl -L -o /tmp/zig.tar.xz "https://ziglang.org/download/$ZIG_VERSION/zig-linux-$(uname -m)-$ZIG_VERSION.tar.xz" && \ +RUN export ZIG_VERSION=$(sed -n -E 's/^\s*\.?minimum_zig_version\s*=\s*"([^"]+)".*/\1/p' build.zig.zon) && curl -L -o /tmp/zig.tar.xz "https://ziglang.org/download/$ZIG_VERSION/zig-$(uname -m)-linux-$ZIG_VERSION.tar.xz" && \ tar -xf /tmp/zig.tar.xz -C /opt && \ rm /tmp/zig.tar.xz && \ - ln -s "/opt/zig-linux-$(uname -m)-$ZIG_VERSION/zig" /usr/local/bin/zig + ln -s "/opt/zig-$(uname -m)-linux-$ZIG_VERSION/zig" /usr/local/bin/zig COPY . /src @@ -41,4 +41,3 @@ RUN zig build \ -Dcpu=baseline RUN ./zig-out/bin/ghostty +version - diff --git a/src/build/docker/lib-c-docs/Dockerfile b/src/build/docker/lib-c-docs/Dockerfile new file mode 100644 index 000000000..a3cfdcc98 --- /dev/null +++ b/src/build/docker/lib-c-docs/Dockerfile @@ -0,0 +1,38 @@ +#-------------------------------------------------------------------- +# Generate documentation with Doxygen +#-------------------------------------------------------------------- +FROM --platform=linux/amd64 archlinux:latest AS builder + +# Build argument for noindex header +ARG ADD_NOINDEX_HEADER=false +RUN pacman -Syu --noconfirm && \ + pacman -S --noconfirm \ + doxygen \ + graphviz && \ + pacman -Scc --noconfirm +WORKDIR /ghostty +COPY include/ ./include/ +COPY images/ ./images/ +COPY dist/doxygen/ ./dist/doxygen/ +COPY example/ ./example/ +COPY Doxyfile ./ +COPY DoxygenLayout.xml ./ +RUN mkdir -p zig-out/share/ghostty/doc/libghostty +RUN doxygen + +#-------------------------------------------------------------------- +# Host the static HTML +#-------------------------------------------------------------------- +FROM nginx:alpine AS runtime + +# Pass build arg to runtime stage +ARG ADD_NOINDEX_HEADER=false +ENV ADD_NOINDEX_HEADER=$ADD_NOINDEX_HEADER + +# Copy documentation and entrypoint script +COPY --from=builder /ghostty/zig-out/share/ghostty/doc/libghostty /usr/share/nginx/html +COPY src/build/docker/lib-c-docs/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 80 +CMD ["/entrypoint.sh"] diff --git a/src/build/docker/lib-c-docs/entrypoint.sh b/src/build/docker/lib-c-docs/entrypoint.sh new file mode 100755 index 000000000..ac9ca1c06 --- /dev/null +++ b/src/build/docker/lib-c-docs/entrypoint.sh @@ -0,0 +1,32 @@ +#!/bin/sh +if [ "$ADD_NOINDEX_HEADER" = "true" ]; then + cat > /etc/nginx/conf.d/noindex.conf << 'EOF' +server { + listen 80; + location / { + root /usr/share/nginx/html; + index index.html; + etag on; + add_header Cache-Control "no-cache" always; + add_header X-Robots-Tag "noindex, nofollow" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always; + } +} +EOF + # Remove default server config + rm -f /etc/nginx/conf.d/default.conf +else + cat > /etc/nginx/conf.d/default.conf << 'EOF' +server { + listen 80; + location / { + root /usr/share/nginx/html; + index index.html; + etag on; + add_header Cache-Control "no-cache" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always; + } +} +EOF +fi +exec nginx -g "daemon off;" diff --git a/src/build/framegen/main.c b/src/build/framegen/main.c new file mode 100644 index 000000000..647768006 --- /dev/null +++ b/src/build/framegen/main.c @@ -0,0 +1,145 @@ +#include +#include +#include +#include +#include +#include +#include + +#define SEPARATOR '\x01' +#define CHUNK_SIZE 16384 + +static int filter_frames(const struct dirent *entry) { + const char *name = entry->d_name; + size_t len = strlen(name); + return len > 4 && strcmp(name + len - 4, ".txt") == 0; +} + +static int compare_frames(const struct dirent **a, const struct dirent **b) { + return strcmp((*a)->d_name, (*b)->d_name); +} + +static char *read_file(const char *path, size_t *out_size) { + FILE *f = fopen(path, "rb"); + if (!f) { + fprintf(stderr, "Failed to open %s: %s\n", path, strerror(errno)); + return NULL; + } + + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + char *buf = malloc(size); + if (!buf) { + return NULL; + } + + if (fread(buf, 1, size, f) != (size_t)size) { + fprintf(stderr, "Failed to read %s\n", path); + return NULL; + } + + fclose(f); + *out_size = size; + return buf; +} + +int main(int argc, char **argv) { + if (argc != 3) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + const char *frames_dir = argv[1]; + const char *output_file = argv[2]; + + struct dirent **namelist; + int n = scandir(frames_dir, &namelist, filter_frames, compare_frames); + if (n < 0) { + fprintf(stderr, "Failed to scan directory %s: %s\n", frames_dir, strerror(errno)); + return 1; + } + + if (n == 0) { + fprintf(stderr, "No frame files found in %s\n", frames_dir); + return 1; + } + + size_t total_size = 0; + char **frame_contents = calloc(n, sizeof(char*)); + size_t *frame_sizes = calloc(n, sizeof(size_t)); + + for (int i = 0; i < n; i++) { + char path[4096]; + snprintf(path, sizeof(path), "%s/%s", frames_dir, namelist[i]->d_name); + + frame_contents[i] = read_file(path, &frame_sizes[i]); + if (!frame_contents[i]) { + return 1; + } + + total_size += frame_sizes[i]; + if (i < n - 1) total_size++; + } + + char *joined = malloc(total_size); + if (!joined) { + fprintf(stderr, "Failed to allocate joined buffer\n"); + return 1; + } + + size_t offset = 0; + for (int i = 0; i < n; i++) { + memcpy(joined + offset, frame_contents[i], frame_sizes[i]); + offset += frame_sizes[i]; + if (i < n - 1) { + joined[offset++] = SEPARATOR; + } + } + + uLongf compressed_size = compressBound(total_size); + unsigned char *compressed = malloc(compressed_size); + if (!compressed) { + fprintf(stderr, "Failed to allocate compression buffer\n"); + return 1; + } + + z_stream stream = {0}; + stream.next_in = (unsigned char*)joined; + stream.avail_in = total_size; + stream.next_out = compressed; + stream.avail_out = compressed_size; + + // Use -MAX_WBITS for raw DEFLATE (no zlib wrapper) + int ret = deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY); + if (ret != Z_OK) { + fprintf(stderr, "deflateInit2 failed: %d\n", ret); + return 1; + } + + ret = deflate(&stream, Z_FINISH); + if (ret != Z_STREAM_END) { + fprintf(stderr, "deflate failed: %d\n", ret); + deflateEnd(&stream); + return 1; + } + + compressed_size = stream.total_out; + deflateEnd(&stream); + + FILE *out = fopen(output_file, "wb"); + if (!out) { + fprintf(stderr, "Failed to create %s: %s\n", output_file, strerror(errno)); + return 1; + } + + if (fwrite(compressed, 1, compressed_size, out) != compressed_size) { + fprintf(stderr, "Failed to write compressed data\n"); + return 1; + } + + fclose(out); + + return 0; +} diff --git a/src/build/framegen/main.zig b/src/build/framegen/main.zig deleted file mode 100644 index f4a7d9443..000000000 --- a/src/build/framegen/main.zig +++ /dev/null @@ -1,273 +0,0 @@ -const std = @import("std"); -const fs = std.fs; - -/// Generates a compressed file of all the ghostty frames -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - - var arg_iter = try std.process.argsWithAllocator(gpa.allocator()); - // Skip the exe name - _ = arg_iter.skip(); - - const out_dir_path = arg_iter.next() orelse return error.MissingOutputPath; - const compressed_out = "framedata.compressed"; - const zig_out = "framedata.zig"; - - const out_dir = try fs.cwd().openDir(out_dir_path, .{}); - const compressed_file = try out_dir.createFile(compressed_out, .{}); - - // Join the frames with a null byte. We'll split on this later - const all_frames = try std.mem.join(gpa.allocator(), "\x01", &frames); - var fbs = std.io.fixedBufferStream(all_frames); - - const reader = fbs.reader(); - try std.compress.flate.compress(reader, compressed_file.writer(), .{}); - - const compressed_path = try std.fs.path.join(gpa.allocator(), &.{ out_dir_path, compressed_out }); - - const zig_file = try out_dir.createFile(zig_out, .{}); - - try zig_file.writer().print( - \\//! This file is auto-generated. Do not edit. - \\ - \\pub const compressed = @embedFile("{s}"); - , .{compressed_path}); -} - -const frames = [_][]const u8{ - @embedFile("frames/frame_001.txt"), - @embedFile("frames/frame_002.txt"), - @embedFile("frames/frame_003.txt"), - @embedFile("frames/frame_004.txt"), - @embedFile("frames/frame_005.txt"), - @embedFile("frames/frame_006.txt"), - @embedFile("frames/frame_007.txt"), - @embedFile("frames/frame_008.txt"), - @embedFile("frames/frame_009.txt"), - @embedFile("frames/frame_010.txt"), - @embedFile("frames/frame_011.txt"), - @embedFile("frames/frame_012.txt"), - @embedFile("frames/frame_013.txt"), - @embedFile("frames/frame_014.txt"), - @embedFile("frames/frame_015.txt"), - @embedFile("frames/frame_016.txt"), - @embedFile("frames/frame_017.txt"), - @embedFile("frames/frame_018.txt"), - @embedFile("frames/frame_019.txt"), - @embedFile("frames/frame_020.txt"), - @embedFile("frames/frame_021.txt"), - @embedFile("frames/frame_022.txt"), - @embedFile("frames/frame_023.txt"), - @embedFile("frames/frame_024.txt"), - @embedFile("frames/frame_025.txt"), - @embedFile("frames/frame_026.txt"), - @embedFile("frames/frame_027.txt"), - @embedFile("frames/frame_028.txt"), - @embedFile("frames/frame_029.txt"), - @embedFile("frames/frame_030.txt"), - @embedFile("frames/frame_031.txt"), - @embedFile("frames/frame_032.txt"), - @embedFile("frames/frame_033.txt"), - @embedFile("frames/frame_034.txt"), - @embedFile("frames/frame_035.txt"), - @embedFile("frames/frame_036.txt"), - @embedFile("frames/frame_037.txt"), - @embedFile("frames/frame_038.txt"), - @embedFile("frames/frame_039.txt"), - @embedFile("frames/frame_040.txt"), - @embedFile("frames/frame_041.txt"), - @embedFile("frames/frame_042.txt"), - @embedFile("frames/frame_043.txt"), - @embedFile("frames/frame_044.txt"), - @embedFile("frames/frame_045.txt"), - @embedFile("frames/frame_046.txt"), - @embedFile("frames/frame_047.txt"), - @embedFile("frames/frame_048.txt"), - @embedFile("frames/frame_049.txt"), - @embedFile("frames/frame_050.txt"), - @embedFile("frames/frame_051.txt"), - @embedFile("frames/frame_052.txt"), - @embedFile("frames/frame_053.txt"), - @embedFile("frames/frame_054.txt"), - @embedFile("frames/frame_055.txt"), - @embedFile("frames/frame_056.txt"), - @embedFile("frames/frame_057.txt"), - @embedFile("frames/frame_058.txt"), - @embedFile("frames/frame_059.txt"), - @embedFile("frames/frame_060.txt"), - @embedFile("frames/frame_061.txt"), - @embedFile("frames/frame_062.txt"), - @embedFile("frames/frame_063.txt"), - @embedFile("frames/frame_064.txt"), - @embedFile("frames/frame_065.txt"), - @embedFile("frames/frame_066.txt"), - @embedFile("frames/frame_067.txt"), - @embedFile("frames/frame_068.txt"), - @embedFile("frames/frame_069.txt"), - @embedFile("frames/frame_070.txt"), - @embedFile("frames/frame_071.txt"), - @embedFile("frames/frame_072.txt"), - @embedFile("frames/frame_073.txt"), - @embedFile("frames/frame_074.txt"), - @embedFile("frames/frame_075.txt"), - @embedFile("frames/frame_076.txt"), - @embedFile("frames/frame_077.txt"), - @embedFile("frames/frame_078.txt"), - @embedFile("frames/frame_079.txt"), - @embedFile("frames/frame_080.txt"), - @embedFile("frames/frame_081.txt"), - @embedFile("frames/frame_082.txt"), - @embedFile("frames/frame_083.txt"), - @embedFile("frames/frame_084.txt"), - @embedFile("frames/frame_085.txt"), - @embedFile("frames/frame_086.txt"), - @embedFile("frames/frame_087.txt"), - @embedFile("frames/frame_088.txt"), - @embedFile("frames/frame_089.txt"), - @embedFile("frames/frame_090.txt"), - @embedFile("frames/frame_091.txt"), - @embedFile("frames/frame_092.txt"), - @embedFile("frames/frame_093.txt"), - @embedFile("frames/frame_094.txt"), - @embedFile("frames/frame_095.txt"), - @embedFile("frames/frame_096.txt"), - @embedFile("frames/frame_097.txt"), - @embedFile("frames/frame_098.txt"), - @embedFile("frames/frame_099.txt"), - @embedFile("frames/frame_100.txt"), - @embedFile("frames/frame_101.txt"), - @embedFile("frames/frame_102.txt"), - @embedFile("frames/frame_103.txt"), - @embedFile("frames/frame_104.txt"), - @embedFile("frames/frame_105.txt"), - @embedFile("frames/frame_106.txt"), - @embedFile("frames/frame_107.txt"), - @embedFile("frames/frame_108.txt"), - @embedFile("frames/frame_109.txt"), - @embedFile("frames/frame_110.txt"), - @embedFile("frames/frame_111.txt"), - @embedFile("frames/frame_112.txt"), - @embedFile("frames/frame_113.txt"), - @embedFile("frames/frame_114.txt"), - @embedFile("frames/frame_115.txt"), - @embedFile("frames/frame_116.txt"), - @embedFile("frames/frame_117.txt"), - @embedFile("frames/frame_118.txt"), - @embedFile("frames/frame_119.txt"), - @embedFile("frames/frame_120.txt"), - @embedFile("frames/frame_121.txt"), - @embedFile("frames/frame_122.txt"), - @embedFile("frames/frame_123.txt"), - @embedFile("frames/frame_124.txt"), - @embedFile("frames/frame_125.txt"), - @embedFile("frames/frame_126.txt"), - @embedFile("frames/frame_127.txt"), - @embedFile("frames/frame_128.txt"), - @embedFile("frames/frame_129.txt"), - @embedFile("frames/frame_130.txt"), - @embedFile("frames/frame_131.txt"), - @embedFile("frames/frame_132.txt"), - @embedFile("frames/frame_133.txt"), - @embedFile("frames/frame_134.txt"), - @embedFile("frames/frame_135.txt"), - @embedFile("frames/frame_136.txt"), - @embedFile("frames/frame_137.txt"), - @embedFile("frames/frame_138.txt"), - @embedFile("frames/frame_139.txt"), - @embedFile("frames/frame_140.txt"), - @embedFile("frames/frame_141.txt"), - @embedFile("frames/frame_142.txt"), - @embedFile("frames/frame_143.txt"), - @embedFile("frames/frame_144.txt"), - @embedFile("frames/frame_145.txt"), - @embedFile("frames/frame_146.txt"), - @embedFile("frames/frame_147.txt"), - @embedFile("frames/frame_148.txt"), - @embedFile("frames/frame_149.txt"), - @embedFile("frames/frame_150.txt"), - @embedFile("frames/frame_151.txt"), - @embedFile("frames/frame_152.txt"), - @embedFile("frames/frame_153.txt"), - @embedFile("frames/frame_154.txt"), - @embedFile("frames/frame_155.txt"), - @embedFile("frames/frame_156.txt"), - @embedFile("frames/frame_157.txt"), - @embedFile("frames/frame_158.txt"), - @embedFile("frames/frame_159.txt"), - @embedFile("frames/frame_160.txt"), - @embedFile("frames/frame_161.txt"), - @embedFile("frames/frame_162.txt"), - @embedFile("frames/frame_163.txt"), - @embedFile("frames/frame_164.txt"), - @embedFile("frames/frame_165.txt"), - @embedFile("frames/frame_166.txt"), - @embedFile("frames/frame_167.txt"), - @embedFile("frames/frame_168.txt"), - @embedFile("frames/frame_169.txt"), - @embedFile("frames/frame_170.txt"), - @embedFile("frames/frame_171.txt"), - @embedFile("frames/frame_172.txt"), - @embedFile("frames/frame_173.txt"), - @embedFile("frames/frame_174.txt"), - @embedFile("frames/frame_175.txt"), - @embedFile("frames/frame_176.txt"), - @embedFile("frames/frame_177.txt"), - @embedFile("frames/frame_178.txt"), - @embedFile("frames/frame_179.txt"), - @embedFile("frames/frame_180.txt"), - @embedFile("frames/frame_181.txt"), - @embedFile("frames/frame_182.txt"), - @embedFile("frames/frame_183.txt"), - @embedFile("frames/frame_184.txt"), - @embedFile("frames/frame_185.txt"), - @embedFile("frames/frame_186.txt"), - @embedFile("frames/frame_187.txt"), - @embedFile("frames/frame_188.txt"), - @embedFile("frames/frame_189.txt"), - @embedFile("frames/frame_190.txt"), - @embedFile("frames/frame_191.txt"), - @embedFile("frames/frame_192.txt"), - @embedFile("frames/frame_193.txt"), - @embedFile("frames/frame_194.txt"), - @embedFile("frames/frame_195.txt"), - @embedFile("frames/frame_196.txt"), - @embedFile("frames/frame_197.txt"), - @embedFile("frames/frame_198.txt"), - @embedFile("frames/frame_199.txt"), - @embedFile("frames/frame_200.txt"), - @embedFile("frames/frame_201.txt"), - @embedFile("frames/frame_202.txt"), - @embedFile("frames/frame_203.txt"), - @embedFile("frames/frame_204.txt"), - @embedFile("frames/frame_205.txt"), - @embedFile("frames/frame_206.txt"), - @embedFile("frames/frame_207.txt"), - @embedFile("frames/frame_208.txt"), - @embedFile("frames/frame_209.txt"), - @embedFile("frames/frame_210.txt"), - @embedFile("frames/frame_211.txt"), - @embedFile("frames/frame_212.txt"), - @embedFile("frames/frame_213.txt"), - @embedFile("frames/frame_214.txt"), - @embedFile("frames/frame_215.txt"), - @embedFile("frames/frame_216.txt"), - @embedFile("frames/frame_217.txt"), - @embedFile("frames/frame_218.txt"), - @embedFile("frames/frame_219.txt"), - @embedFile("frames/frame_220.txt"), - @embedFile("frames/frame_221.txt"), - @embedFile("frames/frame_222.txt"), - @embedFile("frames/frame_223.txt"), - @embedFile("frames/frame_224.txt"), - @embedFile("frames/frame_225.txt"), - @embedFile("frames/frame_226.txt"), - @embedFile("frames/frame_227.txt"), - @embedFile("frames/frame_228.txt"), - @embedFile("frames/frame_229.txt"), - @embedFile("frames/frame_230.txt"), - @embedFile("frames/frame_231.txt"), - @embedFile("frames/frame_232.txt"), - @embedFile("frames/frame_233.txt"), - @embedFile("frames/frame_234.txt"), - @embedFile("frames/frame_235.txt"), -}; diff --git a/src/build/mdgen/ghostty_1_footer.md b/src/build/mdgen/ghostty_1_footer.md index f8e502b45..88aa16273 100644 --- a/src/build/mdgen/ghostty_1_footer.md +++ b/src/build/mdgen/ghostty_1_footer.md @@ -1,15 +1,15 @@ # FILES -_\$XDG_CONFIG_HOME/ghostty/config_ +_\$XDG_CONFIG_HOME/ghostty/config.ghostty_ : Location of the default configuration file. -_\$HOME/Library/Application Support/com.mitchellh.ghostty/config_ +_\$HOME/Library/Application Support/com.mitchellh.ghostty/config.ghostty_ : **On macOS**, location of the default configuration file. This location takes precedence over the XDG environment locations. -_\$LOCALAPPDATA/ghostty/config_ +_\$LOCALAPPDATA/ghostty/config.ghostty_ : **On Windows**, if _\$XDG_CONFIG_HOME_ is not set, _\$LOCALAPPDATA_ will be searched for configuration files. diff --git a/src/build/mdgen/ghostty_5_footer.md b/src/build/mdgen/ghostty_5_footer.md index 380d83a53..d2cf024d1 100644 --- a/src/build/mdgen/ghostty_5_footer.md +++ b/src/build/mdgen/ghostty_5_footer.md @@ -1,15 +1,15 @@ # FILES -_\$XDG_CONFIG_HOME/ghostty/config_ +_\$XDG_CONFIG_HOME/ghostty/config.ghostty_ : Location of the default configuration file. -_\$HOME/Library/Application Support/com.mitchellh.ghostty/config_ +_\$HOME/Library/Application Support/com.mitchellh.ghostty/config.ghostty_ : **On macOS**, location of the default configuration file. This location takes precedence over the XDG environment locations. -_\$LOCALAPPDATA/ghostty/config_ +_\$LOCALAPPDATA/ghostty/config.ghostty_ : **On Windows**, if _\$XDG_CONFIG_HOME_ is not set, _\$LOCALAPPDATA_ will be searched for configuration files. diff --git a/src/build/mdgen/ghostty_5_header.md b/src/build/mdgen/ghostty_5_header.md index 078133861..b9d4cb751 100644 --- a/src/build/mdgen/ghostty_5_header.md +++ b/src/build/mdgen/ghostty_5_header.md @@ -8,11 +8,11 @@ To configure Ghostty, you must use a configuration file. GUI-based configuration is on the roadmap but not yet supported. The configuration file must be placed -at `$XDG_CONFIG_HOME/ghostty/config`, which defaults to `~/.config/ghostty/config` +at `$XDG_CONFIG_HOME/ghostty/config.ghostty`, which defaults to `~/.config/ghostty/config.ghostty` if the [XDG environment is not set](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html). **If you are using macOS, the configuration file can also be placed at -`$HOME/Library/Application Support/com.mitchellh.ghostty/config`.** This is the +`$HOME/Library/Application Support/com.mitchellh.ghostty/config.ghostty`.** This is the default configuration location for macOS. It will be searched before any of the XDG environment locations listed above. diff --git a/src/build/mdgen/main_ghostty_1.zig b/src/build/mdgen/main_ghostty_1.zig index b3663de8d..2bb413d93 100644 --- a/src/build/mdgen/main_ghostty_1.zig +++ b/src/build/mdgen/main_ghostty_1.zig @@ -2,12 +2,15 @@ const std = @import("std"); const gen = @import("mdgen.zig"); pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; const alloc = gpa.allocator(); - const writer = std.io.getStdOut().writer(); + var buffer: [1024]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const writer = &stdout_writer.interface; try gen.substitute(alloc, @embedFile("ghostty_1_header.md"), writer); try gen.genActions(writer); try gen.genConfig(writer, true); try gen.substitute(alloc, @embedFile("ghostty_1_footer.md"), writer); + try writer.flush(); } diff --git a/src/build/mdgen/main_ghostty_5.zig b/src/build/mdgen/main_ghostty_5.zig index 77c72b946..2123b0bce 100644 --- a/src/build/mdgen/main_ghostty_5.zig +++ b/src/build/mdgen/main_ghostty_5.zig @@ -2,12 +2,15 @@ const std = @import("std"); const gen = @import("mdgen.zig"); pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; const alloc = gpa.allocator(); - const output = std.io.getStdOut().writer(); - try gen.substitute(alloc, @embedFile("ghostty_5_header.md"), output); - try gen.genConfig(output, false); - try gen.genKeybindActions(output); - try gen.substitute(alloc, @embedFile("ghostty_5_footer.md"), output); + var buffer: [1024]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const writer = &stdout_writer.interface; + try gen.substitute(alloc, @embedFile("ghostty_5_header.md"), writer); + try gen.genConfig(writer, false); + try gen.genKeybindActions(writer); + try gen.substitute(alloc, @embedFile("ghostty_5_footer.md"), writer); + try writer.flush(); } diff --git a/src/build/mdgen/mdgen.zig b/src/build/mdgen/mdgen.zig index 53ed02067..530c8964f 100644 --- a/src/build/mdgen/mdgen.zig +++ b/src/build/mdgen/mdgen.zig @@ -5,7 +5,7 @@ const Config = @import("../../config/Config.zig"); const Action = @import("../../cli/ghostty.zig").Action; const KeybindAction = @import("../../input/Binding.zig").Action; -pub fn substitute(alloc: std.mem.Allocator, input: []const u8, writer: anytype) !void { +pub fn substitute(alloc: std.mem.Allocator, input: []const u8, writer: *std.Io.Writer) !void { const output = try alloc.alloc(u8, std.mem.replacementSize( u8, input, @@ -18,7 +18,7 @@ pub fn substitute(alloc: std.mem.Allocator, input: []const u8, writer: anytype) try writer.writeAll(output); } -pub fn genConfig(writer: anytype, cli: bool) !void { +pub fn genConfig(writer: *std.Io.Writer, cli: bool) !void { try writer.writeAll( \\ \\# CONFIGURATION OPTIONS @@ -48,7 +48,7 @@ pub fn genConfig(writer: anytype, cli: bool) !void { } } -pub fn genActions(writer: anytype) !void { +pub fn genActions(writer: *std.Io.Writer) !void { try writer.writeAll( \\ \\# COMMAND LINE ACTIONS @@ -83,7 +83,7 @@ pub fn genActions(writer: anytype) !void { } } -pub fn genKeybindActions(writer: anytype) !void { +pub fn genKeybindActions(writer: *std.Io.Writer) !void { try writer.writeAll( \\ \\# KEYBIND ACTIONS diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig new file mode 100644 index 000000000..9a3b4bec7 --- /dev/null +++ b/src/build/uucode_config.zig @@ -0,0 +1,82 @@ +const std = @import("std"); +const config = @import("config.zig"); +const config_x = @import("config.x.zig"); +const d = config.default; +const wcwidth = config_x.wcwidth; + +const Allocator = std.mem.Allocator; + +fn computeWidth( + alloc: std.mem.Allocator, + cp: u21, + data: anytype, + backing: anytype, + tracking: anytype, +) Allocator.Error!void { + _ = alloc; + _ = cp; + _ = backing; + _ = tracking; + data.width = @intCast(@min(2, @max(0, data.wcwidth))); +} + +const width = config.Extension{ + .inputs = &.{"wcwidth"}, + .compute = &computeWidth, + .fields = &.{ + .{ .name = "width", .type = u2 }, + }, +}; + +fn computeIsSymbol( + alloc: Allocator, + cp: u21, + data: anytype, + backing: anytype, + tracking: anytype, +) Allocator.Error!void { + _ = alloc; + _ = cp; + _ = backing; + _ = tracking; + const block = data.block; + data.is_symbol = data.general_category == .other_private_use or + block == .arrows or + block == .dingbats or + block == .emoticons or + block == .miscellaneous_symbols or + block == .enclosed_alphanumerics or + block == .enclosed_alphanumeric_supplement or + block == .miscellaneous_symbols_and_pictographs or + block == .transport_and_map_symbols; +} + +const is_symbol = config.Extension{ + .inputs = &.{ "block", "general_category" }, + .compute = &computeIsSymbol, + .fields = &.{ + .{ .name = "is_symbol", .type = bool }, + }, +}; + +pub const tables = [_]config.Table{ + .{ + .name = "runtime", + .extensions = &.{}, + .fields = &.{ + d.field("is_emoji_presentation"), + d.field("case_folding_full"), + }, + }, + .{ + .name = "buildtime", + .extensions = &.{ wcwidth, width, is_symbol }, + .fields = &.{ + width.field("width"), + d.field("grapheme_break"), + is_symbol.field("is_symbol"), + d.field("is_emoji_modifier"), + d.field("is_emoji_modifier_base"), + }, + }, +}; diff --git a/src/build/webgen/main_actions.zig b/src/build/webgen/main_actions.zig index 5002a5bac..85357b972 100644 --- a/src/build/webgen/main_actions.zig +++ b/src/build/webgen/main_actions.zig @@ -3,6 +3,8 @@ const help_strings = @import("help_strings"); const helpgen_actions = @import("../../input/helpgen_actions.zig"); pub fn main() !void { - const output = std.io.getStdOut().writer(); - try helpgen_actions.generate(output, .markdown, true, std.heap.page_allocator); + var buffer: [2048]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; + try helpgen_actions.generate(stdout, .markdown, true, std.heap.page_allocator); } diff --git a/src/build/webgen/main_commands.zig b/src/build/webgen/main_commands.zig index ad5c75734..65f144522 100644 --- a/src/build/webgen/main_commands.zig +++ b/src/build/webgen/main_commands.zig @@ -3,14 +3,16 @@ const Action = @import("../../cli/ghostty.zig").Action; const help_strings = @import("help_strings"); pub fn main() !void { - const output = std.io.getStdOut().writer(); - try genActions(output); + var buffer: [2048]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; + try genActions(stdout); } // Note: as a shortcut for defining inline editOnGithubLinks per cli action the user // is directed to the folder view on Github. This includes a README pointing them to // the files to edit. -pub fn genActions(writer: anytype) !void { +pub fn genActions(writer: *std.Io.Writer) !void { // Write the header try writer.writeAll( \\--- diff --git a/src/build/webgen/main_config.zig b/src/build/webgen/main_config.zig index 1bde2f9cc..1363fadc4 100644 --- a/src/build/webgen/main_config.zig +++ b/src/build/webgen/main_config.zig @@ -3,11 +3,13 @@ const Config = @import("../../config/Config.zig"); const help_strings = @import("help_strings"); pub fn main() !void { - const output = std.io.getStdOut().writer(); - try genConfig(output); + var buffer: [2048]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; + try genConfig(stdout); } -pub fn genConfig(writer: anytype) !void { +pub fn genConfig(writer: *std.Io.Writer) !void { // Write the header try writer.writeAll( \\--- @@ -122,7 +124,7 @@ pub fn genConfig(writer: anytype) !void { } } -fn endBlock(writer: anytype, block: anytype) !void { +fn endBlock(writer: *std.Io.Writer, block: anytype) !void { if (block) |v| switch (v) { .text => {}, .code => try writer.writeAll("```\n"), diff --git a/src/build/xcframework.zig b/src/build/xcframework.zig new file mode 100644 index 000000000..8713a1c9a --- /dev/null +++ b/src/build/xcframework.zig @@ -0,0 +1,3 @@ +/// Target for xcframework builds. This is a separate file so that +/// our runtime code doesn't need to import build code. +pub const Target = enum { native, universal }; diff --git a/src/build/zig.zig b/src/build/zig.zig index 7e327127d..3ee8ffe74 100644 --- a/src/build/zig.zig +++ b/src/build/zig.zig @@ -7,10 +7,11 @@ pub fn requireZig(comptime required_zig: []const u8) void { const current_vsn = builtin.zig_version; const required_vsn = std.SemanticVersion.parse(required_zig) catch unreachable; if (current_vsn.major != required_vsn.major or - current_vsn.minor != required_vsn.minor) + current_vsn.minor != required_vsn.minor or + current_vsn.patch < required_vsn.patch) { @compileError(std.fmt.comptimePrint( - "Your Zig version v{} does not meet the required build version of v{}", + "Your Zig version v{f} does not meet the required build version of v{f}", .{ current_vsn, required_vsn }, )); } diff --git a/src/cli/args.zig b/src/cli/args.zig index 2d2d199be..a34560b78 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -162,10 +162,11 @@ pub fn parse( error.InvalidField => "unknown field", error.ValueRequired => formatValueRequired(T, arena_alloc, key) catch "value required", error.InvalidValue => formatInvalidValue(T, arena_alloc, key, value) catch "invalid value", - else => try std.fmt.allocPrintZ( + else => try std.fmt.allocPrintSentinel( arena_alloc, "unknown error {}", .{err}, + 0, ), }; @@ -235,14 +236,16 @@ fn formatValueRequired( comptime T: type, arena_alloc: std.mem.Allocator, key: []const u8, -) std.mem.Allocator.Error![:0]const u8 { - var buf = std.ArrayList(u8).init(arena_alloc); - errdefer buf.deinit(); - const writer = buf.writer(); +) std.Io.Writer.Error![:0]const u8 { + var stream: std.Io.Writer.Allocating = .init(arena_alloc); + const writer = &stream.writer; + try writer.print("value required", .{}); try formatValues(T, key, writer); try writer.writeByte(0); - return buf.items[0 .. buf.items.len - 1 :0]; + + const written = stream.written(); + return written[0 .. written.len - 1 :0]; } fn formatInvalidValue( @@ -250,17 +253,23 @@ fn formatInvalidValue( arena_alloc: std.mem.Allocator, key: []const u8, value: ?[]const u8, -) std.mem.Allocator.Error![:0]const u8 { - var buf = std.ArrayList(u8).init(arena_alloc); - errdefer buf.deinit(); - const writer = buf.writer(); +) std.Io.Writer.Error![:0]const u8 { + var stream: std.Io.Writer.Allocating = .init(arena_alloc); + const writer = &stream.writer; + try writer.print("invalid value \"{?s}\"", .{value}); try formatValues(T, key, writer); try writer.writeByte(0); - return buf.items[0 .. buf.items.len - 1 :0]; + + const written = stream.written(); + return written[0 .. written.len - 1 :0]; } -fn formatValues(comptime T: type, key: []const u8, writer: anytype) std.mem.Allocator.Error!void { +fn formatValues( + comptime T: type, + key: []const u8, + writer: *std.Io.Writer, +) std.Io.Writer.Error!void { @setEvalBranchQuota(2000); const typeinfo = @typeInfo(T); inline for (typeinfo.@"struct".fields) |f| { @@ -324,7 +333,7 @@ pub fn parseIntoField( return; } const raw = field.default_value_ptr orelse break :default; - const ptr: *const field.type = @alignCast(@ptrCast(raw)); + const ptr: *const field.type = @ptrCast(@alignCast(raw)); @field(dst, field.name) = ptr.*; return; } @@ -507,13 +516,18 @@ pub fn parseTaggedUnion(comptime T: type, alloc: Allocator, v: []const u8) !T { fn parseStruct(comptime T: type, alloc: Allocator, v: []const u8) !T { return switch (@typeInfo(T).@"struct".layout) { - .auto => parseAutoStruct(T, alloc, v), + .auto => parseAutoStruct(T, alloc, v, null), .@"packed" => parsePackedStruct(T, v), else => @compileError("unsupported struct layout"), }; } -pub fn parseAutoStruct(comptime T: type, alloc: Allocator, v: []const u8) !T { +pub fn parseAutoStruct( + comptime T: type, + alloc: Allocator, + v: []const u8, + default_: ?T, +) !T { const info = @typeInfo(T).@"struct"; comptime assert(info.layout == .auto); @@ -537,8 +551,8 @@ pub fn parseAutoStruct(comptime T: type, alloc: Allocator, v: []const u8) !T { const key = std.mem.trim(u8, entry[0..idx], whitespace); // used if we need to decode a double-quoted string. - var buf: std.ArrayListUnmanaged(u8) = .empty; - defer buf.deinit(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); const value = value: { const value = std.mem.trim(u8, entry[idx + 1 ..], whitespace); @@ -549,10 +563,9 @@ pub fn parseAutoStruct(comptime T: type, alloc: Allocator, v: []const u8) !T { value[value.len - 1] == '"') { // Decode a double-quoted string as a Zig string literal. - const writer = buf.writer(alloc); - const parsed = try std.zig.string_literal.parseWrite(writer, value); + const parsed = try std.zig.string_literal.parseWrite(&buf.writer, value); if (parsed == .failure) return error.InvalidValue; - break :value buf.items; + break :value buf.written(); } break :value value; @@ -573,9 +586,18 @@ pub fn parseAutoStruct(comptime T: type, alloc: Allocator, v: []const u8) !T { // Ensure all required fields are set inline for (info.fields, 0..) |field, i| { if (!fields_set.isSet(i)) { - const default_ptr = field.default_value_ptr orelse return error.InvalidValue; - const typed_ptr: *const field.type = @alignCast(@ptrCast(default_ptr)); - @field(result, field.name) = typed_ptr.*; + @field(result, field.name) = default: { + // If we're given a default value then we inherit those. + // Otherwise we use the default values as specified by the + // struct. + if (default_) |default| { + break :default @field(default, field.name); + } else { + const default_ptr = field.default_value_ptr orelse return error.InvalidValue; + const typed_ptr: *const field.type = @ptrCast(@alignCast(default_ptr)); + break :default typed_ptr.*; + } + }; } } @@ -781,15 +803,13 @@ test "parse: diagnostic location" { } = .{}; defer if (data._arena) |arena| arena.deinit(); - var fbs = std.io.fixedBufferStream( + var r: std.Io.Reader = .fixed( \\a=42 \\what \\b=two ); - const r = fbs.reader(); - const Iter = LineIterator(@TypeOf(r)); - var iter: Iter = .{ .r = r, .filepath = "test" }; + var iter: LineIterator = .{ .r = &r, .filepath = "test" }; try parse(@TypeOf(data), testing.allocator, &data, &iter); try testing.expect(data._arena != null); try testing.expectEqualStrings("42", data.a); @@ -1370,115 +1390,119 @@ test "ArgsIterator" { /// Returns an iterator (implements "next") that reads CLI args by line. /// Each CLI arg is expected to be a single line. This is used to implement /// configuration files. -pub fn LineIterator(comptime ReaderType: type) type { - return struct { - const Self = @This(); +pub const LineIterator = struct { + const Self = @This(); - /// The maximum size a single line can be. We don't expect any - /// CLI arg to exceed this size. Can't wait to git blame this in - /// like 4 years and be wrong about this. - pub const MAX_LINE_SIZE = 4096; + /// The maximum size a single line can be. We don't expect any + /// CLI arg to exceed this size. Can't wait to git blame this in + /// like 4 years and be wrong about this. + pub const MAX_LINE_SIZE = 4096; - /// Our stateful reader. - r: ReaderType, + /// Our stateful reader. + r: *std.Io.Reader, - /// Filepath that is used for diagnostics. This is only used for - /// diagnostic messages so it can be formatted however you want. - /// It is prefixed to the messages followed by the line number. - filepath: []const u8 = "", + /// Filepath that is used for diagnostics. This is only used for + /// diagnostic messages so it can be formatted however you want. + /// It is prefixed to the messages followed by the line number. + filepath: []const u8 = "", - /// The current line that we're on. This is 1-indexed because - /// lines are generally 1-indexed in the real world. The value - /// can be zero if we haven't read any lines yet. - line: usize = 0, + /// The current line that we're on. This is 1-indexed because + /// lines are generally 1-indexed in the real world. The value + /// can be zero if we haven't read any lines yet. + line: usize = 0, - /// This is the buffer where we store the current entry that - /// is formatted to be compatible with the parse function. - entry: [MAX_LINE_SIZE]u8 = [_]u8{ '-', '-' } ++ ([_]u8{0} ** (MAX_LINE_SIZE - 2)), + /// This is the buffer where we store the current entry that + /// is formatted to be compatible with the parse function. + entry: [MAX_LINE_SIZE]u8 = [_]u8{ '-', '-' } ++ ([_]u8{0} ** (MAX_LINE_SIZE - 2)), - pub fn next(self: *Self) ?[]const u8 { - // TODO: detect "--" prefixed lines and give a friendlier error - const buf = buf: { - while (true) { - // Read the full line - var entry = self.r.readUntilDelimiterOrEof(self.entry[2..], '\n') catch |err| switch (err) { - inline else => |e| { - log.warn("cannot read from \"{s}\": {}", .{ self.filepath, e }); - return null; - }, - } orelse return null; + pub fn init(reader: *std.Io.Reader) Self { + return .{ .r = reader }; + } - // Increment our line counter - self.line += 1; + pub fn next(self: *Self) ?[]const u8 { + // First prime the reader. + // File readers at least are initialized with a size of 0, + // and this will actually prompt the reader to get the actual + // size of the file, which will be used in the EOF check below. + // + // This will also optimize reads down the line as we're + // more likely to beworking with buffered data. + self.r.fillMore() catch {}; - // Trim any whitespace (including CR) around it - const trim = std.mem.trim(u8, entry, whitespace ++ "\r"); - if (trim.len != entry.len) { - std.mem.copyForwards(u8, entry, trim); - entry = entry[0..trim.len]; - } + var writer: std.Io.Writer = .fixed(self.entry[2..]); - // Ignore blank lines and comments - if (entry.len == 0 or entry[0] == '#') continue; + var entry = while (self.r.seek != self.r.end) { + // Reset write head + writer.end = 0; - // Trim spaces around '=' - if (mem.indexOf(u8, entry, "=")) |idx| { - const key = std.mem.trim(u8, entry[0..idx], whitespace); - const value = value: { - var value = std.mem.trim(u8, entry[idx + 1 ..], whitespace); + _ = self.r.streamDelimiterEnding(&writer, '\n') catch |e| { + log.warn("cannot read from \"{s}\": {}", .{ self.filepath, e }); + return null; + }; + _ = self.r.discardDelimiterInclusive('\n') catch {}; - // Detect a quoted string. - if (value.len >= 2 and - value[0] == '"' and - value[value.len - 1] == '"') - { - // Trim quotes since our CLI args processor expects - // quotes to already be gone. - value = value[1 .. value.len - 1]; - } + var entry = writer.buffered(); + self.line += 1; - break :value value; - }; + // Trim any whitespace (including CR) around it + const trim = std.mem.trim(u8, entry, whitespace ++ "\r"); + if (trim.len != entry.len) { + std.mem.copyForwards(u8, entry, trim); + entry = entry[0..trim.len]; + } - const len = key.len + value.len + 1; - if (entry.len != len) { - std.mem.copyForwards(u8, entry, key); - entry[key.len] = '='; - std.mem.copyForwards(u8, entry[key.len + 1 ..], value); - entry = entry[0..len]; - } - } + // Ignore blank lines and comments + if (entry.len == 0 or entry[0] == '#') continue; + break entry; + } else return null; - break :buf entry; + if (mem.indexOf(u8, entry, "=")) |idx| { + const key = std.mem.trim(u8, entry[0..idx], whitespace); + const value = value: { + var value = std.mem.trim(u8, entry[idx + 1 ..], whitespace); + + // Detect a quoted string. + if (value.len >= 2 and + value[0] == '"' and + value[value.len - 1] == '"') + { + // Trim quotes since our CLI args processor expects + // quotes to already be gone. + value = value[1 .. value.len - 1]; } + + break :value value; }; - // We need to reslice so that we include our '--' at the beginning - // of our buffer so that we can trick the CLI parser to treat it - // as CLI args. - return self.entry[0 .. buf.len + 2]; + const len = key.len + value.len + 1; + if (entry.len != len) { + std.mem.copyForwards(u8, entry, key); + entry[key.len] = '='; + std.mem.copyForwards(u8, entry[key.len + 1 ..], value); + entry = entry[0..len]; + } } - /// Returns a location for a diagnostic message. - pub fn location( - self: *const Self, - alloc: Allocator, - ) Allocator.Error!?diags.Location { - // If we have no filepath then we have no location. - if (self.filepath.len == 0) return null; + // We need to reslice so that we include our '--' at the beginning + // of our buffer so that we can trick the CLI parser to treat it + // as CLI args. + return self.entry[0 .. entry.len + 2]; + } - return .{ .file = .{ - .path = try alloc.dupe(u8, self.filepath), - .line = self.line, - } }; - } - }; -} + /// Returns a location for a diagnostic message. + pub fn location( + self: *const Self, + alloc: Allocator, + ) Allocator.Error!?diags.Location { + // If we have no filepath then we have no location. + if (self.filepath.len == 0) return null; -// Constructs a LineIterator (see docs for that). -fn lineIterator(reader: anytype) LineIterator(@TypeOf(reader)) { - return .{ .r = reader }; -} + return .{ .file = .{ + .path = try alloc.dupe(u8, self.filepath), + .line = self.line, + } }; + } +}; /// An iterator valid for arg parsing from a slice. pub const SliceIterator = struct { @@ -1501,7 +1525,7 @@ pub fn sliceIterator(slice: []const []const u8) SliceIterator { test "LineIterator" { const testing = std.testing; - var fbs = std.io.fixedBufferStream( + var reader: std.Io.Reader = .fixed( \\A \\B=42 \\C @@ -1516,7 +1540,7 @@ test "LineIterator" { \\F= "value " ); - var iter = lineIterator(fbs.reader()); + var iter: LineIterator = .init(&reader); try testing.expectEqualStrings("--A", iter.next().?); try testing.expectEqualStrings("--B=42", iter.next().?); try testing.expectEqualStrings("--C", iter.next().?); @@ -1529,9 +1553,9 @@ test "LineIterator" { test "LineIterator end in newline" { const testing = std.testing; - var fbs = std.io.fixedBufferStream("A\n\n"); + var reader: std.Io.Reader = .fixed("A\n\n"); - var iter = lineIterator(fbs.reader()); + var iter: LineIterator = .init(&reader); try testing.expectEqualStrings("--A", iter.next().?); try testing.expectEqual(@as(?[]const u8, null), iter.next()); try testing.expectEqual(@as(?[]const u8, null), iter.next()); @@ -1539,9 +1563,9 @@ test "LineIterator end in newline" { test "LineIterator spaces around '='" { const testing = std.testing; - var fbs = std.io.fixedBufferStream("A = B\n\n"); + var reader: std.Io.Reader = .fixed("A = B\n\n"); - var iter = lineIterator(fbs.reader()); + var iter: LineIterator = .init(&reader); try testing.expectEqualStrings("--A=B", iter.next().?); try testing.expectEqual(@as(?[]const u8, null), iter.next()); try testing.expectEqual(@as(?[]const u8, null), iter.next()); @@ -1549,18 +1573,18 @@ test "LineIterator spaces around '='" { test "LineIterator no value" { const testing = std.testing; - var fbs = std.io.fixedBufferStream("A = \n\n"); + var reader: std.Io.Reader = .fixed("A = \n\n"); - var iter = lineIterator(fbs.reader()); + var iter: LineIterator = .init(&reader); try testing.expectEqualStrings("--A=", iter.next().?); try testing.expectEqual(@as(?[]const u8, null), iter.next()); } test "LineIterator with CRLF line endings" { const testing = std.testing; - var fbs = std.io.fixedBufferStream("A\r\nB = C\r\n"); + var reader: std.Io.Reader = .fixed("A\r\nB = C\r\n"); - var iter = lineIterator(fbs.reader()); + var iter: LineIterator = .init(&reader); try testing.expectEqualStrings("--A", iter.next().?); try testing.expectEqualStrings("--B=C", iter.next().?); try testing.expectEqual(@as(?[]const u8, null), iter.next()); diff --git a/src/cli/boo.zig b/src/cli/boo.zig index 72b282ef6..f96fd6282 100644 --- a/src/cli/boo.zig +++ b/src/cli/boo.zig @@ -6,7 +6,7 @@ const Allocator = std.mem.Allocator; const help_strings = @import("help_strings"); const vaxis = @import("vaxis"); -const framedata = @import("framedata"); +const framedata = @import("framedata").compressed; const vxfw = vaxis.vxfw; @@ -218,17 +218,20 @@ var frames: []const []const u8 = undefined; /// Decompress the frames into a slice of individual frames fn decompressFrames(gpa: Allocator) !void { - var fbs = std.io.fixedBufferStream(framedata.compressed); - var list = std.ArrayList(u8).init(gpa); + var src: std.Io.Reader = .fixed(framedata); - try std.compress.flate.decompress(fbs.reader(), list.writer()); - decompressed_data = try list.toOwnedSlice(); + // var buf: [std.compress.flate.max_window_len]u8 = undefined; + var decompress: std.compress.flate.Decompress = .init(&src, .raw, &.{}); - var frame_list = try std.ArrayList([]const u8).initCapacity(gpa, 235); + var out: std.Io.Writer.Allocating = .init(gpa); + _ = try decompress.reader.streamRemaining(&out.writer); + decompressed_data = try out.toOwnedSlice(); + + var frame_list: std.ArrayList([]const u8) = try .initCapacity(gpa, 235); var frame_iter = std.mem.splitScalar(u8, decompressed_data, '\x01'); while (frame_iter.next()) |frame| { - try frame_list.append(frame); + try frame_list.append(gpa, frame); } - frames = try frame_list.toOwnedSlice(); + frames = try frame_list.toOwnedSlice(gpa); } diff --git a/src/cli/crash_report.zig b/src/cli/crash_report.zig index c6a383563..f0940fdab 100644 --- a/src/cli/crash_report.zig +++ b/src/cli/crash_report.zig @@ -38,21 +38,35 @@ pub fn run(alloc_gpa: Allocator) !u8 { try args.parse(Options, alloc_gpa, &opts, &iter); } + var buffer: [1024]u8 = undefined; + var stdout_file: std.fs.File = .stdout(); + var stdout_writer = stdout_file.writer(&buffer); + const stdout = &stdout_writer.interface; + + const result = runInner(alloc, &stdout_file, stdout); + stdout.flush() catch {}; + return result; +} + +fn runInner( + alloc: Allocator, + stdout_file: *std.fs.File, + stdout: *std.Io.Writer, +) !u8 { const crash_dir = try crash.defaultDir(alloc); - var reports = std.ArrayList(crash.Report).init(alloc); + var reports: std.ArrayList(crash.Report) = .empty; + errdefer reports.deinit(alloc); var it = try crash_dir.iterator(); - while (try it.next()) |report| try reports.append(.{ + while (try it.next()) |report| try reports.append(alloc, .{ .name = try alloc.dupe(u8, report.name), .mtime = report.mtime, }); - const stdout = std.io.getStdOut(); - // If we have no reports, then we're done. If we have a tty then we // print a message, otherwise we do nothing. if (reports.items.len == 0) { - if (std.posix.isatty(stdout.handle)) { + if (std.posix.isatty(stdout_file.handle)) { try stdout.writeAll("No crash reports! 👻\n"); } return 0; @@ -60,16 +74,15 @@ pub fn run(alloc_gpa: Allocator) !u8 { std.mem.sort(crash.Report, reports.items, {}, lt); - const writer = stdout.writer(); for (reports.items) |report| { var buf: [128]u8 = undefined; const now = std.time.nanoTimestamp(); const diff = now - report.mtime; const since = if (diff <= 0) "now" else s: { const d = Config.Duration{ .duration = @intCast(diff) }; - break :s try std.fmt.bufPrint(&buf, "{s} ago", .{d.round(std.time.ns_per_s)}); + break :s try std.fmt.bufPrint(&buf, "{f} ago", .{d.round(std.time.ns_per_s)}); }; - try writer.print("{s} ({s})\n", .{ report.name, since }); + try stdout.print("{s} ({s})\n", .{ report.name, since }); } return 0; diff --git a/src/cli/diagnostics.zig b/src/cli/diagnostics.zig index 2c6cb3b30..2af8bb4f8 100644 --- a/src/cli/diagnostics.zig +++ b/src/cli/diagnostics.zig @@ -16,7 +16,7 @@ pub const Diagnostic = struct { message: [:0]const u8, /// Write the full user-friendly diagnostic message to the writer. - pub fn write(self: *const Diagnostic, writer: anytype) !void { + pub fn format(self: *const Diagnostic, writer: *std.Io.Writer) !void { switch (self.location) { .none => {}, .cli => |index| try writer.print("cli:{}:", .{index}), @@ -157,11 +157,14 @@ pub const DiagnosticList = struct { errdefer _ = self.list.pop(); if (comptime precompute_enabled) { - var buf = std.ArrayList(u8).init(alloc); - defer buf.deinit(); - try diag.write(buf.writer()); + var stream: std.Io.Writer.Allocating = .init(alloc); + defer stream.deinit(); + diag.format(&stream.writer) catch |err| switch (err) { + // WriteFailed in this instance can only mean an OOM + error.WriteFailed => return error.OutOfMemory, + }; - const owned: [:0]const u8 = try buf.toOwnedSliceSentinel(0); + const owned: [:0]const u8 = try stream.toOwnedSliceSentinel(0); errdefer alloc.free(owned); try self.precompute.messages.append(alloc, owned); diff --git a/src/cli/edit_config.zig b/src/cli/edit_config.zig index 116843037..37f961a44 100644 --- a/src/cli/edit_config.zig +++ b/src/cli/edit_config.zig @@ -30,9 +30,9 @@ pub const Options = struct { /// this yet. /// /// The filepath opened is the default user-specific configuration -/// file, which is typically located at `$XDG_CONFIG_HOME/ghostty/config`. +/// file, which is typically located at `$XDG_CONFIG_HOME/ghostty/config.ghostty`. /// On macOS, this may also be located at -/// `~/Library/Application Support/com.mitchellh.ghostty/config`. +/// `~/Library/Application Support/com.mitchellh.ghostty/config.ghostty`. /// On macOS, whichever path exists and is non-empty will be prioritized, /// prioritizing the Application Support directory if neither are /// non-empty. @@ -47,7 +47,9 @@ pub fn run(alloc: Allocator) !u8 { // not using `exec` anymore and because this command isn't performance // critical where setting up the defer cleanup is a problem. - const stderr = std.io.getStdErr().writer(); + var buffer: [1024]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&buffer); + const stderr = &stderr_writer.interface; var opts: Options = .{}; defer opts.deinit(); @@ -58,13 +60,20 @@ pub fn run(alloc: Allocator) !u8 { try args.parse(Options, alloc, &opts, &iter); } + const result = runInner(alloc, stderr); + // Flushing *shouldn't* fail but... + stderr.flush() catch {}; + return result; +} + +fn runInner(alloc: Allocator, stderr: *std.Io.Writer) !u8 { // We load the configuration once because that will write our // default configuration files to disk. We don't use the config. var config = try Config.load(alloc); defer config.deinit(); // Find the preferred path. - const path = try Config.preferredDefaultFilePath(alloc); + const path = try configpkg.preferredDefaultFilePath(alloc); defer alloc.free(path); // We don't currently support Windows because we use the exec syscall. @@ -133,23 +142,13 @@ pub fn run(alloc: Allocator) !u8 { // so this is not a big deal. comptime assert(builtin.link_libc); - var buf: std.ArrayListUnmanaged(u8) = .empty; - errdefer buf.deinit(alloc); - - const writer = buf.writer(alloc); - var shellescape: internal_os.ShellEscapeWriter(std.ArrayListUnmanaged(u8).Writer) = .init(writer); - var shellescapewriter = shellescape.writer(); - - try writer.writeAll(editor); - try writer.writeByte(' '); - try shellescapewriter.writeAll(path); - - const command = try buf.toOwnedSliceSentinel(alloc, 0); - defer alloc.free(command); - + const editorZ = try alloc.dupeZ(u8, editor); + defer alloc.free(editorZ); + const pathZ = try alloc.dupeZ(u8, path); + defer alloc.free(pathZ); const err = std.posix.execvpeZ( - "sh", - &.{ "sh", "-c", command }, + editorZ, + &.{ editorZ, pathZ }, std.c.environ, ); diff --git a/src/cli/ghostty.zig b/src/cli/ghostty.zig index adb715d68..f6ac7d93d 100644 --- a/src/cli/ghostty.zig +++ b/src/cli/ghostty.zig @@ -107,12 +107,18 @@ pub const Action = enum { // for all commands by just changing this one place. if (std.mem.eql(u8, field.name, @tagName(self))) { - const stdout = std.io.getStdOut().writer(); + var buffer: [1024]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; const text = @field(help_strings.Action, field.name) ++ "\n"; stdout.writeAll(text) catch |write_err| { std.log.warn("failed to write help text: {}\n", .{write_err}); break :err 1; }; + stdout.flush() catch |flush_err| { + std.log.warn("failed to flush help text: {}\n", .{flush_err}); + break :err 1; + }; break :err 0; } diff --git a/src/cli/help.zig b/src/cli/help.zig index 0528dc1c2..a2b4dde80 100644 --- a/src/cli/help.zig +++ b/src/cli/help.zig @@ -30,7 +30,9 @@ pub fn run(alloc: Allocator) !u8 { try args.parse(Options, alloc, &opts, &iter); } - const stdout = std.io.getStdOut().writer(); + var buffer: [2048]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; try stdout.writeAll( \\Usage: ghostty [+action] [options] \\ @@ -70,6 +72,7 @@ pub fn run(alloc: Allocator) !u8 { \\where `` is one of actions listed above. \\ ); + try stdout.flush(); return 0; } diff --git a/src/cli/list_actions.zig b/src/cli/list_actions.zig index 6f5ce06a2..682eed251 100644 --- a/src/cli/list_actions.zig +++ b/src/cli/list_actions.zig @@ -37,8 +37,15 @@ pub fn run(alloc: Allocator) !u8 { try args.parse(Options, alloc, &opts, &iter); } - const stdout = std.io.getStdOut().writer(); - try helpgen_actions.generate(stdout, .plaintext, opts.docs, std.heap.page_allocator); + var stdout: std.fs.File = .stdout(); + var buffer: [4096]u8 = undefined; + var stdout_writer = stdout.writer(&buffer); + try helpgen_actions.generate( + &stdout_writer.interface, + .plaintext, + opts.docs, + std.heap.page_allocator, + ); return 0; } diff --git a/src/cli/list_colors.zig b/src/cli/list_colors.zig index 63945de99..50c12a693 100644 --- a/src/cli/list_colors.zig +++ b/src/cli/list_colors.zig @@ -39,11 +39,9 @@ pub fn run(alloc: Allocator) !u8 { try args.parse(Options, alloc, &opts, &iter); } - const stdout = std.io.getStdOut(); - - var keys = std.ArrayList([]const u8).init(alloc); - defer keys.deinit(); - for (x11_color.map.keys()) |key| try keys.append(key); + var keys: std.ArrayList([]const u8) = .empty; + defer keys.deinit(alloc); + for (x11_color.map.keys()) |key| try keys.append(alloc, key); std.mem.sortUnstable([]const u8, keys.items, {}, struct { fn lessThan(_: void, lhs: []const u8, rhs: []const u8) bool { @@ -52,12 +50,15 @@ pub fn run(alloc: Allocator) !u8 { }.lessThan); // Despite being under the posix namespace, this also works on Windows as of zig 0.13.0 + var stdout: std.fs.File = .stdout(); if (tui.can_pretty_print and !opts.plain and std.posix.isatty(stdout.handle)) { var arena = std.heap.ArenaAllocator.init(alloc); defer arena.deinit(); return prettyPrint(arena.allocator(), keys.items); } else { - const writer = stdout.writer(); + var buffer: [4096]u8 = undefined; + var stdout_writer = stdout.writer(&buffer); + const writer = &stdout_writer.interface; for (keys.items) |name| { const rgb = x11_color.map.get(name).?; try writer.print("{s} = #{x:0>2}{x:0>2}{x:0>2}\n", .{ @@ -74,19 +75,17 @@ pub fn run(alloc: Allocator) !u8 { fn prettyPrint(alloc: Allocator, keys: [][]const u8) !u8 { // Set up vaxis - var tty = try vaxis.Tty.init(); + var buf: [1024]u8 = undefined; + var tty = try vaxis.Tty.init(&buf); defer tty.deinit(); var vx = try vaxis.init(alloc, .{}); - defer vx.deinit(alloc, tty.anyWriter()); + defer vx.deinit(alloc, tty.writer()); // We know we are ghostty, so let's enable mode 2027. Vaxis normally does this but you need an // event loop to auto-enable it. vx.caps.unicode = .unicode; - try tty.anyWriter().writeAll(vaxis.ctlseqs.unicode_set); - defer tty.anyWriter().writeAll(vaxis.ctlseqs.unicode_reset) catch {}; - - var buf_writer = tty.bufferedWriter(); - const writer = buf_writer.writer().any(); + try tty.writer().writeAll(vaxis.ctlseqs.unicode_set); + defer tty.writer().writeAll(vaxis.ctlseqs.unicode_reset) catch {}; const winsize: vaxis.Winsize = switch (builtin.os.tag) { // We use some default, it doesn't really matter for what @@ -100,7 +99,7 @@ fn prettyPrint(alloc: Allocator, keys: [][]const u8) !u8 { else => try vaxis.Tty.getWinsize(tty.fd), }; - try vx.resize(alloc, tty.anyWriter(), winsize); + try vx.resize(alloc, tty.writer(), winsize); const win = vx.window(); @@ -203,11 +202,8 @@ fn prettyPrint(alloc: Allocator, keys: [][]const u8) !u8 { } // output the data - try vx.prettyPrint(writer); + try vx.prettyPrint(tty.writer()); } - // be sure to flush! - try buf_writer.flush(); - return 0; } diff --git a/src/cli/list_fonts.zig b/src/cli/list_fonts.zig index 58246d3ad..396c4e8a6 100644 --- a/src/cli/list_fonts.zig +++ b/src/cli/list_fonts.zig @@ -77,7 +77,9 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { // Its possible to build Ghostty without font discovery! if (comptime font.Discover == void) { - const stderr = std.io.getStdErr().writer(); + var buffer: [1024]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&buffer); + const stderr = &stderr_writer.interface; try stderr.print( \\Ghostty was built without a font discovery mechanism. This is a compile-time \\option. Please review how Ghostty was built from source, contact the @@ -85,15 +87,18 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { , .{}, ); + try stderr.flush(); return 1; } - const stdout = std.io.getStdOut().writer(); + var buffer: [2048]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; // We'll be putting our fonts into a list categorized by family // so it is easier to read the output. - var families = std.ArrayList([]const u8).init(alloc); - var map = std.StringHashMap(std.ArrayListUnmanaged([]const u8)).init(alloc); + var families: std.ArrayList([]const u8) = .empty; + var map: std.StringHashMap(std.ArrayListUnmanaged([]const u8)) = .init(alloc); // Look up all available fonts var disco = font.Discover.init(); @@ -123,7 +128,7 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { const gop = try map.getOrPut(family); if (!gop.found_existing) { - try families.append(family); + try families.append(alloc, family); gop.value_ptr.* = .{}; } try gop.value_ptr.append(alloc, full_name); @@ -155,5 +160,6 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { try stdout.print("\n", .{}); } + try stdout.flush(); return 0; } diff --git a/src/cli/list_keybinds.zig b/src/cli/list_keybinds.zig index 94f445eea..a8899a4f5 100644 --- a/src/cli/list_keybinds.zig +++ b/src/cli/list_keybinds.zig @@ -64,27 +64,38 @@ pub fn run(alloc: Allocator) !u8 { var config = if (opts.default) try Config.default(alloc) else try Config.load(alloc); defer config.deinit(); - const stdout = std.io.getStdOut(); + var buffer: [1024]u8 = undefined; + const stdout: std.fs.File = .stdout(); + var stdout_writer = stdout.writer(&buffer); + const writer = &stdout_writer.interface; - // Despite being under the posix namespace, this also works on Windows as of zig 0.13.0 - if (tui.can_pretty_print and !opts.plain and std.posix.isatty(stdout.handle)) { + if (tui.can_pretty_print and !opts.plain and stdout.isTty()) { var arena = std.heap.ArenaAllocator.init(alloc); defer arena.deinit(); return prettyPrint(arena.allocator(), config.keybind); } else { try config.keybind.formatEntryDocs( - configpkg.entryFormatter("keybind", stdout.writer()), + configpkg.entryFormatter("keybind", writer), opts.docs, ); } + // Don't forget to flush! + try writer.flush(); return 0; } -const TriggerList = std.SinglyLinkedList(Binding.Trigger); +const TriggerNode = struct { + data: Binding.Trigger, + node: std.SinglyLinkedList.Node = .{}, + + pub fn get(node: *std.SinglyLinkedList.Node) *TriggerNode { + return @fieldParentPtr("node", node); + } +}; const ChordBinding = struct { - triggers: TriggerList, + triggers: std.SinglyLinkedList, action: Binding.Action, // Order keybinds based on various properties @@ -109,7 +120,8 @@ const ChordBinding = struct { const lhs_count: usize = blk: { var count: usize = 0; var maybe_trigger = lhs.triggers.first; - while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) { + while (maybe_trigger) |node| : (maybe_trigger = node.next) { + const trigger: *TriggerNode = .get(node); if (trigger.data.mods.super) count += 1; if (trigger.data.mods.ctrl) count += 1; if (trigger.data.mods.shift) count += 1; @@ -120,7 +132,8 @@ const ChordBinding = struct { const rhs_count: usize = blk: { var count: usize = 0; var maybe_trigger = rhs.triggers.first; - while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) { + while (maybe_trigger) |node| : (maybe_trigger = node.next) { + const trigger: *TriggerNode = .get(node); if (trigger.data.mods.super) count += 1; if (trigger.data.mods.ctrl) count += 1; if (trigger.data.mods.shift) count += 1; @@ -137,8 +150,8 @@ const ChordBinding = struct { var l_trigger = lhs.triggers.first; var r_trigger = rhs.triggers.first; while (l_trigger != null and r_trigger != null) { - const l_int = l_trigger.?.data.mods.int(); - const r_int = r_trigger.?.data.mods.int(); + const l_int = TriggerNode.get(l_trigger.?).data.mods.int(); + const r_int = TriggerNode.get(r_trigger.?).data.mods.int(); if (l_int != r_int) { return l_int > r_int; @@ -154,13 +167,13 @@ const ChordBinding = struct { while (l_trigger != null and r_trigger != null) { const lhs_key: c_int = blk: { - switch (l_trigger.?.data.key) { + switch (TriggerNode.get(l_trigger.?).data.key) { .physical => |key| break :blk @intFromEnum(key), .unicode => |key| break :blk @intCast(key), } }; const rhs_key: c_int = blk: { - switch (r_trigger.?.data.key) { + switch (TriggerNode.get(r_trigger.?).data.key) { .physical => |key| break :blk @intFromEnum(key), .unicode => |key| break :blk @intCast(key), } @@ -186,19 +199,18 @@ const ChordBinding = struct { fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { // Set up vaxis - var tty = try vaxis.Tty.init(); + var buf: [1024]u8 = undefined; + var tty = try vaxis.Tty.init(&buf); defer tty.deinit(); var vx = try vaxis.init(alloc, .{}); - defer vx.deinit(alloc, tty.anyWriter()); + const writer = tty.writer(); + defer vx.deinit(alloc, writer); // We know we are ghostty, so let's enable mode 2027. Vaxis normally does this but you need an // event loop to auto-enable it. vx.caps.unicode = .unicode; - try tty.anyWriter().writeAll(vaxis.ctlseqs.unicode_set); - defer tty.anyWriter().writeAll(vaxis.ctlseqs.unicode_reset) catch {}; - - var buf_writer = tty.bufferedWriter(); - const writer = buf_writer.writer().any(); + try writer.writeAll(vaxis.ctlseqs.unicode_set); + defer writer.writeAll(vaxis.ctlseqs.unicode_reset) catch {}; const winsize: vaxis.Winsize = switch (builtin.os.tag) { // We use some default, it doesn't really matter for what @@ -212,7 +224,7 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { else => try vaxis.Tty.getWinsize(tty.fd), }; - try vx.resize(alloc, tty.anyWriter(), winsize); + try vx.resize(alloc, writer, winsize); const win = vx.window(); @@ -234,7 +246,9 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false }; var maybe_trigger = bind.triggers.first; - while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) { + while (maybe_trigger) |node| : (maybe_trigger = node.next) { + const trigger: *TriggerNode = .get(node); + if (trigger.data.mods.super) { result = win.printSegment(.{ .text = "super", .style = super_style }, .{ .col_offset = result.col }); result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col }); @@ -252,18 +266,18 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col }); } const key = switch (trigger.data.key) { - .physical => |k| try std.fmt.allocPrint(alloc, "{s}", .{@tagName(k)}), + .physical => |k| try std.fmt.allocPrint(alloc, "{t}", .{k}), .unicode => |c| try std.fmt.allocPrint(alloc, "{u}", .{c}), }; result = win.printSegment(.{ .text = key }, .{ .col_offset = result.col }); // Print a separator between chorded keys - if (trigger.next != null) { + if (trigger.node.next != null) { result = win.printSegment(.{ .text = " > ", .style = .{ .bold = true, .fg = .{ .index = 6 } } }, .{ .col_offset = result.col }); } } - const action = try std.fmt.allocPrint(alloc, "{}", .{bind.action}); + 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(&.{ @@ -276,29 +290,33 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { } try vx.prettyPrint(writer); } - try buf_writer.flush(); + try writer.flush(); return 0; } -fn iterateBindings(alloc: Allocator, iter: anytype, win: *const vaxis.Window) !struct { []ChordBinding, u16 } { +fn iterateBindings( + alloc: Allocator, + iter: anytype, + win: *const vaxis.Window, +) !struct { []ChordBinding, u16 } { var widest_chord: u16 = 0; - var bindings = std.ArrayList(ChordBinding).init(alloc); + var bindings: std.ArrayList(ChordBinding) = .empty; while (iter.next()) |bind| { const width = blk: { - var buf = std.ArrayList(u8).init(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); const t = bind.key_ptr.*; - if (t.mods.super) try std.fmt.format(buf.writer(), "super + ", .{}); - if (t.mods.ctrl) try std.fmt.format(buf.writer(), "ctrl + ", .{}); - if (t.mods.alt) try std.fmt.format(buf.writer(), "alt + ", .{}); - if (t.mods.shift) try std.fmt.format(buf.writer(), "shift + ", .{}); + if (t.mods.super) try buf.writer.print("super + ", .{}); + if (t.mods.ctrl) try buf.writer.print("ctrl + ", .{}); + if (t.mods.alt) try buf.writer.print("alt + ", .{}); + if (t.mods.shift) try buf.writer.print("shift + ", .{}); switch (t.key) { - .physical => |k| try std.fmt.format(buf.writer(), "{s}", .{@tagName(k)}), - .unicode => |c| try std.fmt.format(buf.writer(), "{u}", .{c}), + .physical => |k| try buf.writer.print("{t}", .{k}), + .unicode => |c| try buf.writer.print("{u}", .{c}), } - break :blk win.gwidth(buf.items); + break :blk win.gwidth(buf.written()); }; switch (bind.value_ptr.*) { @@ -310,28 +328,28 @@ fn iterateBindings(alloc: Allocator, iter: anytype, win: *const vaxis.Window) !s // Prepend the current keybind onto the list of sub-binds for (sub_bindings) |*nb| { - const prepend_node = try alloc.create(TriggerList.Node); - prepend_node.* = TriggerList.Node{ .data = bind.key_ptr.* }; - nb.triggers.prepend(prepend_node); + const prepend_node = try alloc.create(TriggerNode); + prepend_node.* = .{ .data = bind.key_ptr.* }; + nb.triggers.prepend(&prepend_node.node); } // Add the longest sub-bind width to the current bind width along with a padding // of 5 for the ' > ' spacer widest_chord = @max(widest_chord, width + max_width + 5); - try bindings.appendSlice(sub_bindings); + try bindings.appendSlice(alloc, sub_bindings); }, .leaf => |leaf| { - const node = try alloc.create(TriggerList.Node); - node.* = TriggerList.Node{ .data = bind.key_ptr.* }; - const triggers = TriggerList{ - .first = node, - }; + const node = try alloc.create(TriggerNode); + node.* = .{ .data = bind.key_ptr.* }; widest_chord = @max(widest_chord, width); - try bindings.append(.{ .triggers = triggers, .action = leaf.action }); + try bindings.append(alloc, .{ + .triggers = .{ .first = &node.node }, + .action = leaf.action, + }); }, } } - return .{ try bindings.toOwnedSlice(), widest_chord }; + return .{ try bindings.toOwnedSlice(alloc), widest_chord }; } diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig index 0c0acfe84..63184ddfb 100644 --- a/src/cli/list_themes.zig +++ b/src/cli/list_themes.zig @@ -57,9 +57,12 @@ const ThemeListElement = struct { .host = .{ .raw = "" }, .path = .{ .raw = self.path }, }; - var buf = std.ArrayList(u8).init(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); errdefer buf.deinit(); - try uri.writeToStream(.{ .scheme = true, .authority = true, .path = true }, buf.writer()); + try uri.writeToStream( + &buf.writer, + .{ .scheme = true, .authority = true, .path = true }, + ); return buf.toOwnedSlice(); } }; @@ -114,8 +117,14 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 { var arena = std.heap.ArenaAllocator.init(gpa_alloc); const alloc = arena.allocator(); - const stderr = std.io.getStdErr().writer(); - const stdout = std.io.getStdOut().writer(); + var stdout_buf: [4096]u8 = undefined; + var stdout_file: std.fs.File = .stdout(); + var stdout_writer = stdout_file.writer(&stdout_buf); + const stdout = &stdout_writer.interface; + + var stderr_buf: [4096]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&stderr_buf); + const stderr = &stderr_writer.interface; const resources_dir = global_state.resources_dir.app(); if (resources_dir == null) @@ -124,9 +133,9 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 { var count: usize = 0; - var themes = std.ArrayList(ThemeListElement).init(alloc); + var themes: std.ArrayList(ThemeListElement) = .empty; - var it = themepkg.LocationIterator{ .arena_alloc = arena.allocator() }; + var it: themepkg.LocationIterator = .{ .arena_alloc = arena.allocator() }; while (try it.next()) |loc| { var dir = std.fs.cwd().openDir(loc.dir, .{ .iterate = true }) catch |err| switch (err) { @@ -148,7 +157,7 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 { count += 1; const path = try std.fs.path.join(alloc, &.{ loc.dir, entry.name }); - try themes.append(.{ + try themes.append(alloc, .{ .path = path, .location = loc.location, .theme = try alloc.dupe(u8, entry.name), @@ -166,18 +175,20 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 { std.mem.sortUnstable(ThemeListElement, themes.items, {}, ThemeListElement.lessThan); - if (tui.can_pretty_print and !opts.plain and std.posix.isatty(std.io.getStdOut().handle)) { + if (tui.can_pretty_print and !opts.plain and stdout_file.isTty()) { try preview(gpa_alloc, themes.items, opts.color); return 0; } for (themes.items) |theme| { if (opts.path) - try stdout.print("{s} ({s}) {s}\n", .{ theme.theme, @tagName(theme.location), theme.path }) + try stdout.print("{s} ({t}) {s}\n", .{ theme.theme, theme.location, theme.path }) else - try stdout.print("{s} ({s})\n", .{ theme.theme, @tagName(theme.location) }); + try stdout.print("{s} ({t})\n", .{ theme.theme, theme.location }); } + // Don't forget to flush! + try stdout.flush(); return 0; } @@ -209,23 +220,28 @@ const Preview = struct { text_input: vaxis.widgets.TextInput, theme_filter: ColorScheme, - pub fn init(allocator: std.mem.Allocator, themes: []ThemeListElement, theme_filter: ColorScheme) !*Preview { + pub fn init( + allocator: std.mem.Allocator, + themes: []ThemeListElement, + theme_filter: ColorScheme, + buf: []u8, + ) !*Preview { const self = try allocator.create(Preview); self.* = .{ .allocator = allocator, .should_quit = false, - .tty = try vaxis.Tty.init(), + .tty = try .init(buf), .vx = try vaxis.init(allocator, .{}), .mouse = null, .themes = themes, - .filtered = try std.ArrayList(usize).initCapacity(allocator, themes.len), + .filtered = try .initCapacity(allocator, themes.len), .current = 0, .window = 0, .hex = false, .mode = .normal, .color_scheme = .light, - .text_input = vaxis.widgets.TextInput.init(allocator, &self.vx.unicode), + .text_input = .init(allocator), .theme_filter = theme_filter, }; @@ -236,9 +252,9 @@ const Preview = struct { pub fn deinit(self: *Preview) void { const allocator = self.allocator; - self.filtered.deinit(); + self.filtered.deinit(allocator); self.text_input.deinit(); - self.vx.deinit(allocator, self.tty.anyWriter()); + self.vx.deinit(allocator, self.tty.writer()); self.tty.deinit(); allocator.destroy(self); } @@ -251,12 +267,14 @@ const Preview = struct { try loop.init(); try loop.start(); - try self.vx.enterAltScreen(self.tty.anyWriter()); - try self.vx.setTitle(self.tty.anyWriter(), "👻 Ghostty Theme Preview 👻"); - try self.vx.queryTerminal(self.tty.anyWriter(), 1 * std.time.ns_per_s); - try self.vx.setMouseMode(self.tty.anyWriter(), true); + const writer = self.tty.writer(); + + try self.vx.enterAltScreen(writer); + try self.vx.setTitle(writer, "👻 Ghostty Theme Preview 👻"); + try self.vx.queryTerminal(writer, 1 * std.time.ns_per_s); + try self.vx.setMouseMode(writer, true); if (self.vx.caps.color_scheme_updates) - try self.vx.subscribeToColorSchemeUpdates(self.tty.anyWriter()); + try self.vx.subscribeToColorSchemeUpdates(writer); while (!self.should_quit) { var arena = std.heap.ArenaAllocator.init(self.allocator); @@ -269,9 +287,8 @@ const Preview = struct { } try self.draw(alloc); - var buffered = self.tty.bufferedWriter(); - try self.vx.render(buffered.writer().any()); - try buffered.flush(); + try self.vx.render(writer); + try writer.flush(); } } @@ -308,11 +325,11 @@ const Preview = struct { const string = try std.ascii.allocLowerString(self.allocator, buffer); defer self.allocator.free(string); - var tokens = std.ArrayList([]const u8).init(self.allocator); - defer tokens.deinit(); + var tokens: std.ArrayList([]const u8) = .empty; + defer tokens.deinit(self.allocator); var it = std.mem.tokenizeScalar(u8, string, ' '); - while (it.next()) |token| try tokens.append(token); + while (it.next()) |token| try tokens.append(self.allocator, token); for (self.themes, 0..) |*theme, i| { try theme_config.loadFile(theme_config._arena.?.allocator(), theme.path); @@ -322,13 +339,13 @@ const Preview = struct { .to_lower = true, .plain = true, }); - if (theme.rank != null) try self.filtered.append(i); + if (theme.rank != null) try self.filtered.append(self.allocator, i); } } else { for (self.themes, 0..) |*theme, i| { try theme_config.loadFile(theme_config._arena.?.allocator(), theme.path); if (shouldIncludeTheme(self.theme_filter, theme_config)) { - try self.filtered.append(i); + try self.filtered.append(self.allocator, i); theme.rank = null; } } @@ -421,13 +438,13 @@ const Preview = struct { self.hex = false; if (key.matches('c', .{})) try self.vx.copyToSystemClipboard( - self.tty.anyWriter(), + self.tty.writer(), self.themes[self.filtered.items[self.current]].theme, alloc, ) else if (key.matches('c', .{ .shift = true })) try self.vx.copyToSystemClipboard( - self.tty.anyWriter(), + self.tty.writer(), self.themes[self.filtered.items[self.current]].path, alloc, ); @@ -471,7 +488,7 @@ const Preview = struct { }, .color_scheme => |color_scheme| self.color_scheme = color_scheme, .mouse => |mouse| self.mouse = mouse, - .winsize => |ws| try self.vx.resize(self.allocator, self.tty.anyWriter(), ws), + .winsize => |ws| try self.vx.resize(self.allocator, self.tty.writer(), ws), } } @@ -1044,14 +1061,14 @@ const Preview = struct { ); } - var buf = std.ArrayList(u8).init(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); defer buf.deinit(); for (config._diagnostics.items(), 0..) |diag, captured_i| { const i: u16 = @intCast(captured_i); - try diag.write(buf.writer()); + try diag.format(&buf.writer); _ = child.printSegment( .{ - .text = buf.items, + .text = buf.written(), .style = self.ui_err(), }, .{ @@ -1319,7 +1336,7 @@ const Preview = struct { .{ .text = "const ", .style = color5 }, .{ .text = "stdout ", .style = standard }, .{ .text = "=", .style = color5 }, - .{ .text = " std.io.getStdOut().writer();", .style = standard }, + .{ .text = " std.Io.getStdOut().writer();", .style = standard }, }, .{ .row_offset = 7, @@ -1651,7 +1668,13 @@ fn color(config: Config, palette: usize) vaxis.Color { const lorem_ipsum = @embedFile("lorem_ipsum.txt"); fn preview(allocator: std.mem.Allocator, themes: []ThemeListElement, theme_filter: ColorScheme) !void { - var app = try Preview.init(allocator, themes, theme_filter); + var buf: [4096]u8 = undefined; + var app = try Preview.init( + allocator, + themes, + theme_filter, + &buf, + ); defer app.deinit(); try app.run(); } diff --git a/src/cli/new_window.zig b/src/cli/new_window.zig index 343175b4e..f3f4740d1 100644 --- a/src/cli/new_window.zig +++ b/src/cli/new_window.zig @@ -26,7 +26,7 @@ pub const Options = struct { // If it's not `-e` continue with the standard argument parsning. if (!std.mem.eql(u8, arg, "-e")) return true; - var arguments: std.ArrayListUnmanaged([:0]const u8) = .empty; + var arguments: std.ArrayList([:0]const u8) = .empty; errdefer { for (arguments.items) |argument| alloc.free(argument); arguments.deinit(alloc); @@ -99,12 +99,21 @@ pub const Options = struct { pub fn run(alloc: Allocator) !u8 { var iter = try args.argsIterator(alloc); defer iter.deinit(); - return try runArgs(alloc, &iter); + + var buffer: [1024]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&buffer); + const stderr = &stderr_writer.interface; + + const result = runArgs(alloc, &iter, stderr); + stderr.flush() catch {}; + return result; } -fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { - const stderr = std.io.getStdErr().writer(); - +fn runArgs( + alloc_gpa: Allocator, + argsIter: anytype, + stderr: *std.Io.Writer, +) !u8 { var opts: Options = .{}; defer opts.deinit(); @@ -126,9 +135,7 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { inner: inline for (@typeInfo(Options).@"struct".fields) |field| { if (field.name[0] == '_') continue :inner; if (std.mem.eql(u8, field.name, diagnostic.key)) { - try stderr.writeAll("config error: "); - try diagnostic.write(stderr); - try stderr.writeAll("\n"); + try stderr.print("config error: {f}\n", .{diagnostic}); exit = true; } } diff --git a/src/cli/show_config.zig b/src/cli/show_config.zig index 3f22c75c2..1b73b77c1 100644 --- a/src/cli/show_config.zig +++ b/src/cli/show_config.zig @@ -77,7 +77,10 @@ pub fn run(alloc: Allocator) !u8 { // For some reason `std.fmt.format` isn't working here but it works in // tests so we just do configfmt.format. - const stdout = std.io.getStdOut().writer(); - try configfmt.format("", .{}, stdout); + var stdout: std.fs.File = .stdout(); + var buffer: [4096]u8 = undefined; + var stdout_writer = stdout.writer(&buffer); + try configfmt.format(&stdout_writer.interface); + try stdout_writer.end(); return 0; } diff --git a/src/cli/show_face.zig b/src/cli/show_face.zig index e3b596bcd..9dee777b3 100644 --- a/src/cli/show_face.zig +++ b/src/cli/show_face.zig @@ -64,13 +64,32 @@ pub const Options = struct { pub fn run(alloc: Allocator) !u8 { var iter = try args.argsIterator(alloc); defer iter.deinit(); - return try runArgs(alloc, &iter); + + var stdout_buffer: [1024]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); + const stdout = &stdout_writer.interface; + + var stderr_buffer: [1024]u8 = undefined; + var stderr_writer = std.fs.File.stdout().writer(&stderr_buffer); + const stderr = &stderr_writer.interface; + + const result = runArgs( + alloc, + &iter, + stdout, + stderr, + ); + stdout.flush() catch {}; + stderr.flush() catch {}; + return result; } -fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { - const stdout = std.io.getStdOut().writer(); - const stderr = std.io.getStdErr().writer(); - +fn runArgs( + alloc_gpa: Allocator, + argsIter: anytype, + stdout: *std.Io.Writer, + stderr: *std.Io.Writer, +) !u8 { // Its possible to build Ghostty without font discovery! if (comptime font.Discover == void) { try stderr.print( @@ -104,9 +123,7 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { inner: inline for (@typeInfo(Options).@"struct".fields) |field| { if (field.name[0] == '_') continue :inner; if (std.mem.eql(u8, field.name, diagnostic.key)) { - try stderr.writeAll("config error: "); - try diagnostic.write(stderr); - try stderr.writeAll("\n"); + try stderr.print("config error: {f}\n", .{diagnostic}); exit = true; } } @@ -138,9 +155,7 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { if (field.name[0] == '_') continue :inner; if (std.mem.eql(u8, field.name, diagnostic.key) and (diagnostic.location == .none or diagnostic.location == .cli)) continue :outer; } - try stderr.writeAll("config error: "); - try diagnostic.write(stderr); - try stderr.writeAll("\n"); + try stderr.print("config error: {f}\n", .{diagnostic}); } } @@ -189,8 +204,8 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { fn lookup( alloc: std.mem.Allocator, - stdout: anytype, - stderr: anytype, + stdout: *std.Io.Writer, + stderr: *std.Io.Writer, font_grid: *font.SharedGrid, style: font.Style, presentation: ?font.Presentation, diff --git a/src/cli/ssh-cache/DiskCache.zig b/src/cli/ssh-cache/DiskCache.zig index db138cf37..25d2cd42e 100644 --- a/src/cli/ssh-cache/DiskCache.zig +++ b/src/cli/ssh-cache/DiskCache.zig @@ -7,8 +7,9 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; -const xdg = @import("../../os/main.zig").xdg; -const TempDir = @import("../../os/main.zig").TempDir; +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 @@ -57,8 +58,6 @@ pub fn clear(self: DiskCache) !void { pub const AddResult = enum { added, updated }; -pub const AddError = std.fs.Dir.MakeError || std.fs.File.OpenError || std.fs.File.LockError || std.fs.File.ReadError || std.fs.File.WriteError || std.posix.RealPathError || std.posix.RenameError || Allocator.Error || error{ HostnameIsInvalid, CacheIsLocked }; - /// Add or update a hostname entry in the cache. /// Returns AddResult.added for new entries or AddResult.updated for existing ones. /// The cache file is created if it doesn't exist with secure permissions (0600). @@ -66,7 +65,7 @@ pub fn add( self: DiskCache, alloc: Allocator, hostname: []const u8, -) AddError!AddResult { +) !AddResult { if (!isValidCacheKey(hostname)) return error.HostnameIsInvalid; // Create cache directory if needed @@ -130,15 +129,13 @@ pub fn add( return result; } -pub const RemoveError = std.fs.Dir.OpenError || std.fs.File.OpenError || std.fs.File.ReadError || std.fs.File.WriteError || std.posix.RealPathError || std.posix.RenameError || Allocator.Error || error{ HostnameIsInvalid, CacheIsLocked }; - /// Remove a hostname entry from the cache. /// No error is returned if the hostname doesn't exist or the cache file is missing. pub fn remove( self: DiskCache, alloc: Allocator, hostname: []const u8, -) RemoveError!void { +) !void { if (!isValidCacheKey(hostname)) return error.HostnameIsInvalid; // Open our file @@ -199,7 +196,7 @@ pub fn contains( return entries.contains(hostname); } -fn fixupPermissions(file: std.fs.File) !void { +fn fixupPermissions(file: std.fs.File) (std.fs.File.StatError || std.fs.File.ChmodError)!void { // Windows does not support chmod if (comptime builtin.os.tag == .windows) return; @@ -211,14 +208,12 @@ fn fixupPermissions(file: std.fs.File) !void { } } -pub const WriteCacheFileError = std.fs.Dir.OpenError || std.fs.File.OpenError || std.fs.File.WriteError || std.fs.Dir.RealPathAllocError || std.posix.RealPathError || std.posix.RenameError || error{FileTooBig}; - fn writeCacheFile( self: DiskCache, alloc: Allocator, entries: std.StringHashMap(Entry), expire_days: ?u32, -) WriteCacheFileError!void { +) !void { var td: TempDir = try .init(); defer td.deinit(); @@ -227,14 +222,18 @@ fn writeCacheFile( const tmp_path = try td.dir.realpathAlloc(alloc, "ssh-cache"); defer alloc.free(tmp_path); - const writer = tmp_file.writer(); + var buf: [1024]u8 = undefined; + var writer = tmp_file.writer(&buf); 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); + try kv.value_ptr.format(&writer.interface); } + // Don't forget to flush!! + try writer.interface.flush(); + // Atomic replace try std.fs.renameAbsolute(tmp_path, self.path); } @@ -278,8 +277,12 @@ pub fn deinitEntries( fn readEntries( alloc: Allocator, file: std.fs.File, -) (std.fs.File.ReadError || Allocator.Error || error{FileTooBig})!std.StringHashMap(Entry) { - const content = try file.readToEndAlloc(alloc, MAX_CACHE_SIZE); +) !std.StringHashMap(Entry) { + var reader = file.reader(&.{}); + const content = try reader.interface.allocRemaining( + alloc, + .limited(MAX_CACHE_SIZE), + ); defer alloc.free(content); var entries = std.StringHashMap(Entry).init(alloc); @@ -332,48 +335,28 @@ fn isValidCacheKey(key: []const u8) bool { if (std.mem.indexOf(u8, key, "@")) |at_pos| { const user = key[0..at_pos]; const hostname = key[at_pos + 1 ..]; - return isValidUser(user) and isValidHostname(hostname); + return isValidUser(user) and isValidHost(hostname); } - return isValidHostname(key); + return isValidHost(key); } -// Basic hostname validation - accepts domains and IPs -// (including IPv6 in brackets) -fn isValidHostname(host: []const u8) bool { - if (host.len == 0 or host.len > 253) return false; - - // Handle IPv6 addresses in brackets - if (host.len >= 4 and host[0] == '[' and host[host.len - 1] == ']') { - const ipv6_part = host[1 .. host.len - 1]; - if (ipv6_part.len == 0) return false; - var has_colon = false; - for (ipv6_part) |c| { - switch (c) { - 'a'...'f', 'A'...'F', '0'...'9' => {}, - ':' => has_colon = true, - else => return false, - } - } - return has_colon; +// Checks if a host is a valid hostname or IP address +fn isValidHost(host: []const u8) bool { + // First check for valid hostnames because this is assumed to be the more + // likely ssh host format. + if (internal_os.hostname.isValid(host)) { + return true; } - // Standard hostname/domain validation - for (host) |c| { - switch (c) { - 'a'...'z', 'A'...'Z', '0'...'9', '.', '-' => {}, - else => return false, - } - } - - // No leading/trailing dots or hyphens, no consecutive dots - if (host[0] == '.' or host[0] == '-' or - host[host.len - 1] == '.' or host[host.len - 1] == '-') - { + // We also accept valid IP addresses. In practice, IPv4 addresses are also + // considered valid hostnames due to their overlapping syntax, so we can + // simplify this check to be IPv6-specific. + if (std.net.Address.parseIp6(host, 0)) |_| { + return true; + } else |_| { return false; } - - return std.mem.indexOf(u8, host, "..") == null; } fn isValidUser(user: []const u8) bool { @@ -403,10 +386,12 @@ test "disk cache clear" { // Create our path var td: TempDir = try .init(); defer td.deinit(); + var buf: [4096]u8 = undefined; { var file = try td.dir.createFile("cache", .{}); defer file.close(); - try file.writer().writeAll("HELLO!"); + var file_writer = file.writer(&buf); + try file_writer.interface.writeAll("HELLO!"); } const path = try td.dir.realpathAlloc(alloc, "cache"); defer alloc.free(path); @@ -429,10 +414,14 @@ test "disk cache operations" { // Create our path var td: TempDir = try .init(); defer td.deinit(); + var buf: [4096]u8 = undefined; { var file = try td.dir.createFile("cache", .{}); defer file.close(); - try file.writer().writeAll("HELLO!"); + 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"); defer alloc.free(path); @@ -467,42 +456,36 @@ test "disk cache operations" { } // Tests -test "hostname validation - valid cases" { - const testing = std.testing; - try testing.expect(isValidHostname("example.com")); - try testing.expect(isValidHostname("sub.example.com")); - try testing.expect(isValidHostname("host-name.domain.org")); - try testing.expect(isValidHostname("192.168.1.1")); - try testing.expect(isValidHostname("a")); - try testing.expect(isValidHostname("1")); -} -test "hostname validation - IPv6 addresses" { +test isValidHost { const testing = std.testing; - try testing.expect(isValidHostname("[::1]")); - try testing.expect(isValidHostname("[2001:db8::1]")); - try testing.expect(!isValidHostname("[fe80::1%eth0]")); // Interface notation not supported - try testing.expect(!isValidHostname("[]")); // Empty IPv6 - try testing.expect(!isValidHostname("[invalid]")); // No colons -} -test "hostname validation - invalid cases" { - const testing = std.testing; - try testing.expect(!isValidHostname("")); - try testing.expect(!isValidHostname("host\nname")); - try testing.expect(!isValidHostname(".example.com")); - try testing.expect(!isValidHostname("example.com.")); - try testing.expect(!isValidHostname("host..domain")); - try testing.expect(!isValidHostname("-hostname")); - try testing.expect(!isValidHostname("hostname-")); - try testing.expect(!isValidHostname("host name")); - try testing.expect(!isValidHostname("host_name")); - try testing.expect(!isValidHostname("host@domain")); - try testing.expect(!isValidHostname("host:port")); + // Valid hostnames + try testing.expect(isValidHost("localhost")); + try testing.expect(isValidHost("example.com")); + try testing.expect(isValidHost("sub.example.com")); - // Too long - const long_host = "a" ** 254; - try testing.expect(!isValidHostname(long_host)); + // IPv4 addresses + try testing.expect(isValidHost("127.0.0.1")); + try testing.expect(isValidHost("192.168.1.1")); + + // IPv6 addresses + try testing.expect(isValidHost("::1")); + try testing.expect(isValidHost("2001:db8::1")); + try testing.expect(isValidHost("2001:db8:0:1:1:1:1:1")); + try testing.expect(!isValidHost("fe80::1%eth0")); // scopes not supported + + // Invalid hosts + try testing.expect(!isValidHost("")); + try testing.expect(!isValidHost("host\nname")); + try testing.expect(!isValidHost(".example.com")); + try testing.expect(!isValidHost("host..domain")); + try testing.expect(!isValidHost("-hostname")); + try testing.expect(!isValidHost("hostname-")); + try testing.expect(!isValidHost("host name")); + try testing.expect(!isValidHost("host_name")); + try testing.expect(!isValidHost("host@domain")); + try testing.expect(!isValidHost("host:port")); } test "user validation - valid cases" { @@ -543,7 +526,7 @@ test "cache key validation - hostname format" { try testing.expect(isValidCacheKey("example.com")); try testing.expect(isValidCacheKey("sub.example.com")); try testing.expect(isValidCacheKey("192.168.1.1")); - try testing.expect(isValidCacheKey("[::1]")); + try testing.expect(isValidCacheKey("::1")); try testing.expect(!isValidCacheKey("")); try testing.expect(!isValidCacheKey(".invalid.com")); } @@ -555,7 +538,7 @@ test "cache key validation - user@hostname format" { try testing.expect(isValidCacheKey("test-user@192.168.1.1")); try testing.expect(isValidCacheKey("user_name@host.domain.org")); try testing.expect(isValidCacheKey("git@github.com")); - try testing.expect(isValidCacheKey("ubuntu@[::1]")); + try testing.expect(isValidCacheKey("ubuntu@::1")); try testing.expect(!isValidCacheKey("@example.com")); try testing.expect(!isValidCacheKey("user@")); try testing.expect(!isValidCacheKey("user@@host")); diff --git a/src/cli/ssh-cache/Entry.zig b/src/cli/ssh-cache/Entry.zig index 3a691be80..f3403dbd4 100644 --- a/src/cli/ssh-cache/Entry.zig +++ b/src/cli/ssh-cache/Entry.zig @@ -33,7 +33,7 @@ pub fn parse(line: []const u8) ?Entry { }; } -pub fn format(self: Entry, writer: anytype) !void { +pub fn format(self: Entry, writer: *std.Io.Writer) !void { try writer.print( "{s}|{d}|{s}\n", .{ self.hostname, self.timestamp, self.terminfo_version }, diff --git a/src/cli/ssh_cache.zig b/src/cli/ssh_cache.zig index 1099f0112..9434e9771 100644 --- a/src/cli/ssh_cache.zig +++ b/src/cli/ssh_cache.zig @@ -61,9 +61,30 @@ pub fn run(alloc_gpa: Allocator) !u8 { try args.parse(Options, alloc_gpa, &opts, &iter); } - const stdout = std.io.getStdOut().writer(); - const stderr = std.io.getStdErr().writer(); + var stdout_buffer: [1024]u8 = undefined; + var stdout_file: std.fs.File = .stdout(); + var stdout_writer = stdout_file.writer(&stdout_buffer); + const stdout = &stdout_writer.interface; + var stderr_buffer: [1024]u8 = undefined; + var stderr_file: std.fs.File = .stderr(); + var stderr_writer = stderr_file.writer(&stderr_buffer); + const stderr = &stderr_writer.interface; + + const result = runInner(alloc, opts, stdout, stderr); + + // Flushing *shouldn't* fail but... + stdout.flush() catch {}; + stderr.flush() catch {}; + return result; +} + +pub fn runInner( + alloc: Allocator, + opts: Options, + stdout: *std.Io.Writer, + stderr: *std.Io.Writer, +) !u8 { // Setup our disk cache to the standard location const cache_path = try DiskCache.defaultPath(alloc, "ghostty"); const cache: DiskCache = .{ .path = cache_path }; @@ -165,7 +186,7 @@ pub fn run(alloc_gpa: Allocator) !u8 { fn listEntries( alloc: Allocator, entries: *const std.StringHashMap(Entry), - writer: anytype, + writer: *std.Io.Writer, ) !void { if (entries.count() == 0) { try writer.print("No hosts in cache.\n", .{}); @@ -173,12 +194,12 @@ fn listEntries( } // Sort entries by hostname for consistent output - var items = std.ArrayList(Entry).init(alloc); - defer items.deinit(); + var items: std.ArrayList(Entry) = .empty; + defer items.deinit(alloc); var iter = entries.iterator(); while (iter.next()) |kv| { - try items.append(kv.value_ptr.*); + try items.append(alloc, kv.value_ptr.*); } std.mem.sort(Entry, items.items, {}, struct { diff --git a/src/cli/validate_config.zig b/src/cli/validate_config.zig index 114843e9a..55d861402 100644 --- a/src/cli/validate_config.zig +++ b/src/cli/validate_config.zig @@ -40,8 +40,19 @@ pub fn run(alloc: std.mem.Allocator) !u8 { try args.parse(Options, alloc, &opts, &iter); } - const stdout = std.io.getStdOut().writer(); + var buffer: [1024]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const stdout = &stdout_writer.interface; + const result = runInner(alloc, opts, stdout); + try stdout_writer.end(); + return result; +} +fn runInner( + alloc: std.mem.Allocator, + opts: Options, + stdout: *std.Io.Writer, +) !u8 { var cfg = try Config.default(alloc); defer cfg.deinit(); @@ -58,15 +69,9 @@ pub fn run(alloc: std.mem.Allocator) !u8 { try cfg.finalize(); if (cfg._diagnostics.items().len > 0) { - var buf = std.ArrayList(u8).init(alloc); - defer buf.deinit(); - for (cfg._diagnostics.items()) |diag| { - try diag.write(buf.writer()); - try stdout.print("{s}\n", .{buf.items}); - buf.clearRetainingCapacity(); + try stdout.print("{f}\n", .{diag}); } - return 1; } diff --git a/src/cli/version.zig b/src/cli/version.zig index 22608fa88..cf8e66fa6 100644 --- a/src/cli/version.zig +++ b/src/cli/version.zig @@ -15,8 +15,12 @@ pub const Options = struct {}; /// The `version` command is used to display information about Ghostty. Recognized as /// either `+version` or `--version`. pub fn run(alloc: Allocator) !u8 { - const stdout = std.io.getStdOut().writer(); - const tty = std.io.getStdOut().isTty(); + var buffer: [1024]u8 = undefined; + const stdout_file: std.fs.File = .stdout(); + var stdout_writer = stdout_file.writer(&buffer); + + const stdout = &stdout_writer.interface; + const tty = stdout_file.isTty(); if (tty) if (build_config.version.build) |commit_hash| { try stdout.print( @@ -29,7 +33,7 @@ pub fn run(alloc: Allocator) !u8 { try stdout.print("Version\n", .{}); try stdout.print(" - version: {s}\n", .{build_config.version_string}); - try stdout.print(" - channel: {s}\n", .{@tagName(build_config.release_channel)}); + try stdout.print(" - channel: {t}\n", .{build_config.release_channel}); try stdout.print("Build Config\n", .{}); try stdout.print(" - Zig version : {s}\n", .{builtin.zig_version_string}); @@ -37,20 +41,20 @@ pub fn run(alloc: Allocator) !u8 { try stdout.print(" - app runtime : {}\n", .{build_config.app_runtime}); try stdout.print(" - font engine : {}\n", .{build_config.font_backend}); try stdout.print(" - renderer : {}\n", .{renderer.Renderer}); - try stdout.print(" - libxev : {s}\n", .{@tagName(xev.backend)}); + try stdout.print(" - libxev : {t}\n", .{xev.backend}); if (comptime build_config.app_runtime == .gtk) { if (comptime builtin.os.tag == .linux) { const kernel_info = internal_os.getKernelInfo(alloc); defer if (kernel_info) |k| alloc.free(k); try stdout.print(" - kernel version: {s}\n", .{kernel_info orelse "Kernel information unavailable"}); } - try stdout.print(" - desktop env : {s}\n", .{@tagName(internal_os.desktopEnvironment())}); + try stdout.print(" - desktop env : {t}\n", .{internal_os.desktopEnvironment()}); try stdout.print(" - GTK version :\n", .{}); - try stdout.print(" build : {}\n", .{gtk_version.comptime_version}); - try stdout.print(" runtime : {}\n", .{gtk_version.getRuntimeVersion()}); + try stdout.print(" build : {f}\n", .{gtk_version.comptime_version}); + try stdout.print(" runtime : {f}\n", .{gtk_version.getRuntimeVersion()}); try stdout.print(" - libadwaita : enabled\n", .{}); - try stdout.print(" build : {}\n", .{adw_version.comptime_version}); - try stdout.print(" runtime : {}\n", .{adw_version.getRuntimeVersion()}); + try stdout.print(" build : {f}\n", .{adw_version.comptime_version}); + try stdout.print(" runtime : {f}\n", .{adw_version.getRuntimeVersion()}); if (comptime build_options.x11) { try stdout.print(" - libX11 : enabled\n", .{}); } else { @@ -65,5 +69,8 @@ pub fn run(alloc: Allocator) !u8 { try stdout.print(" - libwayland : disabled\n", .{}); } } + + // Don't forget to flush! + try stdout.flush(); return 0; } diff --git a/src/config.zig b/src/config.zig index e83dff530..4abd319a6 100644 --- a/src/config.zig +++ b/src/config.zig @@ -1,5 +1,6 @@ const builtin = @import("builtin"); +const file_load = @import("config/file_load.zig"); const formatter = @import("config/formatter.zig"); pub const Config = @import("config/Config.zig"); pub const conditional = @import("config/conditional.zig"); @@ -12,6 +13,7 @@ pub const ConditionalState = conditional.State; pub const FileFormatter = formatter.FileFormatter; pub const entryFormatter = formatter.entryFormatter; pub const formatEntry = formatter.formatEntry; +pub const preferredDefaultFilePath = file_load.preferredDefaultFilePath; // Field types pub const BoldColor = Config.BoldColor; @@ -27,8 +29,8 @@ pub const FontStyle = Config.FontStyle; pub const FreetypeLoadFlags = Config.FreetypeLoadFlags; pub const Keybinds = Config.Keybinds; pub const MouseShiftCapture = Config.MouseShiftCapture; +pub const MouseScrollMultiplier = Config.MouseScrollMultiplier; pub const NonNativeFullscreen = Config.NonNativeFullscreen; -pub const OptionAsAlt = Config.OptionAsAlt; pub const RepeatableCodepointMap = Config.RepeatableCodepointMap; pub const RepeatableFontVariation = Config.RepeatableFontVariation; pub const RepeatableString = Config.RepeatableString; diff --git a/src/config/Config.zig b/src/config/Config.zig index 66e63fd3f..c9ae121e4 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -24,6 +24,7 @@ const cli = @import("../cli.zig"); const conditional = @import("conditional.zig"); const Conditional = conditional.Conditional; +const file_load = @import("file_load.zig"); const formatterpkg = @import("formatter.zig"); const themepkg = @import("theme.zig"); const url = @import("url.zig"); @@ -412,13 +413,13 @@ pub const compatibility = std.StaticStringMap( @"adjust-box-thickness": ?MetricModifier = null, /// Height in pixels or percentage adjustment of maximum height for nerd font icons. /// -/// Increasing this value will allow nerd font icons to be larger, but won't -/// necessarily force them to be. Decreasing this value will make nerd font -/// icons smaller. +/// A positive (negative) value will increase (decrease) the maximum icon +/// height. This may not affect all icons equally: the effect depends on whether +/// the default size of the icon is height-constrained, which in turn depends on +/// the aspect ratio of both the icon and your primary font. /// -/// The default value for the icon height is 1.2 times the height of capital -/// letters in your primary font, so something like -16.6% would make icons -/// roughly the same height as capital letters. +/// Certain icons designed for box drawing and terminal graphics, such as +/// Powerline symbols, are not affected by this option. /// /// See the notes about adjustments in `adjust-cell-width`. /// @@ -475,6 +476,11 @@ pub const compatibility = std.StaticStringMap( /// /// * `autohint` - Enable the freetype auto-hinter. Enabled by default. /// +/// * `light` - Use a light hinting style, better preserving glyph shapes. +/// This is the most common setting in GTK apps and therefore also Ghostty's +/// default. This has no effect if `monochrome` is enabled. Enabled by +/// default. +/// /// Example: `hinting`, `no-hinting`, `force-autohint`, `no-force-autohint` @"freetype-load-flags": FreetypeLoadFlags = .{}, @@ -833,14 +839,32 @@ palette: Palette = .{}, /// * `never` @"mouse-shift-capture": MouseShiftCapture = .false, -/// Multiplier for scrolling distance with the mouse wheel. Any value less -/// than 0.01 or greater than 10,000 will be clamped to the nearest valid -/// value. +/// Enable or disable mouse reporting. When set to `false`, mouse events will +/// not be reported to terminal applications even if they request it. This +/// allows you to always use the mouse for selection and other terminal UI +/// interactions without applications capturing mouse input. /// -/// A value of "3" (default) scrolls 3 lines per tick. +/// When set to `true` (the default), terminal applications can request mouse +/// reporting and will receive mouse events according to their requested mode. /// -/// Available since: 1.2.0 -@"mouse-scroll-multiplier": f64 = 3.0, +/// This can be toggled at runtime using the `toggle_mouse_reporting` keybind +/// action. +@"mouse-reporting": bool = true, + +/// Multiplier for scrolling distance with the mouse wheel. +/// +/// A prefix of `precision:` or `discrete:` can be used to set the multiplier +/// only for scrolling with the specific type of devices. These can be +/// comma-separated to set both types of multipliers at the same time, e.g. +/// `precision:0.1,discrete:3`. If no prefix is used, the multiplier applies +/// to all scrolling devices. Specifying a prefix was introduced in Ghostty +/// 1.2.1. +/// +/// The value will be clamped to [0.01, 10,000]. Both of these are extreme +/// and you're likely to have a bad experience if you set either extreme. +/// +/// The default value is "3" for discrete devices and "1" for precision devices. +@"mouse-scroll-multiplier": MouseScrollMultiplier = .default, /// The opacity level (opposite of transparency) of the background. A value of /// 1 is fully opaque and a value of 0 is fully transparent. A value less than 0 @@ -995,6 +1019,82 @@ command: ?Command = null, /// manually. @"initial-command": ?Command = null, +/// Controls when command finished notifications are sent. There are +/// three options: +/// +/// * `never` - Never send notifications (the default). +/// * `unfocused` - Only send notifications if the surface that the command is +/// running in is not focused. +/// * `always` - Always send notifications. +/// +/// Command finished notifications requires that either shell integration is +/// enabled, or that your shell sends OSC 133 escape sequences to mark the start +/// and end of commands. +/// +/// On GTK, there is a context menu item that will enable command finished +/// notifications for a single command, overriding the `never` and `unfocused` +/// options. +/// +/// GTK only. +/// +/// Available since 1.3.0. +@"notify-on-command-finish": NotifyOnCommandFinish = .never, + +/// If command finished notifications are enabled, this controls how the user is +/// notified. +/// +/// Available options: +/// +/// * `bell` - enabled by default +/// * `notify` - disabled by default +/// +/// Options can be combined by listing them as a comma separated list. Options +/// can be negated by prefixing them with `no-`. For example `no-bell,notify`. +/// +/// GTK only. +/// +/// Available since 1.3.0. +@"notify-on-command-finish-action": NotifyOnCommandFinishAction = .{ + .bell = true, + .notify = false, +}, + +/// If command finished notifications are enabled, this controls how long a +/// command must have been running before a notification will be sent. The +/// default is five seconds. +/// +/// The duration is specified as a series of numbers followed by time units. +/// Whitespace is allowed between numbers and units. Each number and unit will +/// be added together to form the total duration. +/// +/// The allowed time units are as follows: +/// +/// * `y` - 365 SI days, or 8760 hours, or 31536000 seconds. No adjustments +/// are made for leap years or leap seconds. +/// * `d` - one SI day, or 86400 seconds. +/// * `h` - one hour, or 3600 seconds. +/// * `m` - one minute, or 60 seconds. +/// * `s` - one second. +/// * `ms` - one millisecond, or 0.001 second. +/// * `us` or `µs` - one microsecond, or 0.000001 second. +/// * `ns` - one nanosecond, or 0.000000001 second. +/// +/// Examples: +/// * `1h30m` +/// * `45s` +/// +/// Units can be repeated and will be added together. This means that +/// `1h1h` is equivalent to `2h`. This is confusing and should be avoided. +/// A future update may disallow this. +/// +/// The maximum value is `584y 49w 23h 34m 33s 709ms 551µs 615ns`. Any +/// value larger than this will be clamped to the maximum value. +/// +/// GTK only. +/// +/// Available since 1.3.0 +@"notify-on-command-finish-after": Duration = .{ .duration = 5 * std.time.ns_per_s }, + /// Extra environment variables to pass to commands launched in a terminal /// surface. The format is `env=KEY=VALUE`. /// @@ -1114,6 +1214,24 @@ input: RepeatableReadableIO = .{}, /// This can be changed at runtime but will only affect new terminal surfaces. @"scrollback-limit": usize = 10_000_000, // 10MB +/// Control when the scrollbar is shown to scroll the scrollback buffer. +/// +/// The default value is `system`. +/// +/// Valid values: +/// +/// * `system` - Respect the system settings for when to show scrollbars. +/// For example, on macOS, this will respect the "Scrollbar behavior" +/// system setting which by default usually only shows scrollbars while +/// actively scrolling or hovering the gutter. +/// +/// * `never` - Never show a scrollbar. You can still scroll using the mouse, +/// keybind actions, etc. but you will not have a visual UI widget showing +/// a scrollbar. +/// +/// This only applies to macOS currently. GTK doesn't yet support scrollbars. +scrollbar: Scrollbar = .system, + /// Match a regular expression against the terminal text and associate clicking /// it with an action. This can be used to match URLs, file paths, etc. Actions /// can be opening using the system opener (e.g. `open` or `xdg-open`) or @@ -1989,7 +2107,7 @@ keybind: Keybinds = .{}, /// When this is true, the default configuration file paths will be loaded. /// The default configuration file paths are currently only the XDG -/// config path ($XDG_CONFIG_HOME/ghostty/config). +/// config path ($XDG_CONFIG_HOME/ghostty/config.ghostty). /// /// If this is false, the default configuration paths will not be loaded. /// This is targeted directly at using Ghostty from the CLI in a way @@ -2350,6 +2468,11 @@ keybind: Keybinds = .{}, /// cache manually using various arguments. /// (Available since: 1.2.0) /// +/// * `path` - Add Ghostty's binary directory to PATH. This ensures the `ghostty` +/// command is available in the shell even if shell init scripts reset PATH. +/// This is particularly useful on macOS where PATH is often overridden by +/// system scripts. The directory is only added if not already present. +/// /// SSH features work independently and can be combined for optimal experience: /// when both `ssh-env` and `ssh-terminfo` are enabled, Ghostty will install its /// terminfo on remote hosts and use `xterm-ghostty` as TERM, falling back to @@ -2771,7 +2894,7 @@ keybind: Keybinds = .{}, /// /// The values `left` or `right` enable this for the left or right *Option* /// key, respectively. -@"macos-option-as-alt": ?OptionAsAlt = null, +@"macos-option-as-alt": ?inputpkg.OptionAsAlt = null, /// Whether to enable the macOS window shadow. The default value is true. /// With some window managers and window transparency settings, you may @@ -2867,10 +2990,7 @@ keybind: Keybinds = .{}, /// Supported formats include PNG, JPEG, and ICNS. /// /// Defaults to `~/.config/ghostty/Ghostty.icns` -/// -/// Note: This configuration is required when `macos-icon` is set to -/// `custom` -@"macos-custom-icon": ?[]const u8 = null, +@"macos-custom-icon": ?[:0]const u8 = null, /// The material to use for the frame of the macOS app icon. /// @@ -3313,7 +3433,7 @@ pub fn loadIter( /// `path` must be resolved and absolute. pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void { assert(std.fs.path.isAbsolute(path)); - var file = openFile(path) catch |err| switch (err) { + var file = file_load.open(path) catch |err| switch (err) { error.NotAFile => { log.warn( "config-file {s}: not reading because it is not a file", @@ -3327,10 +3447,10 @@ pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void { defer file.close(); std.log.info("reading configuration file path={s}", .{path}); - var buf_reader = std.io.bufferedReader(file.reader()); - const reader = buf_reader.reader(); - const Iter = cli.args.LineIterator(@TypeOf(reader)); - var iter: Iter = .{ .r = reader, .filepath = path }; + var buf: [2048]u8 = undefined; + var file_reader = file.reader(&buf); + const reader = &file_reader.interface; + var iter: cli.args.LineIterator = .{ .r = reader, .filepath = path }; try self.loadIter(alloc, &iter); try self.expandPaths(std.fs.path.dirname(path).?); } @@ -3367,39 +3487,70 @@ fn writeConfigTemplate(path: []const u8) !void { } const file = try std.fs.createFileAbsolute(path, .{}); defer file.close(); - try std.fmt.format( - file.writer(), + var buf: [4096]u8 = undefined; + var file_writer = file.writer(&buf); + const writer = &file_writer.interface; + try writer.print( @embedFile("./config-template"), .{ .path = path }, ); } /// Load configurations from the default configuration files. The default -/// configuration file is at `$XDG_CONFIG_HOME/ghostty/config`. +/// configuration file is at `$XDG_CONFIG_HOME/ghostty/config.ghostty`. /// -/// On macOS, `$HOME/Library/Application Support/$CFBundleIdentifier/config` +/// On macOS, `$HOME/Library/Application Support/$CFBundleIdentifier/` /// is also loaded. +/// +/// The legacy `config` file (without extension) is first loaded, +/// then `config.ghostty`. pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { // Load XDG first - const xdg_path = try defaultXdgPath(alloc); + const legacy_xdg_path = try file_load.legacyDefaultXdgPath(alloc); + defer alloc.free(legacy_xdg_path); + const xdg_path = try file_load.defaultXdgPath(alloc); defer alloc.free(xdg_path); - const xdg_action = self.loadOptionalFile(alloc, xdg_path); + const xdg_loaded: bool = xdg_loaded: { + const legacy_xdg_action = self.loadOptionalFile(alloc, legacy_xdg_path); + const xdg_action = self.loadOptionalFile(alloc, xdg_path); + if (xdg_action != .not_found and legacy_xdg_action != .not_found) { + log.warn("both config files `{s}` and `{s}` exist.", .{ legacy_xdg_path, xdg_path }); + log.warn("loading them both in that order", .{}); + break :xdg_loaded true; + } + + break :xdg_loaded xdg_action != .not_found or + legacy_xdg_action != .not_found; + }; // On macOS load the app support directory as well if (comptime builtin.os.tag == .macos) { - const app_support_path = try defaultAppSupportPath(alloc); + const legacy_app_support_path = try file_load.legacyDefaultAppSupportPath(alloc); + defer alloc.free(legacy_app_support_path); + const app_support_path = try file_load.preferredAppSupportPath(alloc); defer alloc.free(app_support_path); - const app_support_action = self.loadOptionalFile(alloc, app_support_path); + const app_support_loaded: bool = loaded: { + const legacy_app_support_action = self.loadOptionalFile(alloc, legacy_app_support_path); + const app_support_action = self.loadOptionalFile(alloc, app_support_path); + if (app_support_action != .not_found and legacy_app_support_action != .not_found) { + log.warn("both config files `{s}` and `{s}` exist.", .{ legacy_app_support_path, app_support_path }); + log.warn("loading them both in that order", .{}); + break :loaded true; + } + + break :loaded app_support_action != .not_found or + legacy_app_support_action != .not_found; + }; // If both files are not found, then we create a template file. // For macOS, we only create the template file in the app support - if (app_support_action == .not_found and xdg_action == .not_found) { + if (!app_support_loaded and !xdg_loaded) { writeConfigTemplate(app_support_path) catch |err| { log.warn("error creating template config file err={}", .{err}); }; } } else { - if (xdg_action == .not_found) { + if (!xdg_loaded) { writeConfigTemplate(xdg_path) catch |err| { log.warn("error creating template config file err={}", .{err}); }; @@ -3407,102 +3558,6 @@ pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { } } -/// Default path for the XDG home configuration file. Returned value -/// must be freed by the caller. -fn defaultXdgPath(alloc: Allocator) ![]const u8 { - return try internal_os.xdg.config( - alloc, - .{ .subdir = "ghostty/config" }, - ); -} - -/// Default path for the macOS Application Support configuration file. -/// Returned value must be freed by the caller. -fn defaultAppSupportPath(alloc: Allocator) ![]const u8 { - return try internal_os.macos.appSupportDir(alloc, "config"); -} - -/// Returns the path to the preferred default configuration file. -/// This is the file where users should place their configuration. -/// -/// This doesn't create or populate the file with any default -/// contents; downstream callers must handle this. -/// -/// The returned value must be freed by the caller. -pub fn preferredDefaultFilePath(alloc: Allocator) ![]const u8 { - switch (builtin.os.tag) { - .macos => { - // macOS prefers the Application Support directory - // if it exists. - const app_support_path = try defaultAppSupportPath(alloc); - if (openFile(app_support_path)) |f| { - f.close(); - return app_support_path; - } else |_| {} - - // Try the XDG path if it exists - const xdg_path = try defaultXdgPath(alloc); - if (openFile(xdg_path)) |f| { - f.close(); - alloc.free(app_support_path); - return xdg_path; - } else |_| {} - defer alloc.free(xdg_path); - - // Neither exist, use app support - return app_support_path; - }, - - // All other platforms use XDG only - else => return try defaultXdgPath(alloc), - } -} - -const OpenFileError = error{ - FileNotFound, - FileIsEmpty, - FileOpenFailed, - NotAFile, -}; - -/// Opens the file at the given path and returns the file handle -/// if it exists and is non-empty. This also constrains the possible -/// errors to a smaller set that we can explicitly handle. -fn openFile(path: []const u8) OpenFileError!std.fs.File { - assert(std.fs.path.isAbsolute(path)); - - var file = std.fs.openFileAbsolute( - path, - .{}, - ) catch |err| switch (err) { - error.FileNotFound => return OpenFileError.FileNotFound, - else => { - log.warn("unexpected file open error path={s} err={}", .{ - path, - err, - }); - return OpenFileError.FileOpenFailed; - }, - }; - errdefer file.close(); - - const stat = file.stat() catch |err| { - log.warn("error getting file stat path={s} err={}", .{ - path, - err, - }); - return OpenFileError.FileOpenFailed; - }; - switch (stat.kind) { - .file => {}, - else => return OpenFileError.NotAFile, - } - - if (stat.size == 0) return OpenFileError.FileIsEmpty; - - return file; -} - /// Load and parse the CLI args. pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { switch (builtin.os.tag) { @@ -3538,17 +3593,17 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { // Next, take all remaining args and use that to build up // a command to execute. - var builder = std.ArrayList([:0]const u8).init(arena_alloc); - errdefer builder.deinit(); + var builder: std.ArrayList([:0]const u8) = .empty; + errdefer builder.deinit(arena_alloc); for (args) |arg_raw| { const arg = std.mem.sliceTo(arg_raw, 0); const copy = try arena_alloc.dupeZ(u8, arg); try self._replay_steps.append(arena_alloc, .{ .arg = copy }); - try builder.append(copy); + try builder.append(arena_alloc, copy); } self.@"_xdg-terminal-exec" = true; - self.@"initial-command" = .{ .direct = try builder.toOwnedSlice() }; + self.@"initial-command" = .{ .direct = try builder.toOwnedSlice(arena_alloc) }; return; } } @@ -3620,13 +3675,13 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { // PRIOR to the "-e" in our replay steps, since everything // after "-e" becomes an "initial-command". To do this, we // dupe the values if we find it. - var replay_suffix = std.ArrayList(Replay.Step).init(alloc_gpa); - defer replay_suffix.deinit(); + var replay_suffix: std.ArrayList(Replay.Step) = .empty; + defer replay_suffix.deinit(alloc_gpa); for (self._replay_steps.items, 0..) |step, i| if (step == .@"-e") { // We don't need to clone the steps because they should // all be allocated in our arena and we're keeping our // arena. - try replay_suffix.appendSlice(self._replay_steps.items[i..]); + try replay_suffix.appendSlice(alloc_gpa, self._replay_steps.items[i..]); // Remove our old values. Again, don't need to free any // memory here because its all part of our arena. @@ -3654,10 +3709,11 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { // We must only load a unique file once if (try loaded.fetchPut(path, {}) != null) { const diag: cli.Diagnostic = .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "config-file {s}: cycle detected", .{path}, + 0, ), }; @@ -3669,10 +3725,11 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { var file = std.fs.openFileAbsolute(path, .{}) catch |err| { if (err != error.FileNotFound or !optional) { const diag: cli.Diagnostic = .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "error opening config-file {s}: {}", .{ path, err }, + 0, ), }; @@ -3688,10 +3745,11 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { .file => {}, else => |kind| { const diag: cli.Diagnostic = .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "config-file {s}: not reading because file type is {s}", .{ path, @tagName(kind) }, + 0, ), }; @@ -3702,10 +3760,10 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { } log.info("loading config-file path={s}", .{path}); - var buf_reader = std.io.bufferedReader(file.reader()); - const reader = buf_reader.reader(); - const Iter = cli.args.LineIterator(@TypeOf(reader)); - var iter: Iter = .{ .r = reader, .filepath = path }; + var buf: [2048]u8 = undefined; + var file_reader = file.reader(&buf); + const reader = &file_reader.interface; + var iter: cli.args.LineIterator = .{ .r = reader, .filepath = path }; try self.loadIter(alloc_gpa, &iter); try self.expandPaths(std.fs.path.dirname(path).?); } @@ -3854,10 +3912,10 @@ fn loadTheme(self: *Config, theme: Theme) !void { errdefer new_config.deinit(); // Load our theme - var buf_reader = std.io.bufferedReader(file.reader()); - const reader = buf_reader.reader(); - const Iter = cli.args.LineIterator(@TypeOf(reader)); - var iter: Iter = .{ .r = reader, .filepath = path }; + var buf: [2048]u8 = undefined; + var file_reader = file.reader(&buf); + const reader = &file_reader.interface; + var iter: cli.args.LineIterator = .{ .r = reader, .filepath = path }; try new_config.loadIter(alloc_gpa, &iter); // Setup our replay to be conditional. @@ -4077,7 +4135,8 @@ pub fn finalize(self: *Config) !void { } // Clamp our mouse scroll multiplier - self.@"mouse-scroll-multiplier" = @min(10_000.0, @max(0.01, self.@"mouse-scroll-multiplier")); + self.@"mouse-scroll-multiplier".precision = @min(10_000.0, @max(0.01, self.@"mouse-scroll-multiplier".precision)); + self.@"mouse-scroll-multiplier".discrete = @min(10_000.0, @max(0.01, self.@"mouse-scroll-multiplier".discrete)); // Clamp our split opacity self.@"unfocused-split-opacity" = @min(1.0, @max(0.15, self.@"unfocused-split-opacity")); @@ -4085,7 +4144,7 @@ pub fn finalize(self: *Config) !void { // Clamp our contrast self.@"minimum-contrast" = @min(21, @max(1, self.@"minimum-contrast")); - // Minimmum window size + // Minimum window size if (self.@"window-width" > 0) self.@"window-width" = @max(10, self.@"window-width"); if (self.@"window-height" > 0) self.@"window-height" = @max(4, self.@"window-height"); @@ -4099,7 +4158,7 @@ pub fn finalize(self: *Config) !void { if (self.@"quit-after-last-window-closed-delay") |duration| { if (duration.duration < 5 * std.time.ns_per_s) { log.warn( - "quit-after-last-window-closed-delay is set to a very short value ({}), which might cause problems", + "quit-after-last-window-closed-delay is set to a very short value ({f}), which might cause problems", .{duration}, ); } @@ -4130,22 +4189,23 @@ pub fn parseManuallyHook( // Build up the command. We don't clean this up because we take // ownership in our allocator. - var command: std.ArrayList([:0]const u8) = .init(alloc); - errdefer command.deinit(); + var command: std.ArrayList([:0]const u8) = .empty; + errdefer command.deinit(alloc); while (iter.next()) |param| { const copy = try alloc.dupeZ(u8, param); try self._replay_steps.append(alloc, .{ .arg = copy }); - try command.append(copy); + try command.append(alloc, copy); } if (command.items.len == 0) { try self._diagnostics.append(alloc, .{ .location = try cli.Location.fromIter(iter, alloc), - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( alloc, "missing command after {s}", .{arg}, + 0, ), }); @@ -4280,10 +4340,11 @@ pub fn addDiagnosticFmt( ) Allocator.Error!void { const alloc = self._arena.?.allocator(); try self._diagnostics.append(alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( alloc, fmt, args, + 0, ), }); } @@ -4726,14 +4787,6 @@ pub const NonNativeFullscreen = enum(c_int) { @"padded-notch", }; -/// Valid values for macos-option-as-alt. -pub const OptionAsAlt = enum { - false, - true, - left, - right, -}; - pub const WindowPaddingColor = enum { background, extend, @@ -4801,7 +4854,7 @@ pub const Color = struct { } /// Used by Formatter - pub fn formatEntry(self: Color, formatter: anytype) !void { + pub fn formatEntry(self: Color, formatter: formatterpkg.EntryFormatter) !void { var buf: [128]u8 = undefined; try formatter.formatEntry( []const u8, @@ -4868,12 +4921,12 @@ pub const Color = struct { test "formatConfig" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var color: Color = .{ .r = 10, .g = 11, .b = 12 }; - try color.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = #0a0b0c\n", buf.items); + try color.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = #0a0b0c\n", buf.written()); } test "parseCLI with whitespace" { @@ -4904,7 +4957,7 @@ pub const TerminalColor = union(enum) { } /// Used by Formatter - pub fn formatEntry(self: TerminalColor, formatter: anytype) !void { + pub fn formatEntry(self: TerminalColor, formatter: formatterpkg.EntryFormatter) !void { switch (self) { .color => try self.color.formatEntry(formatter), @@ -4939,12 +4992,12 @@ pub const TerminalColor = union(enum) { test "formatConfig" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var sc: TerminalColor = .@"cell-foreground"; - try sc.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try testing.expectEqualSlices(u8, "a = cell-foreground\n", buf.items); + try sc.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try testing.expectEqualSlices(u8, "a = cell-foreground\n", buf.written()); } }; @@ -4960,7 +5013,7 @@ pub const BoldColor = union(enum) { } /// Used by Formatter - pub fn formatEntry(self: BoldColor, formatter: anytype) !void { + pub fn formatEntry(self: BoldColor, formatter: formatterpkg.EntryFormatter) !void { switch (self) { .color => try self.color.formatEntry(formatter), .bright => try formatter.formatEntry( @@ -4991,12 +5044,12 @@ pub const BoldColor = union(enum) { test "formatConfig" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var sc: BoldColor = .bright; - try sc.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try testing.expectEqualSlices(u8, "a = bright\n", buf.items); + try sc.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try testing.expectEqualSlices(u8, "a = bright\n", buf.written()); } }; @@ -5083,8 +5136,7 @@ pub const ColorList = struct { // Build up the value of our config. Our buffer size should be // sized to contain all possible maximum values. var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - var writer = fbs.writer(); + var writer: std.Io.Writer = .fixed(&buf); for (self.colors.items, 0..) |color, i| { var color_buf: [128]u8 = undefined; const color_str = try color.formatBuf(&color_buf); @@ -5094,7 +5146,7 @@ pub const ColorList = struct { try formatter.formatEntry( []const u8, - fbs.getWritten(), + writer.buffered(), ); } @@ -5123,7 +5175,7 @@ pub const ColorList = struct { test "format" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -5132,8 +5184,8 @@ pub const ColorList = struct { var p: Self = .{}; try p.parseCLI(alloc, "black,white"); - try p.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = #000000,#ffffff\n", buf.items); + try p.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = #000000,#ffffff\n", buf.written()); } }; @@ -5194,7 +5246,7 @@ pub const Palette = struct { } /// Used by Formatter - pub fn formatEntry(self: Self, formatter: anytype) !void { + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { var buf: [128]u8 = undefined; for (0.., self.value) |k, v| { try formatter.formatEntry( @@ -5249,12 +5301,12 @@ pub const Palette = struct { test "formatConfig" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var list: Self = .{}; - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = 0=#1d1f21\n", buf.items[0..14]); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = 0=#1d1f21\n", buf.written()[0..14]); } test "parseCLI with whitespace" { @@ -5348,7 +5400,7 @@ pub const RepeatableString = struct { } /// Used by Formatter - pub fn formatEntry(self: Self, formatter: anytype) !void { + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { // If no items, we want to render an empty field. if (self.list.items.len == 0) { try formatter.formatEntry(void, {}); @@ -5395,17 +5447,17 @@ pub const RepeatableString = struct { test "formatConfig empty" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var list: Self = .{}; - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = \n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = \n", buf.written()); } test "formatConfig single item" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -5414,13 +5466,13 @@ pub const RepeatableString = struct { var list: Self = .{}; try list.parseCLI(alloc, "A"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = A\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = A\n", buf.written()); } test "formatConfig multiple items" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -5430,8 +5482,8 @@ pub const RepeatableString = struct { var list: Self = .{}; try list.parseCLI(alloc, "A"); try list.parseCLI(alloc, "B"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = A\na = B\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = A\na = B\n", buf.written()); } }; @@ -5547,7 +5599,7 @@ pub const RepeatableFontVariation = struct { test "formatConfig single" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -5556,8 +5608,8 @@ pub const RepeatableFontVariation = struct { var list: Self = .{}; try list.parseCLI(alloc, "wght = 200"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = wght=200\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = wght=200\n", buf.written()); } }; @@ -6358,7 +6410,7 @@ pub const Keybinds = struct { } /// Like formatEntry but has an option to include docs. - pub fn formatEntryDocs(self: Keybinds, formatter: anytype, docs: bool) !void { + pub fn formatEntryDocs(self: Keybinds, formatter: formatterpkg.EntryFormatter, docs: bool) !void { if (self.set.bindings.size == 0) { try formatter.formatEntry(void, {}); return; @@ -6387,14 +6439,14 @@ pub const Keybinds = struct { } } - var buffer_stream = std.io.fixedBufferStream(&buf); - std.fmt.format(buffer_stream.writer(), "{}", .{k}) catch return error.OutOfMemory; - try v.formatEntries(&buffer_stream, formatter); + var writer: std.Io.Writer = .fixed(&buf); + writer.print("{f}", .{k}) catch return error.OutOfMemory; + try v.formatEntries(&writer, formatter); } } /// Used by Formatter - pub fn formatEntry(self: Keybinds, formatter: anytype) !void { + pub fn formatEntry(self: Keybinds, formatter: formatterpkg.EntryFormatter) !void { try self.formatEntryDocs(formatter, false); } @@ -6411,7 +6463,7 @@ pub const Keybinds = struct { test "formatConfig single" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -6420,14 +6472,14 @@ pub const Keybinds = struct { var list: Keybinds = .{}; try list.parseCLI(alloc, "shift+a=csi:hello"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = shift+a=csi:hello\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = shift+a=csi:hello\n", buf.written()); } // Regression test for https://github.com/ghostty-org/ghostty/issues/2734 test "formatConfig multiple items" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -6437,7 +6489,7 @@ pub const Keybinds = struct { var list: Keybinds = .{}; try list.parseCLI(alloc, "ctrl+z>1=goto_tab:1"); try list.parseCLI(alloc, "ctrl+z>2=goto_tab:2"); - try list.formatEntry(formatterpkg.entryFormatter("keybind", buf.writer())); + try list.formatEntry(formatterpkg.entryFormatter("keybind", &buf.writer)); // Note they turn into translated keys because they match // their ASCII mapping. @@ -6446,12 +6498,12 @@ pub const Keybinds = struct { \\keybind = ctrl+z>1=goto_tab:1 \\ ; - try std.testing.expectEqualStrings(want, buf.items); + try std.testing.expectEqualStrings(want, buf.written()); } test "formatConfig multiple items nested" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -6463,7 +6515,7 @@ pub const Keybinds = struct { try list.parseCLI(alloc, "ctrl+a>ctrl+b>w=close_window"); try list.parseCLI(alloc, "ctrl+a>ctrl+c>t=new_tab"); try list.parseCLI(alloc, "ctrl+b>ctrl+d>a=previous_tab"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); // NB: This does not currently retain the order of the keybinds. const want = @@ -6473,7 +6525,7 @@ pub const Keybinds = struct { \\a = ctrl+b>ctrl+d>a=previous_tab \\ ; - try std.testing.expectEqualStrings(want, buf.items); + try std.testing.expectEqualStrings(want, buf.written()); } }; @@ -6508,7 +6560,7 @@ pub const RepeatableCodepointMap = struct { return .{ .map = try self.map.clone(alloc) }; } - /// Compare if two of our value are requal. Required by Config. + /// Compare if two of our value are equal. Required by Config. pub fn equal(self: Self, other: Self) bool { const itemsA = self.map.list.slice(); const itemsB = other.map.list.slice(); @@ -6699,7 +6751,7 @@ pub const RepeatableCodepointMap = struct { test "formatConfig single" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -6708,13 +6760,13 @@ pub const RepeatableCodepointMap = struct { var list: Self = .{}; try list.parseCLI(alloc, "U+ABCD=Comic Sans"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = U+ABCD=Comic Sans\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = U+ABCD=Comic Sans\n", buf.written()); } test "formatConfig range" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -6723,13 +6775,13 @@ pub const RepeatableCodepointMap = struct { var list: Self = .{}; try list.parseCLI(alloc, "U+0001 - U+0005=Verdana"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = U+0001-U+0005=Verdana\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = U+0001-U+0005=Verdana\n", buf.written()); } test "formatConfig multiple" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -6738,12 +6790,12 @@ pub const RepeatableCodepointMap = struct { var list: Self = .{}; try list.parseCLI(alloc, "U+0006-U+0009, U+ABCD=Courier"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); try std.testing.expectEqualSlices(u8, \\a = U+0006-U+0009=Courier \\a = U+ABCD=Courier \\ - , buf.items); + , buf.written()); } }; @@ -6795,7 +6847,7 @@ pub const FontStyle = union(enum) { } /// Used by Formatter - pub fn formatEntry(self: Self, formatter: anytype) !void { + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { switch (self) { .default, .false => try formatter.formatEntry( []const u8, @@ -6827,7 +6879,7 @@ pub const FontStyle = union(enum) { test "formatConfig default" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -6836,13 +6888,13 @@ pub const FontStyle = union(enum) { var p: Self = .{ .default = {} }; try p.parseCLI(alloc, "default"); - try p.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = default\n", buf.items); + try p.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = default\n", buf.written()); } test "formatConfig false" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -6851,13 +6903,13 @@ pub const FontStyle = union(enum) { var p: Self = .{ .default = {} }; try p.parseCLI(alloc, "false"); - try p.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = false\n", buf.items); + try p.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = false\n", buf.written()); } test "formatConfig named" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -6866,8 +6918,8 @@ pub const FontStyle = union(enum) { var p: Self = .{ .default = {} }; try p.parseCLI(alloc, "bold"); - try p.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = bold\n", buf.items); + try p.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = bold\n", buf.written()); } }; @@ -6927,7 +6979,7 @@ pub const RepeatableLink = struct { } /// Used by Formatter - pub fn formatEntry(self: Self, formatter: anytype) !void { + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { // This currently can't be set so we don't format anything. _ = self; _ = formatter; @@ -6984,6 +7036,7 @@ pub const ShellIntegrationFeatures = packed struct { title: bool = true, @"ssh-env": bool = false, @"ssh-terminfo": bool = false, + path: bool = true, }; pub const RepeatableCommand = struct { @@ -7010,6 +7063,7 @@ pub const RepeatableCommand = struct { inputpkg.Command, alloc, input, + null, ); try self.value.append(alloc, cmd); } @@ -7035,7 +7089,10 @@ pub const RepeatableCommand = struct { } /// Used by Formatter - pub fn formatEntry(self: RepeatableCommand, formatter: anytype) !void { + pub fn formatEntry( + self: RepeatableCommand, + formatter: formatterpkg.EntryFormatter, + ) !void { if (self.value.items.len == 0) { try formatter.formatEntry(void, {}); return; @@ -7043,22 +7100,23 @@ pub const RepeatableCommand = struct { for (self.value.items) |item| { var buf: [4096]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - var writer = fbs.writer(); + var writer: std.Io.Writer = .fixed(&buf); - writer.writeAll("title:\"") catch return error.OutOfMemory; - std.zig.stringEscape(item.title, "", .{}, writer) catch return error.OutOfMemory; - writer.writeAll("\"") catch return error.OutOfMemory; + writer.print( + "title:\"{f}\"", + .{std.zig.fmtString(item.title)}, + ) catch return error.OutOfMemory; if (item.description.len > 0) { - writer.writeAll(",description:\"") catch return error.OutOfMemory; - std.zig.stringEscape(item.description, "", .{}, writer) catch return error.OutOfMemory; - writer.writeAll("\"") catch return error.OutOfMemory; + writer.print( + ",description:\"{f}\"", + .{std.zig.fmtString(item.description)}, + ) catch return error.OutOfMemory; } - writer.print(",action:\"{}\"", .{item.action}) catch return error.OutOfMemory; + writer.print(",action:\"{f}\"", .{item.action}) catch return error.OutOfMemory; - try formatter.formatEntry([]const u8, fbs.getWritten()); + try formatter.formatEntry([]const u8, writer.buffered()); } } @@ -7104,17 +7162,17 @@ pub const RepeatableCommand = struct { test "RepeatableCommand formatConfig empty" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var list: RepeatableCommand = .{}; - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = \n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = \n", buf.written()); } test "RepeatableCommand formatConfig single item" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -7123,13 +7181,13 @@ pub const RepeatableCommand = struct { var list: RepeatableCommand = .{}; try list.parseCLI(alloc, "title:Bobr, action:text:Bober"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = title:\"Bobr\",action:\"text:Bober\"\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = title:\"Bobr\",action:\"text:Bober\"\n", buf.written()); } test "RepeatableCommand formatConfig multiple items" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -7139,14 +7197,12 @@ pub const RepeatableCommand = struct { var list: RepeatableCommand = .{}; try list.parseCLI(alloc, "title:Bobr, action:text:kurwa"); try list.parseCLI(alloc, "title:Ja, description: pierdole, action:text:jakie bydle"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = title:\"Bobr\",action:\"text:kurwa\"\na = title:\"Ja\",description:\"pierdole\",action:\"text:jakie bydle\"\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = title:\"Bobr\",action:\"text:kurwa\"\na = title:\"Ja\",description:\"pierdole\",action:\"text:jakie bydle\"\n", buf.written()); } test "RepeatableCommand parseCLI commas" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); - defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); @@ -7319,6 +7375,108 @@ pub const MouseShiftCapture = enum { never, }; +/// See mouse-scroll-multiplier +pub const MouseScrollMultiplier = struct { + const Self = @This(); + + precision: f64 = 1, + discrete: f64 = 3, + + pub const default: MouseScrollMultiplier = .{}; + + pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void { + const input = input_ orelse return error.ValueRequired; + self.* = cli.args.parseAutoStruct( + MouseScrollMultiplier, + alloc, + input, + self.*, + ) catch |err| switch (err) { + error.InvalidValue => bare: { + const v = std.fmt.parseFloat( + f64, + input, + ) catch return error.InvalidValue; + break :bare .{ + .precision = v, + .discrete = v, + }; + }, + else => return err, + }; + } + + /// Deep copy of the struct. Required by Config. + pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { + _ = alloc; + return self.*; + } + + /// Compare if two of our value are equal. Required by Config. + pub fn equal(self: Self, other: Self) bool { + return self.precision == other.precision and self.discrete == other.discrete; + } + + /// Used by Formatter + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { + var buf: [4096]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + writer.print( + "precision:{d},discrete:{d}", + .{ self.precision, self.discrete }, + ) catch return error.OutOfMemory; + try formatter.formatEntry([]const u8, writer.buffered()); + } + + test "parse" { + const testing = std.testing; + const alloc = testing.allocator; + const epsilon = 0.00001; + + var args: Self = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI(alloc, "3"); + try testing.expectApproxEqAbs(3, args.precision, epsilon); + try testing.expectApproxEqAbs(3, args.discrete, epsilon); + + args = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI(alloc, "precision:1"); + try testing.expectApproxEqAbs(1, args.precision, epsilon); + try testing.expectApproxEqAbs(3, args.discrete, epsilon); + + args = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI(alloc, "discrete:5"); + try testing.expectApproxEqAbs(0.1, args.precision, epsilon); + try testing.expectApproxEqAbs(5, args.discrete, epsilon); + + args = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI(alloc, "precision:3,discrete:7"); + try testing.expectApproxEqAbs(3, args.precision, epsilon); + try testing.expectApproxEqAbs(7, args.discrete, epsilon); + + args = .{ .precision = 0.1, .discrete = 3 }; + try args.parseCLI(alloc, "discrete:8,precision:6"); + try testing.expectApproxEqAbs(6, args.precision, epsilon); + try testing.expectApproxEqAbs(8, args.discrete, epsilon); + + args = .default; + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "foo:1")); + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "precision:bar")); + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "precision:1,discrete:3,foo:5")); + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "precision:1,,discrete:3")); + try testing.expectError(error.InvalidValue, args.parseCLI(alloc, ",precision:1,discrete:3")); + } + + test "format entry MouseScrollMultiplier" { + const testing = std.testing; + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + var args: Self = .{ .precision = 1.5, .discrete = 2.5 }; + try args.formatEntry(formatterpkg.entryFormatter("mouse-scroll-multiplier", &buf.writer)); + try testing.expectEqualSlices(u8, "mouse-scroll-multiplier = precision:1.5,discrete:2.5\n", buf.written()); + } +}; + /// How to treat requests to write to or read from the clipboard pub const ClipboardAccess = enum { allow, @@ -7432,7 +7590,7 @@ pub const QuickTerminalSize = struct { return error.MissingUnit; } - fn format(self: Size, writer: anytype) !void { + fn format(self: Size, writer: *std.Io.Writer) !void { switch (self) { .percentage => |v| try writer.print("{d}%", .{v}), .pixels => |v| try writer.print("{}px", .{v}), @@ -7550,20 +7708,19 @@ pub const QuickTerminalSize = struct { }; } - pub fn formatEntry(self: QuickTerminalSize, formatter: anytype) !void { + pub fn formatEntry(self: QuickTerminalSize, formatter: formatterpkg.EntryFormatter) !void { const primary = self.primary orelse return; var buf: [4096]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - const writer = fbs.writer(); + var writer: std.Io.Writer = .fixed(&buf); - primary.format(writer) catch return error.OutOfMemory; + primary.format(&writer) catch return error.OutOfMemory; if (self.secondary) |secondary| { writer.writeByte(',') catch return error.OutOfMemory; - secondary.format(writer) catch return error.OutOfMemory; + secondary.format(&writer) catch return error.OutOfMemory; } - try formatter.formatEntry([]const u8, fbs.getWritten()); + try formatter.formatEntry([]const u8, writer.buffered()); } test "parse QuickTerminalSize" { @@ -7746,11 +7903,15 @@ pub const BackgroundImageFit = enum { pub const FreetypeLoadFlags = packed struct { // The defaults here at the time of writing this match the defaults // for Freetype itself. Ghostty hasn't made any opinionated changes - // to these defaults. + // to these defaults. (Strictly speaking, `light` isn't FreeType's + // own default, but appears to be the effective default with most + // Fontconfig-aware software using FreeType, so until Ghostty + // implements Fontconfig support we default to `light`.) hinting: bool = true, @"force-autohint": bool = false, monochrome: bool = false, autohint: bool = true, + light: bool = true, }; /// See linux-cgroup @@ -7933,6 +8094,7 @@ pub const Theme = struct { Theme, alloc, input, + null, ); return; } @@ -8045,6 +8207,10 @@ pub const Duration = struct { return .{ .duration = self.duration / to * to }; } + pub fn lte(self: Duration, other: Duration) bool { + return self.duration <= other.duration; + } + pub fn parseCLI(input: ?[]const u8) !Duration { var remaining = input orelse return error.ValueRequired; @@ -8118,15 +8284,17 @@ pub const Duration = struct { return if (value) |v| .{ .duration = v } else error.ValueRequired; } - pub fn formatEntry(self: Duration, formatter: anytype) !void { + pub fn formatEntry(self: Duration, formatter: formatterpkg.EntryFormatter) !void { var buf: [64]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - const writer = fbs.writer(); - try self.format("", .{}, writer); - try formatter.formatEntry([]const u8, fbs.getWritten()); + var writer: std.Io.Writer = .fixed(&buf); + try self.format(&writer); + try formatter.formatEntry([]const u8, writer.buffered()); } - pub fn format(self: Duration, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + pub fn format( + self: Duration, + writer: *std.Io.Writer, + ) !void { var value = self.duration; var i: usize = 0; for (units) |unit| { @@ -8193,7 +8361,7 @@ pub const WindowPadding = struct { } } - pub fn formatEntry(self: Self, formatter: anytype) !void { + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { var buf: [128]u8 = undefined; if (self.top_left == self.bottom_right) { try formatter.formatEntry( @@ -8250,6 +8418,12 @@ pub const WindowPadding = struct { } }; +/// See scrollbar +pub const Scrollbar = enum { + system, + never, +}; + /// See scroll-to-bottom pub const ScrollToBottom = packed struct { keystroke: bool = true, @@ -8258,6 +8432,19 @@ pub const ScrollToBottom = packed struct { pub const default: ScrollToBottom = .{}; }; +/// See notify-on-command-finish +pub const NotifyOnCommandFinish = enum { + never, + unfocused, + always, +}; + +/// See notify-on-command-finish-action +pub const NotifyOnCommandFinishAction = packed struct { + bell: bool = true, + notify: bool = false, +}; + test "parse duration" { inline for (Duration.units) |unit| { var buf: [16]u8 = undefined; @@ -8342,7 +8529,7 @@ test "test format" { inline for (Duration.units) |unit| { const d: Duration = .{ .duration = unit.factor }; var actual_buf: [16]u8 = undefined; - const actual = try std.fmt.bufPrint(&actual_buf, "{}", .{d}); + const actual = try std.fmt.bufPrint(&actual_buf, "{f}", .{d}); var expected_buf: [16]u8 = undefined; const expected = if (!std.mem.eql(u8, unit.name, "us")) try std.fmt.bufPrint(&expected_buf, "1{s}", .{unit.name}) @@ -8353,12 +8540,12 @@ test "test format" { } test "test entryFormatter" { - var buf = std.ArrayList(u8).init(std.testing.allocator); + var buf: std.Io.Writer.Allocating = .init(std.testing.allocator); defer buf.deinit(); var p: Duration = .{ .duration = std.math.maxInt(u64) }; - try p.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualStrings("a = 584y 49w 23h 34m 33s 709ms 551µs 615ns\n", buf.items); + try p.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualStrings("a = 584y 49w 23h 34m 33s 709ms 551µs 615ns\n", buf.written()); } const TestIterator = struct { @@ -8468,15 +8655,20 @@ test "clone can then change conditional state" { // Setup our test theme var td = try internal_os.TempDir.init(); defer td.deinit(); + var buf: [4096]u8 = undefined; { var file = try td.dir.createFile("theme_light", .{}); defer file.close(); - try file.writer().writeAll(@embedFile("testdata/theme_light")); + var writer = file.writer(&buf); + try writer.interface.writeAll(@embedFile("testdata/theme_light")); + try writer.end(); } { var file = try td.dir.createFile("theme_dark", .{}); defer file.close(); - try file.writer().writeAll(@embedFile("testdata/theme_dark")); + var writer = file.writer(&buf); + try writer.interface.writeAll(@embedFile("testdata/theme_dark")); + try writer.end(); } var light_buf: [std.fs.max_path_bytes]u8 = undefined; const light = try td.dir.realpath("theme_light", &light_buf); @@ -8602,10 +8794,13 @@ test "theme loading" { // Setup our test theme var td = try internal_os.TempDir.init(); defer td.deinit(); + var buf: [4096]u8 = undefined; { var file = try td.dir.createFile("theme", .{}); defer file.close(); - try file.writer().writeAll(@embedFile("testdata/theme_simple")); + var writer = file.writer(&buf); + try writer.interface.writeAll(@embedFile("testdata/theme_simple")); + try writer.end(); } var path_buf: [std.fs.max_path_bytes]u8 = undefined; const path = try td.dir.realpath("theme", &path_buf); @@ -8638,10 +8833,13 @@ test "theme loading preserves conditional state" { // Setup our test theme var td = try internal_os.TempDir.init(); defer td.deinit(); + var buf: [4096]u8 = undefined; { var file = try td.dir.createFile("theme", .{}); defer file.close(); - try file.writer().writeAll(@embedFile("testdata/theme_simple")); + var writer = file.writer(&buf); + try writer.interface.writeAll(@embedFile("testdata/theme_simple")); + try writer.end(); } var path_buf: [std.fs.max_path_bytes]u8 = undefined; const path = try td.dir.realpath("theme", &path_buf); @@ -8668,10 +8866,13 @@ test "theme priority is lower than config" { // Setup our test theme var td = try internal_os.TempDir.init(); defer td.deinit(); + var buf: [4096]u8 = undefined; { var file = try td.dir.createFile("theme", .{}); defer file.close(); - try file.writer().writeAll(@embedFile("testdata/theme_simple")); + var writer = file.writer(&buf); + try writer.interface.writeAll(@embedFile("testdata/theme_simple")); + try writer.end(); } var path_buf: [std.fs.max_path_bytes]u8 = undefined; const path = try td.dir.realpath("theme", &path_buf); @@ -8702,15 +8903,20 @@ test "theme loading correct light/dark" { // Setup our test theme var td = try internal_os.TempDir.init(); defer td.deinit(); + var buf: [4096]u8 = undefined; { var file = try td.dir.createFile("theme_light", .{}); defer file.close(); - try file.writer().writeAll(@embedFile("testdata/theme_light")); + var writer = file.writer(&buf); + try writer.interface.writeAll(@embedFile("testdata/theme_light")); + try writer.end(); } { var file = try td.dir.createFile("theme_dark", .{}); defer file.close(); - try file.writer().writeAll(@embedFile("testdata/theme_dark")); + var writer = file.writer(&buf); + try writer.interface.writeAll(@embedFile("testdata/theme_dark")); + try writer.end(); } var light_buf: [std.fs.max_path_bytes]u8 = undefined; const light = try td.dir.realpath("theme_light", &light_buf); diff --git a/src/config/RepeatableStringMap.zig b/src/config/RepeatableStringMap.zig index 6f143e95d..d5e634333 100644 --- a/src/config/RepeatableStringMap.zig +++ b/src/config/RepeatableStringMap.zig @@ -104,7 +104,7 @@ pub fn equal(self: RepeatableStringMap, other: RepeatableStringMap) bool { } /// Used by formatter -pub fn formatEntry(self: RepeatableStringMap, formatter: anytype) !void { +pub fn formatEntry(self: RepeatableStringMap, formatter: formatterpkg.EntryFormatter) !void { // If no items, we want to render an empty field. if (self.map.count() == 0) { try formatter.formatEntry(void, {}); @@ -146,12 +146,12 @@ test "RepeatableStringMap: parseCLI" { test "RepeatableStringMap: formatConfig empty" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var list: RepeatableStringMap = .{}; - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = \n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = \n", buf.written()); } test "RepeatableStringMap: formatConfig single item" { @@ -162,20 +162,20 @@ test "RepeatableStringMap: formatConfig single item" { const alloc = arena.allocator(); { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var map: RepeatableStringMap = .{}; try map.parseCLI(alloc, "A=B"); - try map.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = A=B\n", buf.items); + try map.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = A=B\n", buf.written()); } { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var map: RepeatableStringMap = .{}; try map.parseCLI(alloc, " A = B "); - try map.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = A=B\n", buf.items); + try map.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = A=B\n", buf.written()); } } @@ -187,12 +187,12 @@ test "RepeatableStringMap: formatConfig multiple items" { const alloc = arena.allocator(); { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var list: RepeatableStringMap = .{}; try list.parseCLI(alloc, "A=B"); try list.parseCLI(alloc, "B = C"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = A=B\na = B=C\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = A=B\na = B=C\n", buf.written()); } } diff --git a/src/config/command.zig b/src/config/command.zig index 9efeb199e..e0cdc641b 100644 --- a/src/config/command.zig +++ b/src/config/command.zig @@ -166,21 +166,20 @@ pub const Command = union(enum) { }; } - pub fn formatEntry(self: Self, formatter: anytype) !void { + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { switch (self) { .shell => |v| try formatter.formatEntry([]const u8, v), .direct => |v| { var buf: [4096]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - const writer = fbs.writer(); + var writer: std.Io.Writer = .fixed(&buf); writer.writeAll("direct:") catch return error.OutOfMemory; for (v) |arg| { writer.writeAll(arg) catch return error.OutOfMemory; writer.writeByte(' ') catch return error.OutOfMemory; } - const written = fbs.getWritten(); + const written = writer.buffered(); try formatter.formatEntry( []const u8, written[0..@intCast(written.len - 1)], @@ -292,13 +291,13 @@ pub const Command = union(enum) { defer arena.deinit(); const alloc = arena.allocator(); - var buf = std.ArrayList(u8).init(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); defer buf.deinit(); var v: Self = undefined; try v.parseCLI(alloc, "echo hello"); - try v.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = echo hello\n", buf.items); + try v.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = echo hello\n", buf.written()); } test "Command: formatConfig direct" { @@ -307,13 +306,13 @@ pub const Command = union(enum) { defer arena.deinit(); const alloc = arena.allocator(); - var buf = std.ArrayList(u8).init(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); defer buf.deinit(); var v: Self = undefined; try v.parseCLI(alloc, "direct: echo hello"); - try v.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = direct:echo hello\n", buf.items); + try v.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = direct:echo hello\n", buf.written()); } }; diff --git a/src/config/edit.zig b/src/config/edit.zig index 38dc98169..6087106e7 100644 --- a/src/config/edit.zig +++ b/src/config/edit.zig @@ -4,6 +4,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const internal_os = @import("../os/main.zig"); +const file_load = @import("file_load.zig"); /// The path to the configuration that should be opened for editing. /// @@ -89,20 +90,16 @@ fn configPath(alloc_arena: Allocator) ![]const u8 { /// Returns a const list of possible paths the main config file could be /// in for the current OS. fn configPathCandidates(alloc_arena: Allocator) ![]const []const u8 { - var paths = try std.ArrayList([]const u8).initCapacity(alloc_arena, 2); - errdefer paths.deinit(); + var paths: std.ArrayList([]const u8) = try .initCapacity(alloc_arena, 4); + errdefer paths.deinit(alloc_arena); if (comptime builtin.os.tag == .macos) { - paths.appendAssumeCapacity(try internal_os.macos.appSupportDir( - alloc_arena, - "config", - )); + paths.appendAssumeCapacity(try file_load.defaultAppSupportPath(alloc_arena)); + paths.appendAssumeCapacity(try file_load.legacyDefaultAppSupportPath(alloc_arena)); } - paths.appendAssumeCapacity(try internal_os.xdg.config( - alloc_arena, - .{ .subdir = "ghostty/config" }, - )); + paths.appendAssumeCapacity(try file_load.defaultXdgPath(alloc_arena)); + paths.appendAssumeCapacity(try file_load.legacyDefaultXdgPath(alloc_arena)); return paths.items; } diff --git a/src/config/file_load.zig b/src/config/file_load.zig new file mode 100644 index 000000000..8dbefeea8 --- /dev/null +++ b/src/config/file_load.zig @@ -0,0 +1,166 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const internal_os = @import("../os/main.zig"); + +const log = std.log.scoped(.config); + +/// Default path for the XDG home configuration file. Returned value +/// must be freed by the caller. +pub fn defaultXdgPath(alloc: Allocator) ![]const u8 { + return try internal_os.xdg.config( + alloc, + .{ .subdir = "ghostty/config.ghostty" }, + ); +} + +/// Ghostty <1.3.0 default path for the XDG home configuration file. +/// Returned value must be freed by the caller. +pub fn legacyDefaultXdgPath(alloc: Allocator) ![]const u8 { + return try internal_os.xdg.config( + alloc, + .{ .subdir = "ghostty/config" }, + ); +} + +/// Preferred default path for the XDG home configuration file. +/// Returned value must be freed by the caller. +pub fn preferredXdgPath(alloc: Allocator) ![]const u8 { + // If the XDG path exists, use that. + const xdg_path = try defaultXdgPath(alloc); + if (open(xdg_path)) |f| { + f.close(); + return xdg_path; + } else |_| {} + + // Try the legacy path + errdefer alloc.free(xdg_path); + const legacy_xdg_path = try legacyDefaultXdgPath(alloc); + if (open(legacy_xdg_path)) |f| { + f.close(); + alloc.free(xdg_path); + return legacy_xdg_path; + } else |_| {} + + // Legacy path and XDG path both don't exist. Return the + // new one. + alloc.free(legacy_xdg_path); + return xdg_path; +} + +/// Default path for the macOS Application Support configuration file. +/// Returned value must be freed by the caller. +pub fn defaultAppSupportPath(alloc: Allocator) ![]const u8 { + return try internal_os.macos.appSupportDir(alloc, "config.ghostty"); +} + +/// Ghostty <1.3.0 default path for the macOS Application Support +/// configuration file. Returned value must be freed by the caller. +pub fn legacyDefaultAppSupportPath(alloc: Allocator) ![]const u8 { + return try internal_os.macos.appSupportDir(alloc, "config"); +} + +/// Preferred default path for the macOS Application Support configuration file. +/// Returned value must be freed by the caller. +pub fn preferredAppSupportPath(alloc: Allocator) ![]const u8 { + // If the app support path exists, use that. + const app_support_path = try defaultAppSupportPath(alloc); + if (open(app_support_path)) |f| { + f.close(); + return app_support_path; + } else |_| {} + + // Try the legacy path + errdefer alloc.free(app_support_path); + const legacy_app_support_path = try legacyDefaultAppSupportPath(alloc); + if (open(legacy_app_support_path)) |f| { + f.close(); + alloc.free(app_support_path); + return legacy_app_support_path; + } else |_| {} + + // Legacy path and app support path both don't exist. Return the + // new one. + alloc.free(legacy_app_support_path); + return app_support_path; +} + +/// Returns the path to the preferred default configuration file. +/// This is the file where users should place their configuration. +/// +/// This doesn't create or populate the file with any default +/// contents; downstream callers must handle this. +/// +/// The returned value must be freed by the caller. +pub fn preferredDefaultFilePath(alloc: Allocator) ![]const u8 { + switch (builtin.os.tag) { + .macos => { + // macOS prefers the Application Support directory + // if it exists. + const app_support_path = try preferredAppSupportPath(alloc); + const app_support_file = open(app_support_path) catch { + // Try the XDG path if it exists + const xdg_path = try preferredXdgPath(alloc); + const xdg_file = open(xdg_path) catch { + // If neither file exists, use app support + alloc.free(xdg_path); + return app_support_path; + }; + xdg_file.close(); + alloc.free(app_support_path); + return xdg_path; + }; + app_support_file.close(); + return app_support_path; + }, + + // All other platforms use XDG only + else => return try preferredXdgPath(alloc), + } +} + +const OpenFileError = error{ + FileNotFound, + FileIsEmpty, + FileOpenFailed, + NotAFile, +}; + +/// Opens the file at the given path and returns the file handle +/// if it exists and is non-empty. This also constrains the possible +/// errors to a smaller set that we can explicitly handle. +pub fn open(path: []const u8) OpenFileError!std.fs.File { + assert(std.fs.path.isAbsolute(path)); + + var file = std.fs.openFileAbsolute( + path, + .{}, + ) catch |err| switch (err) { + error.FileNotFound => return OpenFileError.FileNotFound, + else => { + log.warn("unexpected file open error path={s} err={}", .{ + path, + err, + }); + return OpenFileError.FileOpenFailed; + }, + }; + errdefer file.close(); + + const stat = file.stat() catch |err| { + log.warn("error getting file stat path={s} err={}", .{ + path, + err, + }); + return OpenFileError.FileOpenFailed; + }; + switch (stat.kind) { + .file => {}, + else => return OpenFileError.NotAFile, + } + + if (stat.size == 0) return OpenFileError.FileIsEmpty; + + return file; +} diff --git a/src/config/formatter.zig b/src/config/formatter.zig index a42395c19..dcf99167d 100644 --- a/src/config/formatter.zig +++ b/src/config/formatter.zig @@ -8,38 +8,36 @@ const Key = @import("key.zig").Key; /// Returns a single entry formatter for the given field name and writer. pub fn entryFormatter( name: []const u8, - writer: anytype, -) EntryFormatter(@TypeOf(writer)) { + writer: *std.Io.Writer, +) EntryFormatter { return .{ .name = name, .writer = writer }; } /// The entry formatter type for a given writer. -pub fn EntryFormatter(comptime WriterType: type) type { - return struct { - name: []const u8, - writer: WriterType, +pub const EntryFormatter = struct { + name: []const u8, + writer: *std.Io.Writer, - pub fn formatEntry( - self: @This(), - comptime T: type, - value: T, - ) !void { - return formatter.formatEntry( - T, - self.name, - value, - self.writer, - ); - } - }; -} + pub fn formatEntry( + self: @This(), + comptime T: type, + value: T, + ) !void { + return formatter.formatEntry( + T, + self.name, + value, + self.writer, + ); + } +}; /// Format a single type with the given name and value. pub fn formatEntry( comptime T: type, name: []const u8, value: T, - writer: anytype, + writer: *std.Io.Writer, ) !void { switch (@typeInfo(T)) { .bool, .int => { @@ -53,7 +51,7 @@ pub fn formatEntry( }, .@"enum" => { - try writer.print("{s} = {s}\n", .{ name, @tagName(value) }); + try writer.print("{s} = {t}\n", .{ name, value }); return; }, @@ -143,19 +141,14 @@ pub const FileFormatter = struct { /// Implements std.fmt so it can be used directly with std.fmt. pub fn format( self: FileFormatter, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, - ) !void { + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { @setEvalBranchQuota(10_000); - _ = layout; - _ = opts; - // If we're change-tracking then we need the default config to // compare against. var default: ?Config = if (self.changed) - try .default(self.alloc) + Config.default(self.alloc) catch return error.WriteFailed else null; defer if (default) |*v| v.deinit(); @@ -179,12 +172,12 @@ pub const FileFormatter = struct { } } - try formatEntry( + formatEntry( field.type, field.name, value, writer, - ); + ) catch return error.WriteFailed; if (do_docs) try writer.print("\n", .{}); } @@ -198,7 +191,7 @@ test "format default config" { var cfg = try Config.default(alloc); defer cfg.deinit(); - var buf = std.ArrayList(u8).init(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); defer buf.deinit(); // We just make sure this works without errors. We aren't asserting output. @@ -206,9 +199,9 @@ test "format default config" { .alloc = alloc, .config = &cfg, }; - try std.fmt.format(buf.writer(), "{}", .{fmt}); + try fmt.format(&buf.writer); - //std.log.warn("{s}", .{buf.items}); + //std.log.warn("{s}", .{buf.written()}); } test "format default config changed" { @@ -218,7 +211,7 @@ test "format default config changed" { defer cfg.deinit(); cfg.@"font-size" = 42; - var buf = std.ArrayList(u8).init(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); defer buf.deinit(); // We just make sure this works without errors. We aren't asserting output. @@ -227,26 +220,26 @@ test "format default config changed" { .config = &cfg, .changed = true, }; - try std.fmt.format(buf.writer(), "{}", .{fmt}); + try fmt.format(&buf.writer); - //std.log.warn("{s}", .{buf.items}); + //std.log.warn("{s}", .{buf.written()}); } test "formatEntry bool" { const testing = std.testing; { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry(bool, "a", true, buf.writer()); - try testing.expectEqualStrings("a = true\n", buf.items); + try formatEntry(bool, "a", true, &buf.writer); + try testing.expectEqualStrings("a = true\n", buf.written()); } { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry(bool, "a", false, buf.writer()); - try testing.expectEqualStrings("a = false\n", buf.items); + try formatEntry(bool, "a", false, &buf.writer); + try testing.expectEqualStrings("a = false\n", buf.written()); } } @@ -254,10 +247,10 @@ test "formatEntry int" { const testing = std.testing; { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry(u8, "a", 123, buf.writer()); - try testing.expectEqualStrings("a = 123\n", buf.items); + try formatEntry(u8, "a", 123, &buf.writer); + try testing.expectEqualStrings("a = 123\n", buf.written()); } } @@ -265,10 +258,10 @@ test "formatEntry float" { const testing = std.testing; { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry(f64, "a", 0.7, buf.writer()); - try testing.expectEqualStrings("a = 0.7\n", buf.items); + try formatEntry(f64, "a", 0.7, &buf.writer); + try testing.expectEqualStrings("a = 0.7\n", buf.written()); } } @@ -277,10 +270,10 @@ test "formatEntry enum" { const Enum = enum { one, two, three }; { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry(Enum, "a", .two, buf.writer()); - try testing.expectEqualStrings("a = two\n", buf.items); + try formatEntry(Enum, "a", .two, &buf.writer); + try testing.expectEqualStrings("a = two\n", buf.written()); } } @@ -288,10 +281,10 @@ test "formatEntry void" { const testing = std.testing; { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry(void, "a", {}, buf.writer()); - try testing.expectEqualStrings("a = \n", buf.items); + try formatEntry(void, "a", {}, &buf.writer); + try testing.expectEqualStrings("a = \n", buf.written()); } } @@ -299,17 +292,17 @@ test "formatEntry optional" { const testing = std.testing; { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry(?bool, "a", null, buf.writer()); - try testing.expectEqualStrings("a = \n", buf.items); + try formatEntry(?bool, "a", null, &buf.writer); + try testing.expectEqualStrings("a = \n", buf.written()); } { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry(?bool, "a", false, buf.writer()); - try testing.expectEqualStrings("a = false\n", buf.items); + try formatEntry(?bool, "a", false, &buf.writer); + try testing.expectEqualStrings("a = false\n", buf.written()); } } @@ -317,10 +310,10 @@ test "formatEntry string" { const testing = std.testing; { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry([]const u8, "a", "hello", buf.writer()); - try testing.expectEqualStrings("a = hello\n", buf.items); + try formatEntry([]const u8, "a", "hello", &buf.writer); + try testing.expectEqualStrings("a = hello\n", buf.written()); } } @@ -332,9 +325,9 @@ test "formatEntry packed struct" { }; { - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); - try formatEntry(Value, "a", .{}, buf.writer()); - try testing.expectEqualStrings("a = one,no-two\n", buf.items); + try formatEntry(Value, "a", .{}, &buf.writer); + try testing.expectEqualStrings("a = one,no-two\n", buf.written()); } } diff --git a/src/config/io.zig b/src/config/io.zig index 8be4be551..9d9a127e8 100644 --- a/src/config/io.zig +++ b/src/config/io.zig @@ -94,10 +94,9 @@ pub const ReadableIO = union(enum) { }; } - pub fn formatEntry(self: Self, formatter: anytype) !void { + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { var buf: [4096]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - const writer = fbs.writer(); + var writer: std.Io.Writer = .fixed(&buf); switch (self) { inline else => |v, tag| { writer.writeAll(@tagName(tag)) catch return error.OutOfMemory; @@ -106,10 +105,9 @@ pub const ReadableIO = union(enum) { }, } - const written = fbs.getWritten(); try formatter.formatEntry( []const u8, - written, + writer.buffered(), ); } @@ -144,13 +142,13 @@ pub const ReadableIO = union(enum) { defer arena.deinit(); const alloc = arena.allocator(); - var buf = std.ArrayList(u8).init(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); defer buf.deinit(); var v: Self = undefined; try v.parseCLI(alloc, "raw:foo"); - try v.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = raw:foo\n", buf.items); + try v.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = raw:foo\n", buf.written()); } }; @@ -222,7 +220,7 @@ pub const RepeatableReadableIO = struct { /// Used by Formatter pub fn formatEntry( self: Self, - formatter: anytype, + formatter: formatterpkg.EntryFormatter, ) !void { if (self.list.items.len == 0) { try formatter.formatEntry(void, {}); diff --git a/src/config/path.zig b/src/config/path.zig index 651dbdb3a..aeba69b94 100644 --- a/src/config/path.zig +++ b/src/config/path.zig @@ -79,7 +79,7 @@ pub const Path = union(enum) { } /// Used by formatter. - pub fn formatEntry(self: *const Path, formatter: anytype) !void { + pub fn formatEntry(self: *const Path, formatter: formatterpkg.EntryFormatter) !void { var buf: [std.fs.max_path_bytes + 1]u8 = undefined; const value = switch (self.*) { .optional => |path| std.fmt.bufPrint( @@ -154,10 +154,11 @@ pub const Path = union(enum) { &buf, ) catch |err| { try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "error expanding home directory for path {s}: {}", .{ path, err }, + 0, ), }); @@ -194,10 +195,11 @@ pub const Path = union(enum) { } try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "error resolving file path {s}: {}", .{ path, err }, + 0, ), }); @@ -306,7 +308,7 @@ pub const Path = union(enum) { test "formatConfig single item" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -315,13 +317,13 @@ pub const Path = union(enum) { var item: Path = undefined; try item.parseCLI(alloc, "A"); - try item.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = A\n", buf.items); + try item.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = A\n", buf.written()); } test "formatConfig multiple items" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -331,8 +333,8 @@ pub const Path = union(enum) { var item: Path = undefined; try item.parseCLI(alloc, "A"); try item.parseCLI(alloc, "?B"); - try item.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = ?B\n", buf.items); + try item.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = ?B\n", buf.written()); } }; @@ -382,7 +384,7 @@ pub const RepeatablePath = struct { } /// Used by Formatter - pub fn formatEntry(self: RepeatablePath, formatter: anytype) !void { + pub fn formatEntry(self: RepeatablePath, formatter: formatterpkg.EntryFormatter) !void { if (self.value.items.len == 0) { try formatter.formatEntry(void, {}); return; @@ -453,17 +455,17 @@ pub const RepeatablePath = struct { test "formatConfig empty" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var list: RepeatablePath = .{}; - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = \n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = \n", buf.written()); } test "formatConfig single item" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -472,13 +474,13 @@ pub const RepeatablePath = struct { var list: RepeatablePath = .{}; try list.parseCLI(alloc, "A"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = A\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = A\n", buf.written()); } test "formatConfig multiple items" { const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); var arena = ArenaAllocator.init(testing.allocator); @@ -488,7 +490,7 @@ pub const RepeatablePath = struct { var list: RepeatablePath = .{}; try list.parseCLI(alloc, "A"); try list.parseCLI(alloc, "?B"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = A\na = ?B\n", buf.items); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = A\na = ?B\n", buf.written()); } }; diff --git a/src/config/theme.zig b/src/config/theme.zig index 8fa7c93dc..b1188a5c4 100644 --- a/src/config/theme.zig +++ b/src/config/theme.zig @@ -125,10 +125,11 @@ pub fn open( ) orelse return null; const stat = file.stat() catch |err| { try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "not reading theme from \"{s}\": {}", .{ theme, err }, + 0, ), }); return null; @@ -137,10 +138,11 @@ pub fn open( .file => {}, else => { try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "not reading theme from \"{s}\": it is a {s}", .{ theme, @tagName(stat.kind) }, + 0, ), }); return null; @@ -152,10 +154,11 @@ pub fn open( const basename = std.fs.path.basename(theme); if (!std.mem.eql(u8, theme, basename)) { try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "theme \"{s}\" cannot include path separators unless it is an absolute path", .{theme}, + 0, ), }); return null; @@ -170,10 +173,11 @@ pub fn open( if (cwd.openFile(path, .{})) |file| { const stat = file.stat() catch |err| { try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "not reading theme from \"{s}\": {}", .{ theme, err }, + 0, ), }); return null; @@ -182,10 +186,11 @@ pub fn open( .file => {}, else => { try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "not reading theme from \"{s}\": it is a {s}", .{ theme, @tagName(stat.kind) }, + 0, ), }); return null; @@ -202,10 +207,11 @@ pub fn open( // Anything else is an error we log and give up on. else => { try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "failed to load theme \"{s}\" from the file \"{s}\": {}", .{ theme, path, err }, + 0, ), }); @@ -222,10 +228,11 @@ pub fn open( while (try it.next()) |loc| { const path = try std.fs.path.join(arena_alloc, &.{ loc.dir, theme }); try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "theme \"{s}\" not found, tried path \"{s}\"", .{ theme, path }, + 0, ), }); } @@ -249,17 +256,19 @@ pub fn openAbsolute( return std.fs.openFileAbsolute(theme, .{}) catch |err| { switch (err) { error.FileNotFound => try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "failed to load theme from the path \"{s}\"", .{theme}, + 0, ), }), else => try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( + .message = try std.fmt.allocPrintSentinel( arena_alloc, "failed to load theme from the path \"{s}\": {}", .{ theme, err }, + 0, ), }), } diff --git a/src/crash/main.zig b/src/crash/main.zig index 5f9aa96c5..1ac971851 100644 --- a/src/crash/main.zig +++ b/src/crash/main.zig @@ -5,7 +5,6 @@ const dir = @import("dir.zig"); const sentry_envelope = @import("sentry_envelope.zig"); -pub const minidump = @import("minidump.zig"); pub const sentry = @import("sentry.zig"); pub const Envelope = sentry_envelope.Envelope; pub const defaultDir = dir.defaultDir; diff --git a/src/crash/minidump.zig b/src/crash/minidump.zig deleted file mode 100644 index 0abd67eae..000000000 --- a/src/crash/minidump.zig +++ /dev/null @@ -1,7 +0,0 @@ -pub const reader = @import("minidump/reader.zig"); -pub const stream = @import("minidump/stream.zig"); -pub const Reader = reader.Reader; - -test { - @import("std").testing.refAllDecls(@This()); -} diff --git a/src/crash/minidump/external.zig b/src/crash/minidump/external.zig deleted file mode 100644 index 451810883..000000000 --- a/src/crash/minidump/external.zig +++ /dev/null @@ -1,59 +0,0 @@ -//! This file contains the external structs and constants for the minidump -//! format. Most are from the Microsoft documentation on the minidump format: -//! https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ -//! -//! Wherever possible, we also compare our definitions to other projects -//! such as rust-minidump, libmdmp, breakpad, etc. to ensure we're doing -//! the right thing. - -/// "MDMP" in little-endian. -pub const signature = 0x504D444D; - -/// The version of the minidump format. -pub const version = 0xA793; - -/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_header -pub const Header = extern struct { - signature: u32, - version: packed struct(u32) { low: u16, high: u16 }, - stream_count: u32, - stream_directory_rva: u32, - checksum: u32, - time_date_stamp: u32, - flags: u64, -}; - -/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_directory -pub const Directory = extern struct { - stream_type: u32, - location: LocationDescriptor, -}; - -/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_location_descriptor -pub const LocationDescriptor = extern struct { - data_size: u32, - rva: u32, -}; - -/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_memory_descriptor -pub const MemoryDescriptor = extern struct { - start_of_memory_range: u64, - memory: LocationDescriptor, -}; - -/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_thread_list -pub const ThreadList = extern struct { - number_of_threads: u32, - threads: [1]Thread, -}; - -/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_thread -pub const Thread = extern struct { - thread_id: u32, - suspend_count: u32, - priority_class: u32, - priority: u32, - teb: u64, - stack: MemoryDescriptor, - thread_context: LocationDescriptor, -}; diff --git a/src/crash/minidump/reader.zig b/src/crash/minidump/reader.zig deleted file mode 100644 index b7f5efe80..000000000 --- a/src/crash/minidump/reader.zig +++ /dev/null @@ -1,242 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const external = @import("external.zig"); -const stream = @import("stream.zig"); -const EncodedStream = stream.EncodedStream; - -const log = std.log.scoped(.minidump_reader); - -/// Possible minidump-specific errors that can occur when reading a minidump. -/// This isn't the full error set since IO errors can also occur depending -/// on the Source type. -pub const ReadError = error{ - InvalidHeader, - InvalidVersion, - StreamSizeMismatch, -}; - -/// Reader creates a new minidump reader for the given source type. The -/// source must have both a "reader()" and "seekableStream()" function. -/// -/// Given the format of a minidump file, we must keep the source open and -/// continually access it because the format of the minidump is full of -/// pointers and offsets that we must follow depending on the stream types. -/// Also, since we're not aware of all stream types (in fact its impossible -/// to be aware since custom stream types are allowed), its possible any stream -/// type can define their own pointers and offsets. So, the source must always -/// be available so callers can decode the streams as needed. -pub fn Reader(comptime S: type) type { - return struct { - const Self = @This(); - - /// The source data. - source: Source, - - /// The endianness of the minidump file. This is detected by reading - /// the byte order of the header. - endian: std.builtin.Endian, - - /// The number of streams within the minidump file. This is read from - /// the header and stored here so we can quickly access them. Note - /// the stream types require reading the source; this is an optimization - /// to avoid any allocations on the reader and the caller can choose - /// to store them if they want. - stream_count: u32, - stream_directory_rva: u32, - - const SourceCallable = switch (@typeInfo(Source)) { - .pointer => |v| v.child, - .@"struct" => Source, - else => @compileError("Source type must be a pointer or struct"), - }; - - const SourceReader = @typeInfo(@TypeOf(SourceCallable.reader)).@"fn".return_type.?; - const SourceSeeker = @typeInfo(@TypeOf(SourceCallable.seekableStream)).@"fn".return_type.?; - - /// A limited reader for reading data from the source. - pub const LimitedReader = std.io.LimitedReader(SourceReader); - - /// The source type for the reader. - pub const Source = S; - - /// The stream types for reading - pub const ThreadList = stream.thread_list.ThreadListReader(Self); - - /// The reader type for stream reading. This has some other methods so - /// you must still call reader() on the result to get the actual - /// reader to read the data. - pub const StreamReader = struct { - source: Source, - endian: std.builtin.Endian, - directory: external.Directory, - - /// Should not be accessed directly. This is setup whenever - /// reader() is called. - limit_reader: LimitedReader = undefined, - - pub const Reader = LimitedReader.Reader; - - /// Returns a Reader implementation that reads the bytes of the - /// stream. - /// - /// The reader is dependent on the state of Source so any - /// state-changing operations on Source will invalidate the - /// reader. For example, making another reader, reading another - /// stream directory, closing the source, etc. - pub fn reader(self: *StreamReader) LimitedReader.Reader { - try self.source.seekableStream().seekTo(self.directory.location.rva); - self.limit_reader = .{ - .inner_reader = self.source.reader(), - .bytes_left = self.directory.location.data_size, - }; - return self.limit_reader.reader(); - } - - /// Seeks the source to the location of the directory. - pub fn seekToPayload(self: *StreamReader) !void { - try self.source.seekableStream().seekTo(self.directory.location.rva); - } - }; - - /// Iterator type to read over the streams in the minidump file. - pub const StreamIterator = struct { - reader: *const Self, - i: u32 = 0, - - pub fn next(self: *StreamIterator) !?StreamReader { - if (self.i >= self.reader.stream_count) return null; - const dir = try self.reader.directory(self.i); - self.i += 1; - return try self.reader.streamReader(dir); - } - }; - - /// Initialize a reader. The source must remain available for the entire - /// lifetime of the reader. The reader does not take ownership of the - /// source so if it has resources that need to be cleaned up, the caller - /// must do so once the reader is no longer needed. - pub fn init(source: Source) !Self { - const header, const endian = try readHeader(Source, source); - return .{ - .source = source, - .endian = endian, - .stream_count = header.stream_count, - .stream_directory_rva = header.stream_directory_rva, - }; - } - - /// Return an iterator to read over the streams in the minidump file. - /// This is very similar to using a simple for loop to stream_count - /// and calling directory() on each index, but is more idiomatic - /// Zig. - pub fn streamIterator(self: *const Self) StreamIterator { - return .{ .reader = self }; - } - - /// Return a StreamReader for the given directory type. This streams - /// from the underlying source so the returned reader is only valid - /// as long as the source is unmodified (i.e. the source is not - /// closed, the source seek position is not moved, etc.). - pub fn streamReader( - self: *const Self, - dir: external.Directory, - ) SourceSeeker.SeekError!StreamReader { - return .{ - .source = self.source, - .endian = self.endian, - .directory = dir, - }; - } - - /// Get the directory entry with the given index. - /// - /// Asserts the index is valid (idx < stream_count). - pub fn directory(self: *const Self, idx: usize) !external.Directory { - assert(idx < self.stream_count); - - // Seek to the directory. - const offset: u32 = @intCast(@sizeOf(external.Directory) * idx); - const rva: u32 = self.stream_directory_rva + offset; - try self.source.seekableStream().seekTo(rva); - - // Read the directory. - return try self.source.reader().readStructEndian( - external.Directory, - self.endian, - ); - } - - /// Return a reader for the given location descriptor. This is only - /// valid until the reader source is modified in some way. - pub fn locationReader( - self: *const Self, - loc: external.LocationDescriptor, - ) !LimitedReader { - try self.source.seekableStream().seekTo(loc.rva); - return .{ - .inner_reader = self.source.reader(), - .bytes_left = loc.data_size, - }; - } - }; -} - -/// Reads the header for the minidump file and returns endianness of -/// the file. -fn readHeader(comptime T: type, source: T) !struct { - external.Header, - std.builtin.Endian, -} { - // Start by trying LE. - var endian: std.builtin.Endian = .little; - var header = try source.reader().readStructEndian(external.Header, endian); - - // If the signature doesn't match, we assume its BE. - if (header.signature != external.signature) { - // Seek back to the start of the file so we can reread. - try source.seekableStream().seekTo(0); - - // Try BE, if the signature doesn't match, return an error. - endian = .big; - header = try source.reader().readStructEndian(external.Header, endian); - if (header.signature != external.signature) return ReadError.InvalidHeader; - } - - // "The low-order word is MINIDUMP_VERSION. The high-order word is an - // internal value that is implementation specific." - if (header.version.low != external.version) return ReadError.InvalidVersion; - - return .{ header, endian }; -} - -// Uncomment to dump some debug information for a minidump file. -test "minidump debug" { - var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp")); - const r = try Reader(*@TypeOf(fbs)).init(&fbs); - var it = r.streamIterator(); - while (try it.next()) |s| { - log.warn("directory i={} dir={}", .{ it.i - 1, s.directory }); - } -} - -test "minidump read" { - const testing = std.testing; - const alloc = testing.allocator; - - var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp")); - const r = try Reader(*@TypeOf(fbs)).init(&fbs); - try testing.expectEqual(std.builtin.Endian.little, r.endian); - try testing.expectEqual(7, r.stream_count); - { - const dir = try r.directory(0); - try testing.expectEqual(3, dir.stream_type); - try testing.expectEqual(584, dir.location.data_size); - - var bytes = std.ArrayList(u8).init(alloc); - defer bytes.deinit(); - var sr = try r.streamReader(dir); - try sr.reader().readAllArrayList(&bytes, std.math.maxInt(usize)); - try testing.expectEqual(584, bytes.items.len); - } -} diff --git a/src/crash/minidump/stream.zig b/src/crash/minidump/stream.zig deleted file mode 100644 index 00ec6b042..000000000 --- a/src/crash/minidump/stream.zig +++ /dev/null @@ -1,30 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; - -const log = std.log.scoped(.minidump_stream); - -/// The known stream types. -pub const thread_list = @import("stream_threadlist.zig"); - -/// A stream within the minidump file. A stream can be either in an encoded -/// form or decoded form. The encoded form are raw bytes and aren't validated -/// until they're decoded. The decoded form is a structured form of the stream. -/// -/// The decoded form is more ergonomic to work with but the encoded form is -/// more efficient to read/write. -pub const Stream = union(enum) { - encoded: EncodedStream, -}; - -/// An encoded stream value. It is "encoded" in the sense that it is raw bytes -/// with a type associated. The raw bytes are not validated to be correct for -/// the type. -pub const EncodedStream = struct { - type: u32, - data: []const u8, -}; - -test { - @import("std").testing.refAllDecls(@This()); -} diff --git a/src/crash/minidump/stream_threadlist.zig b/src/crash/minidump/stream_threadlist.zig deleted file mode 100644 index 51f3f9d4c..000000000 --- a/src/crash/minidump/stream_threadlist.zig +++ /dev/null @@ -1,117 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; -const external = @import("external.zig"); -const readerpkg = @import("reader.zig"); -const Reader = readerpkg.Reader; -const ReadError = readerpkg.ReadError; - -const log = std.log.scoped(.minidump_stream); - -/// This is the list of threads from the process. -/// -/// This is the Reader implementation. You usually do not use this directly. -/// Instead, use Reader(T).ThreadList which will get you the same thing. -/// -/// ThreadList is stream type 0x3. -/// StreamReader is the Reader(T).StreamReader type. -pub fn ThreadListReader(comptime R: type) type { - return struct { - const Self = @This(); - - /// The number of threads in the list. - count: u32, - - /// The rva to the first thread in the list. - rva: u32, - - /// Source data and endianness so we can read. - source: R.Source, - endian: std.builtin.Endian, - - pub fn init(r: *R.StreamReader) !Self { - assert(r.directory.stream_type == 0x3); - try r.seekToPayload(); - const reader = r.source.reader(); - - // Our count is always a u32 in the header. - const count = try reader.readInt(u32, r.endian); - - // Determine if we have padding in our header. It is possible - // for there to be padding if the list header was written by - // a 32-bit process but is being read on a 64-bit process. - const padding = padding: { - const maybe_size = @sizeOf(u32) + (@sizeOf(external.Thread) * count); - switch (std.math.order(maybe_size, r.directory.location.data_size)) { - // It should never be larger than what the directory says. - .gt => return ReadError.StreamSizeMismatch, - - // If the sizes match exactly we're good. - .eq => break :padding 0, - - .lt => { - const padding = r.directory.location.data_size - maybe_size; - if (padding != 4) return ReadError.StreamSizeMismatch; - break :padding padding; - }, - } - }; - - // Rva is the location of the first thread in the list. - const rva = r.directory.location.rva + @as(u32, @sizeOf(u32)) + padding; - - return .{ - .count = count, - .rva = rva, - .source = r.source, - .endian = r.endian, - }; - } - - /// Get the thread entry for the given index. - /// - /// Index is asserted to be less than count. - pub fn thread(self: *const Self, i: usize) !external.Thread { - assert(i < self.count); - - // Seek to the thread - const offset: u32 = @intCast(@sizeOf(external.Thread) * i); - const rva: u32 = self.rva + offset; - try self.source.seekableStream().seekTo(rva); - - // Read the thread - return try self.source.reader().readStructEndian( - external.Thread, - self.endian, - ); - } - }; -} - -test "minidump: threadlist" { - const testing = std.testing; - const alloc = testing.allocator; - - var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp")); - const R = Reader(*@TypeOf(fbs)); - const r = try R.init(&fbs); - - // Get our thread list stream - const dir = try r.directory(0); - try testing.expectEqual(3, dir.stream_type); - var sr = try r.streamReader(dir); - - // Get our rich structure - const v = try R.ThreadList.init(&sr); - log.warn("threadlist count={} rva={}", .{ v.count, v.rva }); - - try testing.expectEqual(12, v.count); - for (0..v.count) |i| { - const t = try v.thread(i); - log.warn("thread i={} thread={}", .{ i, t }); - - // Read our stack memory - var stack_reader = try r.locationReader(t.stack.memory); - const bytes = try stack_reader.reader().readAllAlloc(alloc, t.stack.memory.data_size); - defer alloc.free(bytes); - } -} diff --git a/src/crash/sentry.zig b/src/crash/sentry.zig index 820c3e9a1..555b70fe9 100644 --- a/src/crash/sentry.zig +++ b/src/crash/sentry.zig @@ -265,8 +265,8 @@ pub const Transport = struct { const json = envelope.serialize(); defer sentry.free(@ptrCast(json.ptr)); var parsed: crash.Envelope = parsed: { - var fbs = std.io.fixedBufferStream(json); - break :parsed try crash.Envelope.parse(alloc, fbs.reader()); + var reader: std.Io.Reader = .fixed(json); + break :parsed try crash.Envelope.parse(alloc, &reader); }; defer parsed.deinit(); @@ -298,7 +298,10 @@ pub const Transport = struct { }); const file = try std.fs.cwd().createFile(path, .{}); defer file.close(); - try file.writer().writeAll(json); + var buf: [4096]u8 = undefined; + var file_writer = file.writer(&buf); + try file_writer.interface.writeAll(json); + try file_writer.end(); log.warn("crash report written to disk path={s}", .{path}); } diff --git a/src/crash/sentry_envelope.zig b/src/crash/sentry_envelope.zig index 6b675554c..08573b739 100644 --- a/src/crash/sentry_envelope.zig +++ b/src/crash/sentry_envelope.zig @@ -26,7 +26,7 @@ pub const Envelope = struct { headers: std.json.ObjectMap, /// The items in the envelope in the order they're encoded. - items: std.ArrayListUnmanaged(Item), + items: std.ArrayList(Item), /// Parse an envelope from a reader. /// @@ -37,7 +37,7 @@ pub const Envelope = struct { /// parsing in our use case is not a hot path. pub fn parse( alloc_gpa: Allocator, - reader: anytype, + reader: *std.Io.Reader, ) !Envelope { // We use an arena allocator to read from reader. We pair this // with `alloc_if_needed` when parsing json to allow the json @@ -62,23 +62,24 @@ pub const Envelope = struct { fn parseHeader( alloc: Allocator, - reader: anytype, + reader: *std.Io.Reader, ) !std.json.ObjectMap { - var buf: std.ArrayListUnmanaged(u8) = .{}; - reader.streamUntilDelimiter( - buf.writer(alloc), + var buf: std.Io.Writer.Allocating = .init(alloc); + _ = try reader.streamDelimiterLimit( + &buf.writer, '\n', - 1024 * 1024, // 1MB, arbitrary choice - ) catch |err| switch (err) { - // Envelope can be header-only. + .limited(1024 * 1024), // 1MB, arbitrary choice + ); + _ = reader.discardDelimiterInclusive('\n') catch |err| switch (err) { + // It's okay if there isn't a trailing newline error.EndOfStream => {}, - else => |v| return v, + else => return err, }; const value = try std.json.parseFromSliceLeaky( std.json.Value, alloc, - buf.items, + buf.written(), .{ .allocate = .alloc_if_needed }, ); @@ -90,9 +91,9 @@ pub const Envelope = struct { fn parseItems( alloc: Allocator, - reader: anytype, - ) !std.ArrayListUnmanaged(Item) { - var items: std.ArrayListUnmanaged(Item) = .{}; + reader: *std.Io.Reader, + ) !std.ArrayList(Item) { + var items: std.ArrayList(Item) = .{}; errdefer items.deinit(alloc); while (try parseOneItem(alloc, reader)) |item| { try items.append(alloc, item); @@ -103,22 +104,27 @@ pub const Envelope = struct { fn parseOneItem( alloc: Allocator, - reader: anytype, + reader: *std.Io.Reader, ) !?Item { // Get the next item which must start with a header. - var buf: std.ArrayListUnmanaged(u8) = .{}; - reader.streamUntilDelimiter( - buf.writer(alloc), + var buf: std.Io.Writer.Allocating = .init(alloc); + _ = reader.streamDelimiterLimit( + &buf.writer, '\n', - 1024 * 1024, // 1MB, arbitrary choice + .limited(1024 * 1024), // 1MB, arbitrary choice ) catch |err| switch (err) { - error.EndOfStream => return null, - else => |v| return v, + error.StreamTooLong => return null, + else => return err, + }; + _ = reader.discardDelimiterInclusive('\n') catch |err| switch (err) { + // It's okay if there isn't a trailing newline + error.EndOfStream => {}, + else => return err, }; // Parse the header JSON const headers: std.json.ObjectMap = headers: { - const line = std.mem.trim(u8, buf.items, " \t"); + const line = std.mem.trim(u8, buf.written(), " \t"); if (line.len == 0) return null; const value = try std.json.parseFromSliceLeaky( @@ -156,18 +162,16 @@ pub const Envelope = struct { // Get the payload const payload: []const u8 = if (len_) |len| payload: { // The payload length is specified so read the exact length. - var payload = std.ArrayList(u8).init(alloc); + var payload: std.Io.Writer.Allocating = .init(alloc); defer payload.deinit(); - for (0..len) |_| { - const byte = reader.readByte() catch |err| switch (err) { - error.EndOfStream => return error.EnvelopeItemPayloadTooShort, - else => return err, - }; - try payload.append(byte); - } + + reader.streamExact(&payload.writer, len) catch |err| switch (err) { + error.EndOfStream => return error.EnvelopeItemPayloadTooShort, + else => return err, + }; // The next byte must be a newline. - if (reader.readByte()) |byte| { + if (reader.takeByte()) |byte| { if (byte != '\n') return error.EnvelopeItemPayloadNoNewline; } else |err| switch (err) { error.EndOfStream => {}, @@ -177,16 +181,20 @@ pub const Envelope = struct { break :payload try payload.toOwnedSlice(); } else payload: { // The payload is the next line ending in `\n`. It is required. - var payload = std.ArrayList(u8).init(alloc); - defer payload.deinit(); - reader.streamUntilDelimiter( - payload.writer(), + var payload: std.Io.Writer.Allocating = .init(alloc); + _ = reader.streamDelimiterLimit( + &payload.writer, '\n', - 1024 * 1024 * 50, // 50MB, arbitrary choice + .limited(1024 * 1024), // 50MB, arbitrary choice ) catch |err| switch (err) { - error.EndOfStream => return error.EnvelopeItemPayloadTooShort, + error.StreamTooLong => return error.EnvelopeItemPayloadTooShort, else => |v| return v, }; + _ = reader.discardDelimiterInclusive('\n') catch |err| switch (err) { + // It's okay if there isn't a trailing newline + error.EndOfStream => {}, + else => return err, + }; break :payload try payload.toOwnedSlice(); }; @@ -212,15 +220,13 @@ pub const Envelope = struct { /// therefore may allocate. pub fn serialize( self: *Envelope, - writer: anytype, + writer: *std.Io.Writer, ) !void { // Header line first - try std.json.stringify( + try writer.print("{f}\n", .{std.json.fmt( std.json.Value{ .object = self.headers }, json_opts, - writer, - ); - try writer.writeByte('\n'); + )}); // Write each item const alloc = self.allocator(); @@ -230,13 +236,13 @@ pub const Envelope = struct { const encoded = try item.encode(alloc); assert(item.* == .encoded); - try std.json.stringify( - std.json.Value{ .object = encoded.headers }, - json_opts, - writer, - ); - try writer.writeByte('\n'); - try writer.writeAll(encoded.payload); + try writer.print("{f}\n{s}", .{ + std.json.fmt( + std.json.Value{ .object = encoded.headers }, + json_opts, + ), + encoded.payload, + }); } } }; @@ -425,7 +431,7 @@ pub const Attachment = struct { pub const ObjectMapUnmanaged = std.StringArrayHashMapUnmanaged(std.json.Value); /// The options we must use for serialization. -const json_opts: std.json.StringifyOptions = .{ +const json_opts: std.json.Stringify.Options = .{ // This is the default but I want to be explicit because its // VERY important for the correctness of the envelope. This is // the only whitespace type in std.json that doesn't emit newlines. @@ -437,10 +443,10 @@ test "Envelope parse" { const testing = std.testing; const alloc = testing.allocator; - var fbs = std.io.fixedBufferStream( + var reader: std.Io.Reader = .fixed( \\{} ); - var v = try Envelope.parse(alloc, fbs.reader()); + var v = try Envelope.parse(alloc, &reader); defer v.deinit(); } @@ -448,12 +454,12 @@ test "Envelope parse session" { const testing = std.testing; const alloc = testing.allocator; - var fbs = std.io.fixedBufferStream( + var reader: std.Io.Reader = .fixed( \\{} \\{"type":"session","length":218} \\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}} ); - var v = try Envelope.parse(alloc, fbs.reader()); + var v = try Envelope.parse(alloc, &reader); defer v.deinit(); try testing.expectEqual(@as(usize, 1), v.items.items.len); @@ -464,14 +470,14 @@ test "Envelope parse multiple" { const testing = std.testing; const alloc = testing.allocator; - var fbs = std.io.fixedBufferStream( + var reader: std.Io.Reader = .fixed( \\{} \\{"type":"session","length":218} \\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}} \\{"type":"attachment","length":4,"filename":"test.txt"} \\ABCD ); - var v = try Envelope.parse(alloc, fbs.reader()); + var v = try Envelope.parse(alloc, &reader); defer v.deinit(); try testing.expectEqual(@as(usize, 2), v.items.items.len); @@ -483,14 +489,14 @@ test "Envelope parse multiple no length" { const testing = std.testing; const alloc = testing.allocator; - var fbs = std.io.fixedBufferStream( + var reader: std.Io.Reader = .fixed( \\{} \\{"type":"session"} \\{} \\{"type":"attachment","length":4,"filename":"test.txt"} \\ABCD ); - var v = try Envelope.parse(alloc, fbs.reader()); + var v = try Envelope.parse(alloc, &reader); defer v.deinit(); try testing.expectEqual(@as(usize, 2), v.items.items.len); @@ -502,13 +508,13 @@ test "Envelope parse end in new line" { const testing = std.testing; const alloc = testing.allocator; - var fbs = std.io.fixedBufferStream( + var reader: std.Io.Reader = .fixed( \\{} \\{"type":"session","length":218} \\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}} \\ ); - var v = try Envelope.parse(alloc, fbs.reader()); + var v = try Envelope.parse(alloc, &reader); defer v.deinit(); try testing.expectEqual(@as(usize, 1), v.items.items.len); @@ -519,12 +525,12 @@ test "Envelope parse attachment" { const testing = std.testing; const alloc = testing.allocator; - var fbs = std.io.fixedBufferStream( + var reader: std.Io.Reader = .fixed( \\{} \\{"type":"attachment","length":4,"filename":"test.txt"} \\ABCD ); - var v = try Envelope.parse(alloc, fbs.reader()); + var v = try Envelope.parse(alloc, &reader); defer v.deinit(); try testing.expectEqual(@as(usize, 1), v.items.items.len); @@ -537,14 +543,14 @@ test "Envelope parse attachment" { // Serialization test { - var output = std.ArrayList(u8).init(alloc); + var output: std.Io.Writer.Allocating = .init(alloc); defer output.deinit(); - try v.serialize(output.writer()); + try v.serialize(&output.writer); try testing.expectEqualStrings( \\{} \\{"type":"attachment","length":4,"filename":"test.txt"} \\ABCD - , std.mem.trim(u8, output.items, "\n")); + , std.mem.trim(u8, output.written(), "\n")); } } @@ -552,76 +558,40 @@ test "Envelope serialize empty" { const testing = std.testing; const alloc = testing.allocator; - var fbs = std.io.fixedBufferStream( + var reader: std.Io.Reader = .fixed( \\{} ); - var v = try Envelope.parse(alloc, fbs.reader()); + var v = try Envelope.parse(alloc, &reader); defer v.deinit(); - var output = std.ArrayList(u8).init(alloc); + var output: std.Io.Writer.Allocating = .init(alloc); defer output.deinit(); - try v.serialize(output.writer()); + try v.serialize(&output.writer); try testing.expectEqualStrings( \\{} - , std.mem.trim(u8, output.items, "\n")); + , std.mem.trim(u8, output.written(), "\n")); } test "Envelope serialize session" { const testing = std.testing; const alloc = testing.allocator; - var fbs = std.io.fixedBufferStream( + var reader: std.Io.Reader = .fixed( \\{} \\{"type":"session","length":218} \\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}} ); - var v = try Envelope.parse(alloc, fbs.reader()); + var v = try Envelope.parse(alloc, &reader); defer v.deinit(); - var output = std.ArrayList(u8).init(alloc); + var output: std.Io.Writer.Allocating = .init(alloc); defer output.deinit(); - try v.serialize(output.writer()); + try v.serialize(&output.writer); try testing.expectEqualStrings( \\{} \\{"type":"session","length":218} \\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}} - , std.mem.trim(u8, output.items, "\n")); + , std.mem.trim(u8, output.written(), "\n")); } - -// // Uncomment this test if you want to extract a minidump file from an -// // existing envelope. This is useful for getting new test contents. -// test "Envelope extract mdmp" { -// const testing = std.testing; -// const alloc = testing.allocator; -// -// var fbs = std.io.fixedBufferStream(@embedFile("in.crash")); -// var v = try Envelope.parse(alloc, fbs.reader()); -// defer v.deinit(); -// -// try testing.expect(v.items.items.len > 0); -// for (v.items.items, 0..) |*item, i| { -// if (item.encoded.type != .attachment) { -// log.warn("ignoring item type={} i={}", .{ item.encoded.type, i }); -// continue; -// } -// -// try item.decode(v.allocator()); -// const attach = item.attachment; -// const attach_type = attach.type orelse { -// log.warn("attachment missing type i={}", .{i}); -// continue; -// }; -// if (!std.mem.eql(u8, attach_type, "event.minidump")) { -// log.warn("ignoring attachment type={s} i={}", .{ attach_type, i }); -// continue; -// } -// -// log.warn("found minidump i={}", .{i}); -// var f = try std.fs.cwd().createFile("out.mdmp", .{}); -// defer f.close(); -// try f.writer().writeAll(attach.payload); -// return; -// } -// } diff --git a/src/datastruct/comparison.zig b/src/datastruct/comparison.zig new file mode 100644 index 000000000..4427c143c --- /dev/null +++ b/src/datastruct/comparison.zig @@ -0,0 +1,147 @@ +// The contents of this file is largely based on testing.zig from the Zig 0.15.1 +// stdlib, distributed under the MIT license, copyright (c) Zig contributors +const std = @import("std"); + +/// Generic, recursive equality testing utility using approximate comparison for +/// floats and equality for everything else +/// +/// Based on `std.testing.expectEqual` and `std.testing.expectEqualSlices`. +/// +/// The relative tolerance is currently hardcoded to `sqrt(eps(float_type))`. +pub inline fn expectApproxEqual(expected: anytype, actual: anytype) !void { + const T = @TypeOf(expected, actual); + return expectApproxEqualInner(T, expected, actual); +} + +fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void { + switch (@typeInfo(T)) { + // check approximate equality for floats + .float => { + const sqrt_eps = comptime std.math.sqrt(std.math.floatEps(T)); + if (!std.math.approxEqRel(T, expected, actual, sqrt_eps)) { + print("expected approximately {any}, found {any}\n", .{ expected, actual }); + return error.TestExpectedApproxEqual; + } + }, + + // recurse into containers + .array => { + const diff_index: usize = diff_index: { + const shortest = @min(expected.len, actual.len); + var index: usize = 0; + while (index < shortest) : (index += 1) { + expectApproxEqual(actual[index], expected[index]) catch break :diff_index index; + } + break :diff_index if (expected.len == actual.len) return else shortest; + }; + print("slices not approximately equal. first significant difference occurs at index {d} (0x{X})\n", .{ diff_index, diff_index }); + return error.TestExpectedApproxEqual; + }, + .vector => |info| { + var i: usize = 0; + while (i < info.len) : (i += 1) { + expectApproxEqual(expected[i], actual[i]) catch { + print("index {d} incorrect. expected approximately {any}, found {any}\n", .{ + i, expected[i], actual[i], + }); + return error.TestExpectedApproxEqual; + }; + } + }, + .@"struct" => |structType| { + inline for (structType.fields) |field| { + try expectApproxEqual(@field(expected, field.name), @field(actual, field.name)); + } + }, + + // unwrap unions, optionals, and error unions + .@"union" => |union_info| { + 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 { + return error.TestExpectedApproxEqual; + }; + } + + const Tag = std.meta.Tag(@TypeOf(expected)); + + const expectedTag = @as(Tag, expected); + const actualTag = @as(Tag, actual); + + std.testing.expectEqual(expectedTag, actualTag) catch { + return error.TestExpectedApproxEqual; + }; + + // we only reach this switch if the tags are equal + switch (expected) { + inline else => |val, tag| try expectApproxEqual(val, @field(actual, @tagName(tag))), + } + }, + .optional, .error_union => { + if (expected) |expected_payload| if (actual) |actual_payload| { + return expectApproxEqual(expected_payload, actual_payload); + }; + // 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 { + return error.TestExpectedApproxEqual; + }; + }, + + // fall back to expectEqual for everything else + else => std.testing.expectEqual(expected, actual) catch { + return error.TestExpectedApproxEqual; + }, + } +} + +/// Copy of std.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) { + std.debug.print(fmt, args); + } +} + +// Tests based on the `expectEqual` tests in the Zig stdlib +test "expectApproxEqual.union(enum)" { + const T = union(enum) { + a: i32, + b: f32, + }; + + const b10 = T{ .b = 10.0 }; + const b10plus = T{ .b = 10.000001 }; + + try expectApproxEqual(b10, b10plus); +} + +test "expectApproxEqual nested array" { + const a = [2][2]f32{ + [_]f32{ 1.0, 0.0 }, + [_]f32{ 0.0, 1.0 }, + }; + + const b = [2][2]f32{ + [_]f32{ 1.000001, 0.0 }, + [_]f32{ 0.0, 0.999999 }, + }; + + try expectApproxEqual(a, b); +} + +test "expectApproxEqual vector" { + const a: @Vector(4, f32) = @splat(4.0); + const b: @Vector(4, f32) = @splat(4.000001); + + try expectApproxEqual(a, b); +} + +test "expectApproxEqual struct" { + const a = .{ 1, @as(f32, 1.0) }; + const b = .{ 1, @as(f32, 0.999999) }; + + try expectApproxEqual(a, b); +} diff --git a/src/datastruct/intrusive_linked_list.zig b/src/datastruct/intrusive_linked_list.zig index 61bf8157c..734b82fff 100644 --- a/src/datastruct/intrusive_linked_list.zig +++ b/src/datastruct/intrusive_linked_list.zig @@ -23,7 +23,7 @@ pub fn DoublyLinkedList(comptime T: type) type { /// Arguments: /// node: Pointer to a node in the list. /// new_node: Pointer to the new node to insert. - pub fn insertAfter(list: *Self, node: *Node, new_node: *Node) void { + pub inline fn insertAfter(list: *Self, node: *Node, new_node: *Node) void { new_node.prev = node; if (node.next) |next_node| { // Intermediate node. @@ -42,7 +42,7 @@ pub fn DoublyLinkedList(comptime T: type) type { /// Arguments: /// node: Pointer to a node in the list. /// new_node: Pointer to the new node to insert. - pub fn insertBefore(list: *Self, node: *Node, new_node: *Node) void { + pub inline fn insertBefore(list: *Self, node: *Node, new_node: *Node) void { new_node.next = node; if (node.prev) |prev_node| { // Intermediate node. @@ -60,7 +60,7 @@ pub fn DoublyLinkedList(comptime T: type) type { /// /// Arguments: /// new_node: Pointer to the new node to insert. - pub fn append(list: *Self, new_node: *Node) void { + pub inline fn append(list: *Self, new_node: *Node) void { if (list.last) |last| { // Insert after last. list.insertAfter(last, new_node); @@ -74,7 +74,7 @@ pub fn DoublyLinkedList(comptime T: type) type { /// /// Arguments: /// new_node: Pointer to the new node to insert. - pub fn prepend(list: *Self, new_node: *Node) void { + pub inline fn prepend(list: *Self, new_node: *Node) void { if (list.first) |first| { // Insert before first. list.insertBefore(first, new_node); @@ -91,7 +91,7 @@ pub fn DoublyLinkedList(comptime T: type) type { /// /// Arguments: /// node: Pointer to the node to be removed. - pub fn remove(list: *Self, node: *Node) void { + pub inline fn remove(list: *Self, node: *Node) void { if (node.prev) |prev_node| { // Intermediate node. prev_node.next = node.next; @@ -113,7 +113,7 @@ pub fn DoublyLinkedList(comptime T: type) type { /// /// Returns: /// A pointer to the last node in the list. - pub fn pop(list: *Self) ?*Node { + pub inline fn pop(list: *Self) ?*Node { const last = list.last orelse return null; list.remove(last); return last; @@ -123,7 +123,7 @@ pub fn DoublyLinkedList(comptime T: type) type { /// /// Returns: /// A pointer to the first node in the list. - pub fn popFirst(list: *Self) ?*Node { + pub inline fn popFirst(list: *Self) ?*Node { const first = list.first orelse return null; list.remove(first); return first; diff --git a/src/datastruct/lru.zig b/src/datastruct/lru.zig index 7bf42e82d..1c6df69ce 100644 --- a/src/datastruct/lru.zig +++ b/src/datastruct/lru.zig @@ -33,8 +33,13 @@ pub fn HashMap( ) type { return struct { const Self = @This(); - const Map = std.HashMapUnmanaged(K, *Queue.Node, Context, max_load_percentage); - const Queue = std.DoublyLinkedList(KV); + const Queue = std.DoublyLinkedList; + const Map = std.HashMapUnmanaged( + K, + *Entry, + Context, + max_load_percentage, + ); /// Map to maintain our entries. map: Map, @@ -46,6 +51,15 @@ pub fn HashMap( /// misses will begin evicting entries. capacity: Map.Size, + const Entry = struct { + data: KV, + node: Queue.Node, + + fn fromNode(node: *Queue.Node) *Entry { + return @fieldParentPtr("node", node); + } + }; + pub const KV = struct { key: K, value: V, @@ -82,7 +96,7 @@ pub fn HashMap( var it = self.queue.first; while (it) |node| { it = node.next; - alloc.destroy(node); + alloc.destroy(Entry.fromNode(node)); } self.map.deinit(alloc); @@ -108,8 +122,8 @@ pub fn HashMap( const map_gop = try self.map.getOrPutContext(alloc, key, ctx); if (map_gop.found_existing) { // Move to end to mark as most recently used - self.queue.remove(map_gop.value_ptr.*); - self.queue.append(map_gop.value_ptr.*); + self.queue.remove(&map_gop.value_ptr.*.node); + self.queue.append(&map_gop.value_ptr.*.node); return GetOrPutResult{ .found_existing = true, @@ -122,37 +136,34 @@ pub fn HashMap( // We're evicting if our map insertion increased our capacity. const evict = self.map.count() > self.capacity; - // Get our node. If we're not evicting then we allocate a new - // node. If we are evicting then we avoid allocation by just - // reusing the node we would've evicted. - var node = if (!evict) try alloc.create(Queue.Node) else node: { + // Get our entry. If we're not evicting then we allocate a new + // entry. If we are evicting then we avoid allocation by just + // reusing the entry we would've evicted. + const entry: *Entry = if (!evict) try alloc.create(Entry) else entry: { // Our first node is the least recently used. - const least_used = self.queue.first.?; - - // Move our least recently used to the end to make - // it the most recently used. - self.queue.remove(least_used); + const least_used_node = self.queue.popFirst().?; + const least_used_entry: *Entry = .fromNode(least_used_node); // Remove the least used from the map - _ = self.map.remove(least_used.data.key); + _ = self.map.remove(least_used_entry.data.key); - break :node least_used; + break :entry least_used_entry; }; - errdefer if (!evict) alloc.destroy(node); + errdefer if (!evict) alloc.destroy(entry); - // Store our node in the map. - map_gop.value_ptr.* = node; + // Store our entry in the map. + map_gop.value_ptr.* = entry; - // Mark the node as most recently used - self.queue.append(node); + // Mark the entry as most recently used + self.queue.append(&entry.node); // Set our key - node.data.key = key; + entry.data.key = key; - return GetOrPutResult{ + return .{ .found_existing = map_gop.found_existing, - .value_ptr = &node.data.value, - .evicted = if (!evict) null else node.data, + .value_ptr = &entry.data.value, + .evicted = if (!evict) null else entry.data, }; } @@ -193,11 +204,12 @@ pub fn HashMap( var i: Map.Size = 0; while (i < delta) : (i += 1) { - const node = self.queue.first.?; - evicted[i] = node.data.value; + const node = self.queue.popFirst().?; + const entry: *Entry = .fromNode(node); + evicted[i] = entry.data.value; self.queue.remove(node); - _ = self.map.remove(node.data.key); - alloc.destroy(node); + _ = self.map.remove(entry.data.key); + alloc.destroy(entry); } self.capacity = capacity; diff --git a/src/datastruct/split_tree.zig b/src/datastruct/split_tree.zig index 28b45ceed..eb371187c 100644 --- a/src/datastruct/split_tree.zig +++ b/src/datastruct/split_tree.zig @@ -1023,45 +1023,33 @@ pub fn SplitTree(comptime V: type) type { } /// Format the tree in a human-readable format. By default this will - /// output a diagram followed by a textual representation. This can - /// be controlled via the formatting string: - /// - /// - `diagram` - Output a diagram of the split tree only. - /// - `text` - Output a textual representation of the split tree only. - /// - Empty - Output both a diagram and a textual representation. - /// + /// output a diagram followed by a textual representation. pub fn format( self: *const Self, - comptime fmt: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { - _ = options; - if (self.nodes.len == 0) { try writer.writeAll("empty"); return; } - - if (std.mem.eql(u8, fmt, "diagram")) { - self.formatDiagram(writer) catch - try writer.writeAll("failed to draw split tree diagram"); - } else if (std.mem.eql(u8, fmt, "text")) { - try self.formatText(writer, .root, 0); - } else if (fmt.len == 0) { - self.formatDiagram(writer) catch {}; - try self.formatText(writer, .root, 0); - } else { - return error.InvalidFormat; - } + self.formatDiagram(writer) catch {}; + try self.formatText(writer); } - fn formatText( - self: *const Self, - writer: anytype, + pub fn formatText(self: Self, writer: *std.Io.Writer) std.Io.Writer.Error!void { + if (self.nodes.len == 0) { + try writer.writeAll("empty"); + return; + } + try self.formatTextInner(writer, .root, 0); + } + + fn formatTextInner( + self: Self, + writer: *std.Io.Writer, current: Node.Handle, depth: usize, - ) !void { + ) std.Io.Writer.Error!void { for (0..depth) |_| try writer.writeAll(" "); if (self.zoomed) |zoomed| if (zoomed == current) { @@ -1075,20 +1063,25 @@ pub fn SplitTree(comptime V: type) type { try writer.print("leaf: {d}\n", .{current}), .split => |s| { - try writer.print("split (layout: {s}, ratio: {d:.2})\n", .{ - @tagName(s.layout), + try writer.print("split (layout: {t}, ratio: {d:.2})\n", .{ + s.layout, s.ratio, }); - try self.formatText(writer, s.left, depth + 1); - try self.formatText(writer, s.right, depth + 1); + try self.formatTextInner(writer, s.left, depth + 1); + try self.formatTextInner(writer, s.right, depth + 1); }, } } - fn formatDiagram( - self: *const Self, - writer: anytype, - ) !void { + pub fn formatDiagram( + self: Self, + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { + if (self.nodes.len == 0) { + try writer.writeAll("empty"); + return; + } + // Use our arena's GPA to allocate some intermediate memory. // Requiring allocation for formatting is nasty but this is really // only used for debugging and testing and shouldn't hit OOM @@ -1099,7 +1092,7 @@ pub fn SplitTree(comptime V: type) type { // Get our spatial representation. const sp = spatial: { - const sp = try self.spatial(alloc); + const sp = self.spatial(alloc) catch return error.WriteFailed; // Scale our spatial representation to have minimum width/height 1. var min_w: f16 = 1; @@ -1111,7 +1104,7 @@ pub fn SplitTree(comptime V: type) type { const ratio_w: f16 = 1 / min_w; const ratio_h: f16 = 1 / min_h; - const slots = try alloc.dupe(Spatial.Slot, sp.slots); + const slots = alloc.dupe(Spatial.Slot, sp.slots) catch return error.WriteFailed; for (slots) |*slot| { slot.x *= ratio_w; slot.y *= ratio_h; @@ -1168,9 +1161,9 @@ pub fn SplitTree(comptime V: type) type { width *= cell_width; height *= cell_height; - const rows = try alloc.alloc([]u8, height); + const rows = alloc.alloc([]u8, height) catch return error.WriteFailed; for (0..rows.len) |y| { - rows[y] = try alloc.alloc(u8, width + 1); + rows[y] = alloc.alloc(u8, width + 1) catch return error.WriteFailed; @memset(rows[y], ' '); rows[y][width] = '\n'; } @@ -1223,7 +1216,7 @@ pub fn SplitTree(comptime V: type) type { const label: []const u8 = if (@hasDecl(View, "splitTreeLabel")) node.leaf.splitTreeLabel() else - try std.fmt.bufPrint(&buf, "{d}", .{handle}); + std.fmt.bufPrint(&buf, "{d}", .{handle}) catch return error.WriteFailed; // Draw the handle in the center const x_mid = width / 2 + x; @@ -1231,7 +1224,7 @@ pub fn SplitTree(comptime V: type) type { const label_width = label.len; const label_start = x_mid - label_width / 2; const row = grid[y_mid][label_start..]; - _ = try std.fmt.bufPrint(row, "{s}", .{label}); + _ = std.fmt.bufPrint(row, "{s}", .{label}) catch return error.WriteFailed; } // Output every row @@ -1339,7 +1332,7 @@ test "SplitTree: empty tree" { var t: TestTree = .empty; defer t.deinit(); - const str = try std.fmt.allocPrint(alloc, "{}", .{t}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{t}); defer alloc.free(str); try testing.expectEqualStrings(str, \\empty @@ -1353,7 +1346,7 @@ test "SplitTree: single node" { var t: TestTree = try .init(alloc, &v); defer t.deinit(); - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{t}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(t, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---+ @@ -1383,7 +1376,7 @@ test "SplitTree: split horizontal" { defer t3.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{}", .{t3}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{t3}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---++---+ @@ -1415,7 +1408,7 @@ test "SplitTree: split horizontal" { defer t4.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{}", .{t4}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{t4}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+--------++---++---+ @@ -1449,7 +1442,7 @@ test "SplitTree: split horizontal" { defer t5.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{}", .{t5}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{t5}); defer alloc.free(str); try testing.expectEqualStrings( \\+------------------++--------++---++---+ @@ -1547,7 +1540,7 @@ test "SplitTree: split vertical" { ); defer t3.deinit(); - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{t3}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(t3, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---+ @@ -1583,7 +1576,7 @@ test "SplitTree: split horizontal with zero ratio" { const split = splitAB; { - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---+ @@ -1617,7 +1610,7 @@ test "SplitTree: split vertical with zero ratio" { const split = splitAB; { - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---+ @@ -1651,7 +1644,7 @@ test "SplitTree: split horizontal with full width" { const split = splitAB; { - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---+ @@ -1685,7 +1678,7 @@ test "SplitTree: split vertical with full width" { const split = splitAB; { - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---+ @@ -1727,7 +1720,7 @@ test "SplitTree: remove leaf" { ); defer t4.deinit(); - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{t4}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(t4, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---+ @@ -1772,7 +1765,7 @@ test "SplitTree: split twice, remove intermediary" { defer split2.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split2}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split2, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---++---+ @@ -1798,7 +1791,7 @@ test "SplitTree: split twice, remove intermediary" { defer split3.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split3}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split3, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---+ @@ -1883,7 +1876,7 @@ test "SplitTree: spatial goto" { const split = splitBD; { - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---++---+ @@ -1943,7 +1936,7 @@ test "SplitTree: spatial goto" { defer equal.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{equal}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(equal, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---++---+ @@ -1979,7 +1972,7 @@ test "SplitTree: resize" { defer split.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{split}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+---++---+ @@ -2005,7 +1998,7 @@ test "SplitTree: resize" { 0.25, ); defer resized.deinit(); - const str = try std.fmt.allocPrint(alloc, "{diagram}", .{resized}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(resized, .formatDiagram)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\+-------------++---+ @@ -2026,7 +2019,7 @@ test "SplitTree: clone empty tree" { defer t2.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{}", .{t2}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{t2}); defer alloc.free(str); try testing.expectEqualStrings(str, \\empty @@ -2064,7 +2057,7 @@ test "SplitTree: zoom" { }); { - const str = try std.fmt.allocPrint(alloc, "{text}", .{split}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatText)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\split (layout: horizontal, ratio: 0.50) @@ -2079,7 +2072,7 @@ test "SplitTree: zoom" { defer clone.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{text}", .{clone}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(clone, .formatText)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\split (layout: horizontal, ratio: 0.50) @@ -2122,7 +2115,7 @@ test "SplitTree: split resets zoom" { defer split.deinit(); { - const str = try std.fmt.allocPrint(alloc, "{text}", .{split}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatText)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\split (layout: horizontal, ratio: 0.50) @@ -2178,7 +2171,7 @@ test "SplitTree: remove and zoom" { defer removed.deinit(); try testing.expect(removed.zoomed == null); - const str = try std.fmt.allocPrint(alloc, "{text}", .{removed}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(removed, .formatText)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\leaf: B @@ -2201,7 +2194,7 @@ test "SplitTree: remove and zoom" { ); defer removed.deinit(); - const str = try std.fmt.allocPrint(alloc, "{text}", .{removed}); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(removed, .formatText)}); defer alloc.free(str); try testing.expectEqualStrings(str, \\(zoomed) leaf: A diff --git a/src/extra/bash.zig b/src/extra/bash.zig index 536cadbc4..ee9a7895c 100644 --- a/src/extra/bash.zig +++ b/src/extra/bash.zig @@ -19,18 +19,18 @@ pub const completions = comptimeGenerateBashCompletions(); fn comptimeGenerateBashCompletions() []const u8 { comptime { @setEvalBranchQuota(50000); - var counter = std.io.countingWriter(std.io.null_writer); - try writeBashCompletions(&counter.writer()); + var counter: std.Io.Writer.Discarding = .init(&.{}); + try writeBashCompletions(&counter.writer); - var buf: [counter.bytes_written]u8 = undefined; - var stream = std.io.fixedBufferStream(&buf); - try writeBashCompletions(stream.writer()); + var buf: [counter.count]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try writeBashCompletions(&writer); const final = buf; - return final[0..stream.getWritten().len]; + return final[0..writer.end]; } } -fn writeBashCompletions(writer: anytype) !void { +fn writeBashCompletions(writer: *std.Io.Writer) !void { const pad1 = " "; const pad2 = pad1 ++ pad1; const pad3 = pad2 ++ pad1; diff --git a/src/extra/fish.zig b/src/extra/fish.zig index 5a4b38e32..7ffc23093 100644 --- a/src/extra/fish.zig +++ b/src/extra/fish.zig @@ -11,18 +11,18 @@ pub const completions = comptimeGenerateCompletions(); fn comptimeGenerateCompletions() []const u8 { comptime { @setEvalBranchQuota(50000); - var counter = std.io.countingWriter(std.io.null_writer); - try writeCompletions(&counter.writer()); + var counter: std.Io.Writer.Discarding = .init(&.{}); + try writeCompletions(&counter.writer); - var buf: [counter.bytes_written]u8 = undefined; - var stream = std.io.fixedBufferStream(&buf); - try writeCompletions(stream.writer()); + var buf: [counter.count]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try writeCompletions(&writer); const final = buf; - return final[0..stream.getWritten().len]; + return final[0..writer.end]; } } -fn writeCompletions(writer: anytype) !void { +fn writeCompletions(writer: *std.Io.Writer) !void { { try writer.writeAll("set -l commands \""); var count: usize = 0; diff --git a/src/extra/vim.zig b/src/extra/vim.zig index e5261cd74..9140b83f8 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/* set ft=ghostty + \\au BufRead,BufNewFile */ghostty/config,*/ghostty/themes/*,*.ghostty setf ghostty \\ ; pub const ftplugin = @@ -59,19 +59,20 @@ pub const compiler = /// Generates the syntax file at comptime. fn comptimeGenSyntax() []const u8 { comptime { - var counting_writer = std.io.countingWriter(std.io.null_writer); - try writeSyntax(&counting_writer.writer()); + @setEvalBranchQuota(50000); + var counter: std.Io.Writer.Discarding = .init(&.{}); + try writeSyntax(&counter.writer); - var buf: [counting_writer.bytes_written]u8 = undefined; - var stream = std.io.fixedBufferStream(&buf); - try writeSyntax(stream.writer()); + var buf: [counter.count]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try writeSyntax(&writer); const final = buf; - return final[0..stream.getWritten().len]; + return final[0..writer.end]; } } /// Writes the syntax file to the given writer. -fn writeSyntax(writer: anytype) !void { +fn writeSyntax(writer: *std.Io.Writer) !void { try writer.writeAll( \\" Vim syntax file \\" Language: Ghostty config file diff --git a/src/extra/zsh.zig b/src/extra/zsh.zig index 6bddcd285..2fad4234a 100644 --- a/src/extra/zsh.zig +++ b/src/extra/zsh.zig @@ -12,18 +12,18 @@ const equals_required = "=-:::"; fn comptimeGenerateZshCompletions() []const u8 { comptime { @setEvalBranchQuota(50000); - var counter = std.io.countingWriter(std.io.null_writer); - try writeZshCompletions(&counter.writer()); + var counter: std.Io.Writer.Discarding = .init(&.{}); + try writeZshCompletions(&counter.writer); - var buf: [counter.bytes_written]u8 = undefined; - var stream = std.io.fixedBufferStream(&buf); - try writeZshCompletions(stream.writer()); + var buf: [counter.count]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try writeZshCompletions(&writer); const final = buf; - return final[0..stream.getWritten().len]; + return final[0..writer.end]; } } -fn writeZshCompletions(writer: anytype) !void { +fn writeZshCompletions(writer: *std.Io.Writer) !void { try writer.writeAll( \\#compdef ghostty \\ diff --git a/src/fastmem.zig b/src/fastmem.zig index bdea44155..d4a0a7750 100644 --- a/src/fastmem.zig +++ b/src/fastmem.zig @@ -2,20 +2,13 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; -/// Same as std.mem.copyForwards/Backwards but prefers libc memmove if it is -/// available because it is generally much faster. +/// Same as @memmove but prefers libc memmove if it is +/// available because it is generally much faster?. pub inline fn move(comptime T: type, dest: []T, source: []const T) void { if (builtin.link_libc) { _ = memmove(dest.ptr, source.ptr, source.len * @sizeOf(T)); } else { - // Depending on the ordering of the copy, we need to use the - // proper call here. Unfortunately this function call is - // too generic to know this at comptime. - if (@intFromPtr(dest.ptr) <= @intFromPtr(source.ptr)) { - std.mem.copyForwards(T, dest, source); - } else { - std.mem.copyBackwards(T, dest, source); - } + @memmove(dest, source); } } diff --git a/src/font/Atlas.zig b/src/font/Atlas.zig index 68ccaddcc..e2d9a5de2 100644 --- a/src/font/Atlas.zig +++ b/src/font/Atlas.zig @@ -355,7 +355,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: anytype) !void { +pub fn dump(self: Atlas, writer: *std.Io.Writer) !void { try writer.print( \\P{c} \\{d} {d} diff --git a/src/font/CodepointResolver.zig b/src/font/CodepointResolver.zig index ba74065ab..a4f13c290 100644 --- a/src/font/CodepointResolver.zig +++ b/src/font/CodepointResolver.zig @@ -13,7 +13,7 @@ const CodepointResolver = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; -const ziglyph = @import("ziglyph"); +const uucode = @import("uucode"); const font = @import("main.zig"); const Atlas = font.Atlas; const CodepointMap = font.CodepointMap; @@ -150,7 +150,7 @@ pub fn getIndex( // we'll do this multiple times if we recurse, but this is a cached function // call higher up (GroupCache) so this should be rare. const p_mode: Collection.PresentationMode = if (p) |v| .{ .explicit = v } else .{ - .default = if (ziglyph.emoji.isEmojiPresentation(@intCast(cp))) + .default = if (uucode.get(.is_emoji_presentation, @intCast(cp))) .emoji else .text, diff --git a/src/font/Collection.zig b/src/font/Collection.zig index ad9590d70..b587245aa 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -19,6 +19,7 @@ const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const config = @import("../config.zig"); +const comparison = @import("../datastruct/comparison.zig"); const font = @import("main.zig"); const options = font.options; const DeferredFace = font.DeferredFace; @@ -222,12 +223,13 @@ fn getFaceFromEntry( // Calculate the scale factor for this // entry now that we have a loaded face. - entry.scale_factor = .{ - .scale = self.scaleFactor( + if (entry.scale_factor == .adjustment) { + const factor = self.scaleFactor( face.getMetrics(), entry.scale_factor.adjustment, - ), - }; + ); + entry.scale_factor = .{ .scale = factor }; + } // If our scale factor is something other // than 1.0 then we need to resize the face. @@ -1199,7 +1201,7 @@ test "metrics" { try c.updateMetrics(); - try std.testing.expectEqual(font.Metrics{ + try comparison.expectApproxEqual(font.Metrics{ .cell_width = 8, // The cell height is 17 px because the calculation is // @@ -1213,6 +1215,9 @@ test "metrics" { // and 1em should be the point size * dpi scale, so 12 * (96/72) // which is 16, and 16 * 1.049 = 16.784, which finally is rounded // to 17. + // + // The icon height is (2 * cap_height + face_height) / 3 + // = (2 * 623 + 1049) / 3 = 765, and 16 * 0.765 = 12.24. .cell_height = 17, .cell_baseline = 3, .underline_position = 17, @@ -1223,12 +1228,16 @@ test "metrics" { .overline_thickness = 1, .box_thickness = 1, .cursor_height = 17, - .icon_height = 11, + .icon_height = 16.784, + .icon_height_single = 12.24, + .face_width = 8.0, + .face_height = 16.784, + .face_y = -0.04, }, c.metrics); // Resize should change metrics try c.setSize(.{ .points = 24, .xdpi = 96, .ydpi = 96 }); - try std.testing.expectEqual(font.Metrics{ + try comparison.expectApproxEqual(font.Metrics{ .cell_width = 16, .cell_height = 34, .cell_baseline = 6, @@ -1240,7 +1249,11 @@ test "metrics" { .overline_thickness = 2, .box_thickness = 2, .cursor_height = 34, - .icon_height = 23, + .icon_height = 33.568, + .icon_height_single = 24.48, + .face_width = 16.0, + .face_height = 33.568, + .face_y = -0.08, }, c.metrics); } @@ -1369,3 +1382,133 @@ test "adjusted sizes" { ); } } + +test "face metrics" { + // The web canvas backend doesn't calculate face metrics, only cell metrics + if (options.backend != .web_canvas) return error.SkipZigTest; + + const testing = std.testing; + const alloc = testing.allocator; + const narrowFont = font.embedded.cozette; + const wideFont = font.embedded.geist_mono; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var c = init(); + defer c.deinit(alloc); + const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 }; + c.load_options = .{ .library = lib, .size = size }; + + const narrowIndex = try c.add(alloc, try .init( + lib, + narrowFont, + .{ .size = size }, + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); + const wideIndex = try c.add(alloc, try .init( + lib, + wideFont, + .{ .size = size }, + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); + + const narrowMetrics: font.Metrics.FaceMetrics = (try c.getFace(narrowIndex)).getMetrics(); + const wideMetrics: font.Metrics.FaceMetrics = (try c.getFace(wideIndex)).getMetrics(); + + // Verify provided/measured metrics. Measured + // values are backend-dependent due to hinting. + const narrowMetricsExpected = font.Metrics.FaceMetrics{ + .px_per_em = 16.0, + .cell_width = switch (options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => 8.0, + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + => 7.3828125, + .web_canvas => unreachable, + }, + .ascent = 12.3046875, + .descent = -3.6953125, + .line_gap = 0.0, + .underline_position = -1.2265625, + .underline_thickness = 1.2265625, + .strikethrough_position = 6.15625, + .strikethrough_thickness = 1.234375, + .cap_height = 9.84375, + .ex_height = 7.3828125, + .ascii_height = switch (options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => 18.0625, + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + => 16.0, + .web_canvas => unreachable, + }, + }; + const wideMetricsExpected = font.Metrics.FaceMetrics{ + .px_per_em = 16.0, + .cell_width = switch (options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => 10.0, + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + => 9.6, + .web_canvas => unreachable, + }, + .ascent = 14.72, + .descent = -3.52, + .line_gap = 1.6, + .underline_position = -1.6, + .underline_thickness = 0.8, + .strikethrough_position = 4.24, + .strikethrough_thickness = 0.8, + .cap_height = 11.36, + .ex_height = 8.48, + .ascii_height = switch (options.backend) { + .freetype, + .fontconfig_freetype, + .coretext_freetype, + => 16.0, + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + => 15.472000000000001, + .web_canvas => unreachable, + }, + }; + + inline for ( + .{ narrowMetricsExpected, wideMetricsExpected }, + .{ narrowMetrics, wideMetrics }, + ) |metricsExpected, metricsActual| { + try comparison.expectApproxEqual(metricsExpected, metricsActual); + } + + // Verify estimated metrics. icWidth() should equal the smaller of + // 2 * cell_width and ascii_height. For a narrow (wide) font, the + // smaller quantity is the former (latter). + try std.testing.expectEqual( + 2 * narrowMetrics.cell_width, + narrowMetrics.icWidth(), + ); + try std.testing.expectEqual( + wideMetrics.ascii_height, + wideMetrics.icWidth(), + ); +} diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig index 9f6df9dc3..ec89763ea 100644 --- a/src/font/Metrics.zig +++ b/src/font/Metrics.zig @@ -36,11 +36,20 @@ cursor_thickness: u32 = 1, cursor_height: u32, /// The constraint height for nerd fonts icons. -icon_height: u32, +icon_height: f64, -/// Original cell width in pixels. This is used to keep -/// glyphs centered if the cell width is adjusted wider. -original_cell_width: ?u32 = null, +/// The constraint height for nerd fonts icons limited to a single cell width. +icon_height_single: f64, + +/// The unrounded face width, used in scaling calculations. +face_width: f64, + +/// The unrounded face height, used in scaling calculations. +face_height: f64, + +/// The vertical bearing of face within the pixel-rounded +/// and possibly height-adjusted cell +face_y: f64, /// Minimum acceptable values for some fields to prevent modifiers /// from being able to, for example, cause 0-thickness underlines. @@ -53,7 +62,10 @@ const Minimums = struct { const box_thickness = 1; const cursor_thickness = 1; const cursor_height = 1; - const icon_height = 1; + const icon_height = 1.0; + const icon_height_single = 1.0; + const face_height = 1.0; + const face_width = 1.0; }; /// Metrics extracted from a font face, based on @@ -214,8 +226,10 @@ pub fn calc(face: FaceMetrics) Metrics { // We use the ceiling of the provided cell width and height to ensure // that the cell is large enough for the provided size, since we cast // it to an integer later. - const cell_width = @ceil(face.cell_width); - const cell_height = @ceil(face.lineHeight()); + const face_width = face.cell_width; + const face_height = face.lineHeight(); + const cell_width = @ceil(face_width); + const cell_height = @ceil(face_height); // We split our line gap in two parts, and put half of it on the top // of the cell and the other half on the bottom, so that our text never @@ -224,7 +238,11 @@ pub fn calc(face: FaceMetrics) Metrics { // Unlike all our other metrics, `cell_baseline` is relative to the // BOTTOM of the cell. - const cell_baseline = @round(half_line_gap - face.descent); + const face_baseline = half_line_gap - face.descent; + const cell_baseline = @round(face_baseline); + + // We keep track of the vertical bearing of the face in the cell + const face_y = cell_baseline - face_baseline; // We calculate a top_to_baseline to make following calculations simpler. const top_to_baseline = cell_height - cell_baseline; @@ -237,16 +255,11 @@ pub fn calc(face: FaceMetrics) Metrics { const underline_position = @round(top_to_baseline - face.underlinePosition()); const strikethrough_position = @round(top_to_baseline - face.strikethroughPosition()); - // The calculation for icon height in the nerd fonts patcher - // is two thirds cap height to one third line height, but we - // use an opinionated default of 1.2 * cap height instead. - // - // Doing this prevents fonts with very large line heights - // from having excessively oversized icons, and allows fonts - // with very small line heights to still have roomy icons. - // - // We do cap it at `cell_height` though for obvious reasons. - const icon_height = @min(cell_height, cap_height * 1.2); + // Same heuristic as the font_patcher script. We store icon_height + // separately from face_height such that modifiers can apply to the former + // without affecting the latter. + const icon_height = face_height; + const icon_height_single = (2 * cap_height + face_height) / 3; var result: Metrics = .{ .cell_width = @intFromFloat(cell_width), @@ -260,7 +273,11 @@ pub fn calc(face: FaceMetrics) Metrics { .overline_thickness = @intFromFloat(underline_thickness), .box_thickness = @intFromFloat(underline_thickness), .cursor_height = @intFromFloat(cell_height), - .icon_height = @intFromFloat(icon_height), + .icon_height = icon_height, + .icon_height_single = icon_height_single, + .face_width = face_width, + .face_height = face_height, + .face_y = face_y, }; // Ensure all metrics are within their allowable range. @@ -286,11 +303,6 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void { const new = @max(entry.value_ptr.apply(original), 1); if (new == original) continue; - // Preserve the original cell width if not set. - if (self.original_cell_width == null) { - self.original_cell_width = self.cell_width; - } - // Set the new value @field(self, @tagName(tag)) = new; @@ -307,6 +319,7 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void { const diff = new - original; const diff_bottom = diff / 2; const diff_top = diff - diff_bottom; + self.face_y += @floatFromInt(diff_bottom); self.cell_baseline +|= diff_bottom; self.underline_position +|= diff_top; self.strikethrough_position +|= diff_top; @@ -315,6 +328,7 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void { const diff = original - new; const diff_bottom = diff / 2; const diff_top = diff - diff_bottom; + self.face_y -= @floatFromInt(diff_bottom); self.cell_baseline -|= diff_bottom; self.underline_position -|= diff_top; self.strikethrough_position -|= diff_top; @@ -322,6 +336,10 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void { } } }, + inline .icon_height => { + self.icon_height = entry.value_ptr.apply(self.icon_height); + self.icon_height_single = entry.value_ptr.apply(self.icon_height_single); + }, inline else => |tag| { @field(self, @tagName(tag)) = entry.value_ptr.apply(@field(self, @tagName(tag))); @@ -417,25 +435,35 @@ pub const Modifier = union(enum) { /// Apply a modifier to a numeric value. pub fn apply(self: Modifier, v: anytype) @TypeOf(v) { const T = @TypeOf(v); - const signed = @typeInfo(T).int.signedness == .signed; - return switch (self) { - .percent => |p| percent: { - const p_clamped: f64 = @max(0, p); - const v_f64: f64 = @floatFromInt(v); - const applied_f64: f64 = @round(v_f64 * p_clamped); - const applied_T: T = @intFromFloat(applied_f64); - break :percent applied_T; - }, + const Tinfo = @typeInfo(T); + return switch (comptime Tinfo) { + .int, .comptime_int => switch (self) { + .percent => |p| percent: { + const p_clamped: f64 = @max(0, p); + const v_f64: f64 = @floatFromInt(v); + const applied_f64: f64 = @round(v_f64 * p_clamped); + const applied_T: T = @intFromFloat(applied_f64); + break :percent applied_T; + }, - .absolute => |abs| absolute: { - const v_i64: i64 = @intCast(v); - const abs_i64: i64 = @intCast(abs); - const applied_i64: i64 = v_i64 +| abs_i64; - const clamped_i64: i64 = if (signed) applied_i64 else @max(0, applied_i64); - const applied_T: T = std.math.cast(T, clamped_i64) orelse - std.math.maxInt(T) * @as(T, @intCast(std.math.sign(clamped_i64))); - break :absolute applied_T; + .absolute => |abs| absolute: { + const v_i64: i64 = @intCast(v); + const abs_i64: i64 = @intCast(abs); + const applied_i64: i64 = v_i64 +| abs_i64; + const clamped_i64: i64 = if (Tinfo.int.signedness == .signed) + applied_i64 + else + @max(0, applied_i64); + const applied_T: T = std.math.cast(T, clamped_i64) orelse + std.math.maxInt(T) * @as(T, @intCast(std.math.sign(clamped_i64))); + break :absolute applied_T; + }, }, + .float, .comptime_float => return switch (self) { + .percent => |p| v * @max(0, p), + .absolute => |abs| v + @as(T, @floatFromInt(abs)), + }, + else => {}, }; } @@ -455,23 +483,23 @@ pub const Modifier = union(enum) { test "formatConfig percent" { const configpkg = @import("../config.zig"); const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); const p = try parseCLI("24%"); - try p.formatEntry(configpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = 24%\n", buf.items); + try p.formatEntry(configpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = 24%\n", buf.written()); } test "formatConfig absolute" { const configpkg = @import("../config.zig"); const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); + var buf: std.Io.Writer.Allocating = .init(testing.allocator); defer buf.deinit(); const p = try parseCLI("-30"); - try p.formatEntry(configpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = -30\n", buf.items); + try p.formatEntry(configpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = -30\n", buf.written()); } }; @@ -481,7 +509,7 @@ pub const Key = key: { var enumFields: [field_infos.len]std.builtin.Type.EnumField = undefined; var count: usize = 0; for (field_infos, 0..) |field, i| { - if (field.type != u32 and field.type != i32) continue; + if (field.type != u32 and field.type != i32 and field.type != f64) continue; enumFields[i] = .{ .name = field.name, .value = i }; count += 1; } @@ -512,7 +540,11 @@ fn init() Metrics { .overline_thickness = 0, .box_thickness = 0, .cursor_height = 0, - .icon_height = 0, + .icon_height = 0.0, + .icon_height_single = 0.0, + .face_width = 0.0, + .face_height = 0.0, + .face_y = 0.0, }; } @@ -542,6 +574,7 @@ test "Metrics: adjust cell height smaller" { try set.put(alloc, .cell_height, .{ .percent = 0.75 }); var m: Metrics = init(); + m.face_y = 0.33; m.cell_baseline = 50; m.underline_position = 55; m.strikethrough_position = 30; @@ -549,6 +582,7 @@ test "Metrics: adjust cell height smaller" { m.cell_height = 100; m.cursor_height = 100; m.apply(set); + try testing.expectEqual(-11.67, m.face_y); try testing.expectEqual(@as(u32, 75), m.cell_height); try testing.expectEqual(@as(u32, 38), m.cell_baseline); try testing.expectEqual(@as(u32, 42), m.underline_position); @@ -570,6 +604,7 @@ test "Metrics: adjust cell height larger" { try set.put(alloc, .cell_height, .{ .percent = 1.75 }); var m: Metrics = init(); + m.face_y = 0.33; m.cell_baseline = 50; m.underline_position = 55; m.strikethrough_position = 30; @@ -577,6 +612,7 @@ test "Metrics: adjust cell height larger" { m.cell_height = 100; m.cursor_height = 100; m.apply(set); + try testing.expectEqual(37.33, m.face_y); try testing.expectEqual(@as(u32, 175), m.cell_height); try testing.expectEqual(@as(u32, 87), m.cell_baseline); try testing.expectEqual(@as(u32, 93), m.underline_position); @@ -586,6 +622,48 @@ test "Metrics: adjust cell height larger" { try testing.expectEqual(@as(u32, 100), m.cursor_height); } +test "Metrics: adjust icon height by percentage" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: ModifierSet = .{}; + defer set.deinit(alloc); + try set.put(alloc, .icon_height, .{ .percent = 0.75 }); + + var m: Metrics = init(); + m.icon_height = 100.0; + m.icon_height_single = 80.0; + m.face_height = 100.0; + m.face_y = 1.0; + m.apply(set); + try testing.expectEqual(75.0, m.icon_height); + try testing.expectEqual(60.0, m.icon_height_single); + // Face metrics not affected + try testing.expectEqual(100.0, m.face_height); + try testing.expectEqual(1.0, m.face_y); +} + +test "Metrics: adjust icon height by absolute pixels" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: ModifierSet = .{}; + defer set.deinit(alloc); + try set.put(alloc, .icon_height, .{ .absolute = -5 }); + + var m: Metrics = init(); + m.icon_height = 100.0; + m.icon_height_single = 80.0; + m.face_height = 100.0; + m.face_y = 1.0; + m.apply(set); + try testing.expectEqual(95.0, m.icon_height); + try testing.expectEqual(75.0, m.icon_height_single); + // Face metrics not affected + try testing.expectEqual(100.0, m.face_height); + try testing.expectEqual(1.0, m.face_y); +} + test "Modifier: parse absolute" { const testing = std.testing; diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index e79fd117f..3fd9cf204 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -270,11 +270,9 @@ pub fn renderGlyph( // Always use these constraints for emoji. if (p == .emoji) { render_opts.constraint = .{ - // Make the emoji as wide as possible, scaling proportionally, - // but then scale it down as necessary if its new size exceeds - // the cell height. - .size_horizontal = .cover, - .size_vertical = .fit, + // Scale emoji to be as large as possible + // while preserving their aspect ratio. + .size = .cover, // Center the emoji in its cells. .align_horizontal = .center, diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index 813a8d6d0..4512e23cc 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -596,10 +596,10 @@ pub const Key = struct { // from DerivedConfig below. var config = try DerivedConfig.init(alloc, config_src); - var descriptors = std.ArrayList(discovery.Descriptor).init(alloc); - defer descriptors.deinit(); + var descriptors: std.ArrayList(discovery.Descriptor) = .empty; + defer descriptors.deinit(alloc); for (config.@"font-family".list.items) |family| { - try descriptors.append(.{ + try descriptors.append(alloc, .{ .family = family, .style = config.@"font-style".nameValue(), .size = font_size.points, @@ -617,7 +617,7 @@ pub const Key = struct { // italic. for (config.@"font-family-bold".list.items) |family| { const style = config.@"font-style-bold".nameValue(); - try descriptors.append(.{ + try descriptors.append(alloc, .{ .family = family, .style = style, .size = font_size.points, @@ -627,7 +627,7 @@ pub const Key = struct { } for (config.@"font-family-italic".list.items) |family| { const style = config.@"font-style-italic".nameValue(); - try descriptors.append(.{ + try descriptors.append(alloc, .{ .family = family, .style = style, .size = font_size.points, @@ -637,7 +637,7 @@ pub const Key = struct { } for (config.@"font-family-bold-italic".list.items) |family| { const style = config.@"font-style-bold-italic".nameValue(); - try descriptors.append(.{ + try descriptors.append(alloc, .{ .family = family, .style = style, .size = font_size.points, @@ -681,7 +681,7 @@ pub const Key = struct { return .{ .arena = arena, - .descriptors = try descriptors.toOwnedSlice(), + .descriptors = try descriptors.toOwnedSlice(alloc), .style_offsets = .{ regular_offset, bold_offset, diff --git a/src/font/face.zig b/src/font/face.zig index 9da3c30f6..a1312c45a 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -93,6 +93,14 @@ pub const Variation = struct { }; }; +/// The size and position of a glyph. +pub const GlyphSize = struct { + width: f64, + height: f64, + x: f64, + y: f64, +}; + /// Additional options for rendering glyphs. pub const RenderOptions = struct { /// The metrics that are defining the grid layout. These are usually @@ -136,10 +144,8 @@ pub const RenderOptions = struct { /// Don't constrain the glyph in any way. pub const none: Constraint = .{}; - /// Vertical sizing rule. - size_vertical: Size = .none, - /// Horizontal sizing rule. - size_horizontal: Size = .none, + /// Sizing rule. + size: Size = .none, /// Vertical alignment rule. align_vertical: Align = .none, @@ -155,42 +161,40 @@ pub const RenderOptions = struct { /// Bottom padding when resizing. pad_bottom: f64 = 0.0, - // This acts as a multiple of the provided width when applying - // constraints, so if this is 1.6 for example, then a width of - // 10 would be treated as though it were 16. - group_width: f64 = 1.0, - // This acts as a multiple of the provided height when applying - // constraints, so if this is 1.6 for example, then a height of - // 10 would be treated as though it were 16. - group_height: f64 = 1.0, - // This is an x offset for the actual width within the group width. - // If this is 0.5 then the glyph will be offset so that its left - // edge sits at the halfway point of the group width. - group_x: f64 = 0.0, - // This is a y offset for the actual height within the group height. - // If this is 0.5 then the glyph will be offset so that its bottom - // edge sits at the halfway point of the group height. - group_y: f64 = 0.0, + // Size and bearings of the glyph relative + // to the bounding box of its scale group. + relative_width: f64 = 1.0, + relative_height: f64 = 1.0, + relative_x: f64 = 0.0, + relative_y: f64 = 0.0, - /// Maximum ratio of width to height when resizing. + /// Maximum aspect ratio (width/height) to allow when stretching. max_xy_ratio: ?f64 = null, /// Maximum number of cells horizontally to use. max_constraint_width: u2 = 2, - /// What to use as the height metric when constraining the glyph. + /// What to use as the height metric when constraining the glyph and + /// the constraint width is 1, height: Height = .cell, pub const Size = enum { /// Don't change the size of this glyph. none, - /// Move the glyph and optionally scale it down - /// proportionally to fit within the given axis. + /// Scale the glyph down if needed to fit within the bounds, + /// preserving aspect ratio. fit, - /// Move and resize the glyph proportionally to - /// cover the given axis. + /// Scale the glyph up or down to exactly match the bounds, + /// preserving aspect ratio. cover, - /// Same as `cover` but not proportional. + /// Scale the glyph down if needed to fit within the bounds, + /// preserving aspect ratio. If the glyph doesn't cover a + /// single cell, scale up. If the glyph exceeds a single + /// cell but is within the bounds, do nothing. + /// (Nerd Font specific rule.) + fit_cover1, + /// Stretch the glyph to exactly fit the bounds in both + /// directions, disregarding aspect ratio. stretch, }; @@ -205,30 +209,29 @@ pub const RenderOptions = struct { end, /// Move the glyph so that it is centered on this axis. center, + /// Move the glyph so that it is centered on this axis, + /// but always with respect to the first cell even for + /// multi-cell constraints. (Nerd Font specific rule.) + center1, }; pub const Height = enum { - /// Use the full height of the cell for constraining this glyph. + /// Use the full line height of the primary face for + /// constraining this glyph. cell, - /// Use the "icon height" from the grid metrics as the height. + /// Use the icon height from the grid metrics for + /// constraining this glyph. Unlike `cell`, the value of + /// this height depends on both the constraint width and the + /// affected by the `adjust-icon-height` config option. icon, }; - /// The size and position of a glyph. - pub const GlyphSize = struct { - width: f64, - height: f64, - x: f64, - y: f64, - }; - /// Returns true if the constraint does anything. If it doesn't, /// because it neither sizes nor positions the glyph, then this /// returns false. pub inline fn doesAnything(self: Constraint) bool { - return self.size_horizontal != .none or + return self.size != .none or self.align_horizontal != .none or - self.size_vertical != .none or self.align_vertical != .none; } @@ -241,156 +244,252 @@ pub const RenderOptions = struct { /// Number of cells horizontally available for this glyph. constraint_width: u2, ) GlyphSize { - var g = glyph; + if (!self.doesAnything()) return glyph; - const available_width: f64 = @floatFromInt( - metrics.cell_width * @min( - self.max_constraint_width, - constraint_width, - ), - ); - const available_height: f64 = @floatFromInt(switch (self.height) { - .cell => metrics.cell_height, - .icon => metrics.icon_height, - }); - - const w = available_width - - self.pad_left * available_width - - self.pad_right * available_width; - const h = available_height - - self.pad_top * available_height - - self.pad_bottom * available_height; - - // Subtract padding from the bearings so that our - // alignment and sizing code works correctly. We - // re-add before returning. - g.x -= self.pad_left * available_width; - g.y -= self.pad_bottom * available_height; - - // Multiply by group width and height for better sizing. - g.width *= self.group_width; - g.height *= self.group_height; - - switch (self.size_horizontal) { - .none => {}, - .fit => if (g.width > w) { - const orig_height = g.height; - // Adjust our height and width to proportionally - // scale them to fit the glyph to the cell width. - g.height *= w / g.width; - g.width = w; - // Set our x to 0 since anything else would mean - // the glyph extends outside of the cell width. - g.x = 0; - // Compensate our y to keep things vertically - // centered as they're scaled down. - g.y += (orig_height - g.height) / 2; - } else if (g.width + g.x > w) { - // If the width of the glyph can fit in the cell but - // is currently outside due to the left bearing, then - // we reduce the left bearing just enough to fit it - // back in the cell. - g.x = w - g.width; - } else if (g.x < 0) { - g.x = 0; - }, - .cover => { - const orig_height = g.height; - - g.height *= w / g.width; - g.width = w; - - g.x = 0; - - g.y += (orig_height - g.height) / 2; - }, + switch (self.size) { .stretch => { - g.width = w; - g.x = 0; + // Stretched glyphs are usually meant to align across cell + // boundaries, which works best if they're scaled and + // aligned to the grid rather than the face. This is most + // easily done by inserting this little fib in the metrics. + var m = metrics; + m.face_width = @floatFromInt(m.cell_width); + m.face_height = @floatFromInt(m.cell_height); + m.face_y = 0.0; + + // Negative padding for stretched glyphs is a band-aid to + // avoid gaps due to pixel rounding, but at the cost of + // unsightly overlap artifacts. Since we scale and align to + // the grid rather than the face, we don't need it. + var c = self; + c.pad_bottom = @max(0, c.pad_bottom); + c.pad_top = @max(0, c.pad_top); + c.pad_left = @max(0, c.pad_left); + c.pad_right = @max(0, c.pad_right); + + return c.constrainInner(glyph, m, constraint_width); }, + else => return self.constrainInner(glyph, metrics, constraint_width), } + } - switch (self.size_vertical) { - .none => {}, - .fit => if (g.height > h) { - const orig_width = g.width; - // Adjust our height and width to proportionally - // scale them to fit the glyph to the cell height. - g.width *= h / g.height; - g.height = h; - // Set our y to 0 since anything else would mean - // the glyph extends outside of the cell height. - g.y = 0; - // Compensate our x to keep things horizontally - // centered as they're scaled down. - g.x += (orig_width - g.width) / 2; - } else if (g.height + g.y > h) { - // If the height of the glyph can fit in the cell but - // is currently outside due to the bottom bearing, then - // we reduce the bottom bearing just enough to fit it - // back in the cell. - g.y = h - g.height; - } else if (g.y < 0) { - g.y = 0; - }, - .cover => { - const orig_width = g.width; + fn constrainInner( + self: Constraint, + glyph: GlyphSize, + metrics: Metrics, + constraint_width: u2, + ) GlyphSize { + // For extra wide font faces, never stretch glyphs across two cells. + // This mirrors font_patcher. + const min_constraint_width: u2 = if ((self.size == .stretch) and (metrics.face_width > 0.9 * metrics.face_height)) + 1 + else + @min(self.max_constraint_width, constraint_width); - g.width *= h / g.height; - g.height = h; - - g.y = 0; - - g.x += (orig_width - g.width) / 2; - }, - .stretch => { - g.height = h; - g.y = 0; - }, - } - - // Add group-relative position - g.x += self.group_x * g.width; - g.y += self.group_y * g.height; - - // Divide group width and height back out before we align. - g.width /= self.group_width; - g.height /= self.group_height; - - if (self.max_xy_ratio) |ratio| if (g.width > g.height * ratio) { - const orig_width = g.width; - g.width = g.height * ratio; - g.x += (orig_width - g.width) / 2; + // The bounding box for the glyph's scale group. + // Scaling and alignment rules are calculated for + // this box and then applied to the glyph. + var group: GlyphSize = group: { + const group_width = glyph.width / self.relative_width; + const group_height = glyph.height / self.relative_height; + break :group .{ + .width = group_width, + .height = group_height, + .x = glyph.x - (group_width * self.relative_x), + .y = glyph.y - (group_height * self.relative_y), + }; }; - switch (self.align_horizontal) { - .none => {}, - .start => g.x = 0, - .end => g.x = w - g.width, - .center => g.x = (w - g.width) / 2, + // Apply prescribed scaling, preserving the + // center bearings of the group bounding box + const width_factor, const height_factor = self.scale_factors(group, metrics, min_constraint_width); + const center_x = group.x + (group.width / 2); + const center_y = group.y + (group.height / 2); + group.width *= width_factor; + group.height *= height_factor; + group.x = center_x - (group.width / 2); + group.y = center_y - (group.height / 2); + + // NOTE: font_patcher jumps through a lot of hoops at this + // point to ensure that the glyph remains within the target + // bounding box after rounding to font definition units. + // This is irrelevant here as we're not rounding, we're + // staying in f64 and heading straight to rendering. + + // Apply prescribed alignment + group.y = self.aligned_y(group, metrics); + group.x = self.aligned_x(group, metrics, min_constraint_width); + + // Transfer the scaling and alignment back to the glyph and return. + return .{ + .width = width_factor * glyph.width, + .height = height_factor * glyph.height, + .x = group.x + (group.width * self.relative_x), + .y = group.y + (group.height * self.relative_y), + }; + } + + /// Return width and height scaling factors for this scaling group. + fn scale_factors( + self: Constraint, + group: GlyphSize, + metrics: Metrics, + min_constraint_width: u2, + ) struct { f64, f64 } { + if (self.size == .none) { + return .{ 1.0, 1.0 }; } - switch (self.align_vertical) { - .none => {}, - .start => g.y = 0, - .end => g.y = h - g.height, - .center => g.y = (h - g.height) / 2, + const multi_cell = (min_constraint_width > 1); + + const pad_width_factor = @as(f64, @floatFromInt(min_constraint_width)) - (self.pad_left + self.pad_right); + const pad_height_factor = 1 - (self.pad_bottom + self.pad_top); + + const target_width = pad_width_factor * metrics.face_width; + const target_height = pad_height_factor * switch (self.height) { + .cell => metrics.face_height, + // Like font-patcher, the icon constraint height depends on the + // constraint width. Unlike font-patcher, the multi-cell + // icon_height may be different from face_height due to the + // `adjust-icon-height` config option. + .icon => if (multi_cell) + metrics.icon_height + else + metrics.icon_height_single, + }; + + var width_factor = target_width / group.width; + var height_factor = target_height / group.height; + + switch (self.size) { + .none => unreachable, + .fit => { + // Scale down to fit if needed + height_factor = @min(1, width_factor, height_factor); + width_factor = height_factor; + }, + .cover => { + // Scale to cover + height_factor = @min(width_factor, height_factor); + width_factor = height_factor; + }, + .fit_cover1 => { + // Scale down to fit or up to cover at least one cell + // NOTE: This is similar to font_patcher's "pa" mode, + // however, font_patcher will only do the upscaling + // part if the constraint width is 1, resulting in + // some icons becoming smaller when the constraint + // width increases. You'd see icons shrinking when + // opening up a space after them. This makes no + // sense, so we've fixed the rule such that these + // icons are scaled to the same size for multi-cell + // constraints as they would be for single-cell. + height_factor = @min(width_factor, height_factor); + if (multi_cell and (height_factor > 1)) { + // Call back into this function with + // constraint width 1 to get single-cell scale + // factors. We use the height factor as width + // could have been modified by max_xy_ratio. + _, const single_height_factor = self.scale_factors(group, metrics, 1); + height_factor = @max(1, single_height_factor); + } + width_factor = height_factor; + }, + .stretch => {}, } - // Re-add our padding before returning. - g.x += self.pad_left * available_width; - g.y += self.pad_bottom * available_height; + // Reduce aspect ratio if required + if (self.max_xy_ratio) |ratio| { + if (group.width * width_factor > group.height * height_factor * ratio) { + width_factor = group.height * height_factor * ratio / group.width; + } + } - // If the available height is less than the cell height, we - // add half of the difference to center it in the full height. - // - // If necessary, in the future, we can adjust this to account - // for alignment, but that isn't necessary with any of the nf - // icons afaict. - const cell_height: f64 = @floatFromInt(metrics.cell_height); - g.y += (cell_height - available_height) / 2; + return .{ width_factor, height_factor }; + } - return g; + /// Return vertical bearing for aligning this group + fn aligned_y( + self: Constraint, + group: GlyphSize, + metrics: Metrics, + ) f64 { + if ((self.size == .none) and (self.align_vertical == .none)) { + // If we don't have any constraints affecting the vertical axis, + // we don't touch vertical alignment. + return group.y; + } + // We use face_height and offset by face_y, rather than + // using cell_height directly, to account for the asymmetry + // of the pixel cell around the face (a consequence of + // aligning the baseline with a pixel boundary rather than + // vertically centering the face). + const pad_bottom_dy = self.pad_bottom * metrics.face_height; + const pad_top_dy = self.pad_top * metrics.face_height; + const start_y = metrics.face_y + pad_bottom_dy; + const end_y = metrics.face_y + (metrics.face_height - group.height - pad_top_dy); + const center_y = (start_y + end_y) / 2; + return switch (self.align_vertical) { + // NOTE: Even if there is no prescribed alignment, we ensure + // that the group doesn't protrude outside the padded cell, + // since this is implied by every available size constraint. If + // the group is too high we fall back to centering, though if we + // hit the .none prong we always have self.size != .none, so + // this should never happen. + .none => if (end_y < start_y) + center_y + else + @max(start_y, @min(group.y, end_y)), + .start => start_y, + .end => end_y, + .center, .center1 => center_y, + }; + } + + /// Return horizontal bearing for aligning this group + fn aligned_x( + self: Constraint, + group: GlyphSize, + metrics: Metrics, + min_constraint_width: u2, + ) f64 { + if ((self.size == .none) and (self.align_horizontal == .none)) { + // If we don't have any constraints affecting the horizontal + // axis, we don't touch horizontal alignment. + return group.x; + } + // For multi-cell constraints, we align relative to the span + // from the left edge of the first cell to the right edge of + // the last face cell assuming it's left-aligned within the + // rounded and adjusted pixel cell. Any horizontal offset to + // center the face within the grid cell is the responsibility + // of the backend-specific rendering code, and should be done + // after applying constraints. + const full_face_span = metrics.face_width + @as(f64, @floatFromInt((min_constraint_width - 1) * metrics.cell_width)); + const pad_left_dx = self.pad_left * metrics.face_width; + const pad_right_dx = self.pad_right * metrics.face_width; + const start_x = pad_left_dx; + const end_x = full_face_span - group.width - pad_right_dx; + return switch (self.align_horizontal) { + // NOTE: Even if there is no prescribed alignment, we ensure + // that the glyph doesn't protrude outside the padded cell, + // since this is implied by every available size constraint. The + // left-side bound has priority if the group is too wide, though + // if we hit the .none prong we always have self.size != .none, + // so this should never happen. + .none => @max(start_x, @min(group.x, end_x)), + .start => start_x, + .end => @max(start_x, end_x), + .center => @max(start_x, (start_x + end_x) / 2), + // NOTE: .center1 implements the font_patcher rule of centering + // in the first cell even for multi-cell constraints. Since glyphs + // are not allowed to protrude to the left, this results in the + // left-alignment like .start when the glyph is wider than a cell. + .center1 => center1: { + const end1_x = metrics.face_width - group.width - pad_right_dx; + break :center1 @max(start_x, (start_x + end1_x) / 2); + }, + }; } }; }; @@ -412,3 +511,197 @@ test "Variation.Id: slnt should be 1936486004" { try testing.expectEqual(@as(u32, 1936486004), @as(u32, @bitCast(id))); try testing.expectEqualStrings("slnt", &(id.str())); } + +test "Constraints" { + const comparison = @import("../datastruct/comparison.zig"); + const getConstraint = @import("nerd_font_attributes.zig").getConstraint; + + // Hardcoded data matches metrics from CoreText at size 12 and DPI 96. + + // Define grid metrics (matches font-family = JetBrains Mono) + const metrics: Metrics = .{ + .cell_width = 10, + .cell_height = 22, + .cell_baseline = 5, + .underline_position = 19, + .underline_thickness = 1, + .strikethrough_position = 12, + .strikethrough_thickness = 1, + .overline_position = 0, + .overline_thickness = 1, + .box_thickness = 1, + .cursor_thickness = 1, + .cursor_height = 22, + .icon_height = 21.12, + .icon_height_single = 44.48 / 3.0, + .face_width = 9.6, + .face_height = 21.12, + .face_y = 0.2, + }; + + // ASCII (no constraint). + { + const constraint: RenderOptions.Constraint = .none; + + // BBox of 'x' from JetBrains Mono. + const glyph_x: GlyphSize = .{ + .width = 6.784, + .height = 15.28, + .x = 1.408, + .y = 4.84, + }; + + // Any constraint width: do nothing. + inline for (.{ 1, 2 }) |constraint_width| { + try comparison.expectApproxEqual( + glyph_x, + constraint.constrain(glyph_x, metrics, constraint_width), + ); + } + } + + // Symbol (same constraint as hardcoded in Renderer.addGlyph). + { + const constraint: RenderOptions.Constraint = .{ .size = .fit }; + + // BBox of '■' (0x25A0 black square) from Iosevka. + // NOTE: This glyph is designed to span two cells. + const glyph_25A0: GlyphSize = .{ + .width = 10.272, + .height = 10.272, + .x = 2.864, + .y = 5.304, + }; + + // Constraint width 1: scale down and shift to fit a single cell. + try comparison.expectApproxEqual( + GlyphSize{ + .width = metrics.face_width, + .height = metrics.face_width, + .x = 0, + .y = 5.64, + }, + constraint.constrain(glyph_25A0, metrics, 1), + ); + + // Constraint width 2: do nothing. + try comparison.expectApproxEqual( + glyph_25A0, + constraint.constrain(glyph_25A0, metrics, 2), + ); + } + + // Emoji (same constraint as hardcoded in SharedGrid.renderGlyph). + { + const constraint: RenderOptions.Constraint = .{ + .size = .cover, + .align_horizontal = .center, + .align_vertical = .center, + .pad_left = 0.025, + .pad_right = 0.025, + }; + + // BBox of '🥸' (0x1F978) from Apple Color Emoji. + const glyph_1F978: GlyphSize = .{ + .width = 20, + .height = 20, + .x = 0.46, + .y = 1, + }; + + // Constraint width 2: scale to cover two cells with padding, center; + try comparison.expectApproxEqual( + GlyphSize{ + .width = 18.72, + .height = 18.72, + .x = 0.44, + .y = 1.4, + }, + constraint.constrain(glyph_1F978, metrics, 2), + ); + } + + // Nerd Font default. + { + const constraint = getConstraint(0xea61).?; + + // Verify that this is the constraint we expect. + try std.testing.expectEqual(.fit_cover1, constraint.size); + try std.testing.expectEqual(.icon, constraint.height); + try std.testing.expectEqual(.center1, constraint.align_horizontal); + try std.testing.expectEqual(.center1, constraint.align_vertical); + + // BBox of '' (0xEA61 nf-cod-lightbulb) from Symbols Only. + // NOTE: This icon is part of a group, so the + // constraint applies to a larger bounding box. + const glyph_EA61: GlyphSize = .{ + .width = 9.015625, + .height = 13.015625, + .x = 3.015625, + .y = 3.76525, + }; + + // Constraint width 1: scale and shift group to fit a single cell. + try comparison.expectApproxEqual( + GlyphSize{ + .width = 7.2125, + .height = 10.4125, + .x = 0.8125, + .y = 5.950695224719102, + }, + constraint.constrain(glyph_EA61, metrics, 1), + ); + + // Constraint width 2: no scaling; left-align and vertically center group. + try comparison.expectApproxEqual( + GlyphSize{ + .width = glyph_EA61.width, + .height = glyph_EA61.height, + .x = 1.015625, + .y = 4.7483690308988775, + }, + constraint.constrain(glyph_EA61, metrics, 2), + ); + } + + // Nerd Font stretch. + { + const constraint = getConstraint(0xe0c0).?; + + // Verify that this is the constraint we expect. + try std.testing.expectEqual(.stretch, constraint.size); + try std.testing.expectEqual(.cell, constraint.height); + try std.testing.expectEqual(.start, constraint.align_horizontal); + try std.testing.expectEqual(.center1, constraint.align_vertical); + + // BBox of ' ' (0xE0C0 nf-ple-flame_thick) from Symbols Only. + const glyph_E0C0: GlyphSize = .{ + .width = 16.796875, + .height = 16.46875, + .x = -0.796875, + .y = 1.7109375, + }; + + // Constraint width 1: stretch and position to exactly cover one cell. + try comparison.expectApproxEqual( + GlyphSize{ + .width = @floatFromInt(metrics.cell_width), + .height = @floatFromInt(metrics.cell_height), + .x = 0, + .y = 0, + }, + constraint.constrain(glyph_E0C0, metrics, 1), + ); + + // Constraint width 1: stretch and position to exactly cover two cells. + try comparison.expectApproxEqual( + GlyphSize{ + .width = @floatFromInt(2 * metrics.cell_width), + .height = @floatFromInt(metrics.cell_height), + .x = 0, + .y = 0, + }, + constraint.constrain(glyph_E0C0, metrics, 2), + ); + } +} diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index cb9993cbf..9e7bc4d5d 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -319,17 +319,6 @@ pub const Face = struct { rect.origin.y -= line_width / 2; }; - // We make an assumption that font smoothing ("thicken") - // adds no more than 1 extra pixel to any edge. We don't - // add extra size if it's a sbix color font though, since - // bitmaps aren't affected by smoothing. - if (opts.thicken and !sbix) { - rect.size.width += 2.0; - rect.size.height += 2.0; - rect.origin.x -= 1.0; - rect.origin.y -= 1.0; - } - // If our rect is smaller than a quarter pixel in either axis // then it has no outlines or they're too small to render. // @@ -349,14 +338,7 @@ pub const Face = struct { const cell_height: f64 = @floatFromInt(metrics.cell_height); // Next we apply any constraints to get the final size of the glyph. - var constraint = opts.constraint; - - // We eliminate any negative vertical padding since these overlap - // values aren't needed with how precisely we apply constraints, - // and they can lead to extra height that looks bad for things like - // powerline glyphs. - constraint.pad_top = @max(0.0, constraint.pad_top); - constraint.pad_bottom = @max(0.0, constraint.pad_bottom); + const constraint = opts.constraint; // We need to add the baseline position before passing to the constrain // function since it operates on cell-relative positions, not baseline. @@ -378,6 +360,18 @@ pub const Face = struct { var width = glyph_size.width; var height = glyph_size.height; + // We center all glyphs within the pixel-rounded and adjusted + // cell width if it's larger than the face width, so that they + // aren't weirdly off to the left. + // + // We don't do this if the glyph has a stretch constraint, + // since in that case the position was already calculated with the + // new cell width in mind. + if ((constraint.size != .stretch) and (metrics.face_width < cell_width)) { + // We add half the difference to re-center. + x += (cell_width - metrics.face_width) / 2; + } + // If this is a bitmap glyph, it will always render as full pixels, // not fractional pixels, so we need to quantize its position and // size accordingly to align to full pixels so we get good results. @@ -388,25 +382,16 @@ pub const Face = struct { y = @round(y); } - // If the cell width was adjusted wider, we re-center all glyphs - // in the new width, so that they aren't weirdly off to the left. - if (metrics.original_cell_width) |original| recenter: { - // We don't do this if the constraint has a horizontal alignment, - // since in that case the position was already calculated with the - // new cell width in mind. - if (opts.constraint.align_horizontal != .none) break :recenter; - - // If the original width was wider then we don't do anything. - if (original >= metrics.cell_width) break :recenter; - - // We add half the difference to re-center. - x += (cell_width - @as(f64, @floatFromInt(original))) / 2; - } + // We make an assumption that font smoothing ("thicken") + // adds no more than 1 extra pixel to any edge. We don't + // add extra size if it's a sbix color font though, since + // bitmaps aren't affected by smoothing. + const canvas_padding: u32 = if (opts.thicken and !sbix) 1 else 0; // Our whole-pixel bearings for the final glyph. // The fractional portion will be included in the rasterized position. - const px_x: i32 = @intFromFloat(@floor(x)); - const px_y: i32 = @intFromFloat(@floor(y)); + const px_x = @as(i32, @intFromFloat(@floor(x))) - @as(i32, @intCast(canvas_padding)); + const px_y = @as(i32, @intFromFloat(@floor(y))) - @as(i32, @intCast(canvas_padding)); // We keep track of the fractional part of the pixel bearings, which // we will add as an offset when rasterizing to make sure we get the @@ -416,9 +401,9 @@ pub const Face = struct { // Add the fractional pixel to the width and height and take // the ceiling to get a canvas size that will definitely fit - // our drawn glyph, including the fractional offset. - const px_width: u32 = @intFromFloat(@ceil(width + frac_x)); - const px_height: u32 = @intFromFloat(@ceil(height + frac_y)); + // our drawn glyph, including the fractional offset and font smoothing. + const px_width = @as(u32, @intFromFloat(@ceil(width + frac_x))) + (2 * canvas_padding); + const px_height = @as(u32, @intFromFloat(@ceil(height + frac_y))) + (2 * canvas_padding); // Settings that are specific to if we are rendering text or emoji. const color: struct { @@ -529,8 +514,8 @@ pub const Face = struct { // `drawGlyphs`, we pass the negated bearings. context.translateCTM( ctx, - frac_x, - frac_y, + frac_x + @as(f64, @floatFromInt(canvas_padding)), + frac_y + @as(f64, @floatFromInt(canvas_padding)), ); // Scale the drawing context so that when we draw diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 82cf107c8..95f05881b 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -170,7 +170,7 @@ pub const Face = struct { if (string.len > 1024) break :skip; var tmp: [512]u16 = undefined; const max = string.len / 2; - for (@as([]const u16, @alignCast(@ptrCast(string))), 0..) |c, j| tmp[j] = @byteSwap(c); + for (@as([]const u16, @ptrCast(@alignCast(string))), 0..) |c, j| tmp[j] = @byteSwap(c); const len = std.unicode.utf16LeToUtf8(buf, tmp[0..max]) catch return string; return buf[0..len]; } @@ -351,26 +351,16 @@ pub const Face = struct { return glyph.*.bitmap.pixel_mode == freetype.c.FT_PIXEL_MODE_BGRA; } - /// Render a glyph using the glyph index. The rendered glyph is stored in the - /// given texture atlas. - pub fn renderGlyph( - self: Face, - alloc: Allocator, - atlas: *font.Atlas, - glyph_index: u32, - opts: font.face.RenderOptions, - ) !Glyph { - self.ft_mutex.lock(); - defer self.ft_mutex.unlock(); - + /// Set the load flags to use when loading a glyph for measurement or + /// rendering. + fn glyphLoadFlags(self: Face, constrained: bool) freetype.LoadFlags { // Hinting should only be enabled if the configured load flags specify // it and the provided constraint doesn't actually do anything, since // if it does, then it'll mess up the hinting anyway when it moves or // resizes the glyph. - const do_hinting = self.load_flags.hinting and !opts.constraint.doesAnything(); + const do_hinting = self.load_flags.hinting and !constrained; - // Load the glyph. - try self.face.loadGlyph(glyph_index, .{ + return .{ // If our glyph has color, we want to render the color .color = self.face.hasColor(), @@ -388,46 +378,75 @@ pub const Face = struct { // else it won't look very good at all. .target_mono = self.load_flags.monochrome, + // Otherwise we select hinter based on the `light` flag. + .target_normal = !self.load_flags.light and !self.load_flags.monochrome, + .target_light = self.load_flags.light and !self.load_flags.monochrome, + // NO_SVG set to true because we don't currently support rendering // SVG glyphs under FreeType, since that requires bundling another // dependency to handle rendering the SVG. .no_svg = true, - }); + }; + } + + /// Get a rect that represents the position and size of the loaded glyph. + fn getGlyphSize(glyph: freetype.c.FT_GlyphSlot) font.face.GlyphSize { + // If we're dealing with an outline glyph then we get the + // outline's bounding box instead of using the built-in + // metrics, since that's more precise and allows better + // cell-fitting. + if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_OUTLINE) { + // Get the glyph's bounding box before we transform it at all. + // We use this rather than the metrics, since it's more precise. + var bbox: freetype.c.FT_BBox = undefined; + _ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox); + + return .{ + .x = f26dot6ToF64(bbox.xMin), + .y = f26dot6ToF64(bbox.yMin), + .width = f26dot6ToF64(bbox.xMax - bbox.xMin), + .height = f26dot6ToF64(bbox.yMax - bbox.yMin), + }; + } + + return .{ + .x = f26dot6ToF64(glyph.*.metrics.horiBearingX), + .y = f26dot6ToF64(glyph.*.metrics.horiBearingY - glyph.*.metrics.height), + .width = f26dot6ToF64(glyph.*.metrics.width), + .height = f26dot6ToF64(glyph.*.metrics.height), + }; + } + + /// Render a glyph using the glyph index. The rendered glyph is stored in the + /// given texture atlas. + pub fn renderGlyph( + self: Face, + alloc: Allocator, + atlas: *font.Atlas, + glyph_index: u32, + opts: font.face.RenderOptions, + ) !Glyph { + self.ft_mutex.lock(); + defer self.ft_mutex.unlock(); + + // Load the glyph. + try self.face.loadGlyph(glyph_index, self.glyphLoadFlags(opts.constraint.doesAnything())); const glyph = self.face.handle.*.glyph; + // For synthetic bold, we embolden the glyph. + if (self.synthetic.bold) { + // We need to scale the embolden amount based on the font size. + // This is a heuristic I found worked well across a variety of + // founts: 1 pixel per 64 units of height. + const font_height: f64 = @floatFromInt(self.face.handle.*.size.*.metrics.height); + const ratio: f64 = 64.0 / 2048.0; + const amount = @ceil(font_height * ratio); + _ = freetype.c.FT_Outline_Embolden(&glyph.*.outline, @intFromFloat(amount)); + } + // We get a rect that represents the position - // and size of the glyph before any changes. - const rect: struct { - x: f64, - y: f64, - width: f64, - height: f64, - } = metrics: { - // If we're dealing with an outline glyph then we get the - // outline's bounding box instead of using the built-in - // metrics, since that's more precise and allows better - // cell-fitting. - if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_OUTLINE) { - // Get the glyph's bounding box before we transform it at all. - // We use this rather than the metrics, since it's more precise. - var bbox: freetype.c.FT_BBox = undefined; - _ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox); - - break :metrics .{ - .x = f26dot6ToF64(bbox.xMin), - .y = f26dot6ToF64(bbox.yMin), - .width = f26dot6ToF64(bbox.xMax - bbox.xMin), - .height = f26dot6ToF64(bbox.yMax - bbox.yMin), - }; - } - - break :metrics .{ - .x = f26dot6ToF64(glyph.*.metrics.horiBearingX), - .y = f26dot6ToF64(glyph.*.metrics.horiBearingY - glyph.*.metrics.height), - .width = f26dot6ToF64(glyph.*.metrics.width), - .height = f26dot6ToF64(glyph.*.metrics.height), - }; - }; + // and size of the glyph before constraints. + const rect = getGlyphSize(glyph); // If our glyph is smaller than a quarter pixel in either axis // then it has no outlines or they're too small to render. @@ -443,30 +462,12 @@ pub const Face = struct { .atlas_y = 0, }; - // For synthetic bold, we embolden the glyph. - if (self.synthetic.bold) { - // We need to scale the embolden amount based on the font size. - // This is a heuristic I found worked well across a variety of - // founts: 1 pixel per 64 units of height. - const font_height: f64 = @floatFromInt(self.face.handle.*.size.*.metrics.height); - const ratio: f64 = 64.0 / 2048.0; - const amount = @ceil(font_height * ratio); - _ = freetype.c.FT_Outline_Embolden(&glyph.*.outline, @intFromFloat(amount)); - } - const metrics = opts.grid_metrics; const cell_width: f64 = @floatFromInt(metrics.cell_width); const cell_height: f64 = @floatFromInt(metrics.cell_height); // Next we apply any constraints to get the final size of the glyph. - var constraint = opts.constraint; - - // We eliminate any negative vertical padding since these overlap - // values aren't needed with how precisely we apply constraints, - // and they can lead to extra height that looks bad for things like - // powerline glyphs. - constraint.pad_top = @max(0.0, constraint.pad_top); - constraint.pad_bottom = @max(0.0, constraint.pad_bottom); + const constraint = opts.constraint; // We need to add the baseline position before passing to the constrain // function since it operates on cell-relative positions, not baseline. @@ -488,6 +489,24 @@ pub const Face = struct { var x = glyph_size.x; var y = glyph_size.y; + // We center all glyphs within the pixel-rounded and adjusted + // cell width if it's larger than the face width, so that they + // aren't weirdly off to the left. + // + // We don't do this if the glyph has a stretch constraint, + // since in that case the position was already calculated with the + // new cell width in mind. + if ((constraint.size != .stretch) and (metrics.face_width < cell_width)) { + // We add half the difference to re-center. + // + // NOTE: We round this to a whole-pixel amount because under + // FreeType, the outlines will be hinted, which isn't + // the case under CoreText. If we move the outlines by + // a non-whole-pixel amount, it completely ruins the + // hinting. + x += @round((cell_width - metrics.face_width) / 2); + } + // If this is a bitmap glyph, it will always render as full pixels, // not fractional pixels, so we need to quantize its position and // size accordingly to align to full pixels so we get good results. @@ -498,27 +517,6 @@ pub const Face = struct { y = @round(y); } - // If the cell width was adjusted wider, we re-center all glyphs - // in the new width, so that they aren't weirdly off to the left. - if (metrics.original_cell_width) |original| recenter: { - // We don't do this if the constraint has a horizontal alignment, - // since in that case the position was already calculated with the - // new cell width in mind. - if (opts.constraint.align_horizontal != .none) break :recenter; - - // If the original width was wider then we don't do anything. - if (original >= metrics.cell_width) break :recenter; - - // We add half the difference to re-center. - // - // NOTE: We round this to a whole-pixel amount because under - // FreeType, the outlines will be hinted, which isn't - // the case under CoreText. If we move the outlines by - // a non-whole-pixel amount, it completely ruins the - // hinting. - x += @round((cell_width - @as(f64, @floatFromInt(original))) / 2); - } - // Now we can render the glyph. var bitmap: freetype.c.FT_Bitmap = undefined; _ = freetype.c.FT_Bitmap_Init(&bitmap); @@ -976,23 +974,15 @@ pub const Face = struct { var c: u8 = ' '; while (c < 127) : (c += 1) { if (face.getCharIndex(c)) |glyph_index| { - if (face.loadGlyph(glyph_index, .{ - .render = false, - .no_svg = true, - })) { + if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) { const glyph = face.handle.*.glyph; max = @max( f26dot6ToF64(glyph.*.advance.x), max, ); - top = @max( - f26dot6ToF64(glyph.*.metrics.horiBearingY), - top, - ); - bottom = @min( - f26dot6ToF64(glyph.*.metrics.horiBearingY - glyph.*.metrics.height), - bottom, - ); + const rect = getGlyphSize(glyph); + top = @max(rect.y + rect.height, top); + bottom = @min(rect.y, bottom); } else |_| {} } } @@ -1031,11 +1021,8 @@ pub const Face = struct { self.ft_mutex.lock(); defer self.ft_mutex.unlock(); if (face.getCharIndex('H')) |glyph_index| { - if (face.loadGlyph(glyph_index, .{ - .render = false, - .no_svg = true, - })) { - break :cap f26dot6ToF64(face.handle.*.glyph.*.metrics.height); + if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) { + break :cap getGlyphSize(face.handle.*.glyph).height; } else |_| {} } break :cap null; @@ -1044,11 +1031,8 @@ pub const Face = struct { self.ft_mutex.lock(); defer self.ft_mutex.unlock(); if (face.getCharIndex('x')) |glyph_index| { - if (face.loadGlyph(glyph_index, .{ - .render = false, - .no_svg = true, - })) { - break :ex f26dot6ToF64(face.handle.*.glyph.*.metrics.height); + if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) { + break :ex getGlyphSize(face.handle.*.glyph).height; } else |_| {} } break :ex null; @@ -1063,10 +1047,7 @@ pub const Face = struct { const glyph = face.getCharIndex('水') orelse break :ic_width null; - face.loadGlyph(glyph, .{ - .render = false, - .no_svg = true, - }) catch break :ic_width null; + face.loadGlyph(glyph, self.glyphLoadFlags(false)) catch break :ic_width null; const ft_glyph = face.handle.*.glyph; @@ -1078,21 +1059,19 @@ pub const Face = struct { // This can sometimes happen if there's a CJK font that has been // patched with the nerd fonts patcher and it butchers the advance // values so the advance ends up half the width of the actual glyph. - if (ft_glyph.*.metrics.width > ft_glyph.*.advance.x) { + const ft_glyph_width = getGlyphSize(ft_glyph).width; + const advance = f26dot6ToF64(ft_glyph.*.advance.x); + if (ft_glyph_width > advance) { var buf: [1024]u8 = undefined; const font_name = self.name(&buf) catch ""; log.warn( "(getMetrics) Width of glyph '水' for font \"{s}\" is greater than its advance ({d} > {d}), discarding ic_width metric.", - .{ - font_name, - f26dot6ToF64(ft_glyph.*.metrics.width), - f26dot6ToF64(ft_glyph.*.advance.x), - }, + .{ font_name, ft_glyph_width, advance }, ); break :ic_width null; } - break :ic_width f26dot6ToF64(ft_glyph.*.advance.x); + break :ic_width advance; }; return .{ @@ -1168,7 +1147,7 @@ test { ft_font.glyphIndex('A').?, .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) }, ); - try testing.expectEqual(@as(u32, 20), g2.height); + try testing.expectEqual(@as(u32, 21), g2.height); } } @@ -1202,37 +1181,6 @@ test "color emoji" { const glyph_id = ft_font.glyphIndex('🥸').?; try testing.expect(ft_font.isColorGlyph(glyph_id)); } - - // resize - // TODO: Comprehensive tests for constraints, - // this is just an adapted legacy test. - { - const glyph = try ft_font.renderGlyph( - alloc, - &atlas, - ft_font.glyphIndex('🥸').?, - .{ .grid_metrics = .{ - .cell_width = 13, - .cell_height = 24, - .cell_baseline = 0, - .underline_position = 0, - .underline_thickness = 0, - .strikethrough_position = 0, - .strikethrough_thickness = 0, - .overline_position = 0, - .overline_thickness = 0, - .box_thickness = 0, - .cursor_height = 0, - .icon_height = 0, - }, .constraint_width = 2, .constraint = .{ - .size_horizontal = .cover, - .size_vertical = .cover, - .align_horizontal = .center, - .align_vertical = .center, - } }, - ); - try testing.expectEqual(@as(u32, 24), glyph.height); - } } test "mono to bgra" { diff --git a/src/font/nerd_font_attributes.zig b/src/font/nerd_font_attributes.zig index 11902d310..f4a19d963 100644 --- a/src/font/nerd_font_attributes.zig +++ b/src/font/nerd_font_attributes.zig @@ -6,1059 +6,2768 @@ const Constraint = @import("face.zig").RenderOptions.Constraint; -/// Get the a constraints for the provided codepoint. +/// Get the constraints for the provided codepoint. pub fn getConstraint(cp: u21) ?Constraint { return switch (cp) { - 0x2500...0x259f, - => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, - .max_constraint_width = 1, - .align_horizontal = .center, - .align_vertical = .center, - .pad_left = -0.02, - .pad_right = -0.02, - .pad_top = -0.01, - .pad_bottom = -0.01, - }, 0x2630, => .{ - .size_horizontal = .cover, - .size_vertical = .fit, + .size = .cover, .height = .icon, .max_constraint_width = 1, - .align_horizontal = .center, - .align_vertical = .center, - .pad_left = 0.1, - .pad_right = 0.1, - .pad_top = 0.1, - .pad_bottom = 0.1, + .align_horizontal = .center1, + .align_vertical = .center1, + .pad_left = 0.05, + .pad_right = 0.05, + .pad_top = 0.05, + .pad_bottom = 0.05, }, 0x276c...0x276d, => .{ - .size_horizontal = .cover, - .size_vertical = .fit, + .size = .cover, .max_constraint_width = 1, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3999999999999999, - .group_height = 1.1222570532915361, - .group_x = 0.1428571428571428, - .group_y = 0.0349162011173184, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7142857142857143, + .relative_height = 0.8910614525139665, + .relative_x = 0.1428571428571428, + .relative_y = 0.0349162011173184, .pad_top = 0.15, .pad_bottom = 0.15, }, 0x276e...0x276f, => .{ - .size_horizontal = .cover, - .size_vertical = .fit, + .size = .cover, .max_constraint_width = 1, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0115606936416186, - .group_height = 1.1222570532915361, - .group_x = 0.0057142857142857, - .group_y = 0.0125698324022346, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9885714285714285, + .relative_height = 0.8910614525139665, + .relative_x = 0.0057142857142857, + .relative_y = 0.0125698324022346, .pad_top = 0.15, .pad_bottom = 0.15, }, 0x2770...0x2771, => .{ - .size_horizontal = .cover, - .size_vertical = .fit, + .size = .cover, .max_constraint_width = 1, - .align_horizontal = .center, - .align_vertical = .center, + .align_horizontal = .center1, + .align_vertical = .center1, .pad_top = 0.15, .pad_bottom = 0.15, }, + 0xe0a0...0xe0a3, + 0xe0cf, + => .{ + .size = .fit_cover1, + .align_horizontal = .center1, + .align_vertical = .center1, + }, 0xe0b0, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, - .pad_left = -0.06, - .pad_right = -0.06, - .pad_top = -0.01, - .pad_bottom = -0.01, + .align_vertical = .center1, + .pad_left = -0.03, + .pad_right = -0.03, + .pad_top = -0.005, + .pad_bottom = -0.005, .max_xy_ratio = 0.7, }, 0xe0b1, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .max_xy_ratio = 0.7, }, 0xe0b2, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, - .pad_left = -0.06, - .pad_right = -0.06, - .pad_top = -0.01, - .pad_bottom = -0.01, + .align_vertical = .center1, + .pad_left = -0.03, + .pad_right = -0.03, + .pad_top = -0.005, + .pad_bottom = -0.005, .max_xy_ratio = 0.7, }, 0xe0b3, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .max_xy_ratio = 0.7, }, 0xe0b4, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, - .pad_left = -0.06, - .pad_right = -0.06, - .pad_top = -0.01, - .pad_bottom = -0.01, + .align_vertical = .center1, + .pad_left = -0.03, + .pad_right = -0.03, + .pad_top = -0.005, + .pad_bottom = -0.005, .max_xy_ratio = 0.59, }, 0xe0b5, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .max_xy_ratio = 0.5, }, 0xe0b6, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, - .pad_left = -0.06, - .pad_right = -0.06, - .pad_top = -0.01, - .pad_bottom = -0.01, + .align_vertical = .center1, + .pad_left = -0.03, + .pad_right = -0.03, + .pad_top = -0.005, + .pad_bottom = -0.005, .max_xy_ratio = 0.59, }, 0xe0b7, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, .max_xy_ratio = 0.5, }, 0xe0b8, 0xe0bc, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, - .pad_left = -0.05, - .pad_right = -0.05, - .pad_top = -0.01, - .pad_bottom = -0.01, + .align_vertical = .center1, + .pad_left = -0.025, + .pad_right = -0.025, + .pad_top = -0.005, + .pad_bottom = -0.005, }, 0xe0b9, 0xe0bd, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, }, 0xe0ba, 0xe0be, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, - .pad_left = -0.05, - .pad_right = -0.05, - .pad_top = -0.01, - .pad_bottom = -0.01, + .align_vertical = .center1, + .pad_left = -0.025, + .pad_right = -0.025, + .pad_top = -0.005, + .pad_bottom = -0.005, }, 0xe0bb, 0xe0bf, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, }, 0xe0c0, 0xe0c8, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .start, - .align_vertical = .center, - .pad_left = -0.05, - .pad_right = -0.05, - .pad_top = -0.01, - .pad_bottom = -0.01, + .align_vertical = .center1, + .pad_left = -0.025, + .pad_right = -0.025, + .pad_top = -0.005, + .pad_bottom = -0.005, }, 0xe0c1, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, }, 0xe0c2, 0xe0ca, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .end, - .align_vertical = .center, - .pad_left = -0.05, - .pad_right = -0.05, - .pad_top = -0.01, - .pad_bottom = -0.01, + .align_vertical = .center1, + .pad_left = -0.025, + .pad_right = -0.025, + .pad_top = -0.005, + .pad_bottom = -0.005, }, 0xe0c3, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .end, - .align_vertical = .center, + .align_vertical = .center1, }, 0xe0c4, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .start, - .align_vertical = .center, - .pad_left = 0.03, - .pad_right = 0.03, - .pad_top = 0.03, - .pad_bottom = 0.03, + .align_vertical = .center1, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, .max_xy_ratio = 0.86, }, 0xe0c5, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .end, - .align_vertical = .center, - .pad_left = 0.03, - .pad_right = 0.03, - .pad_top = 0.03, - .pad_bottom = 0.03, + .align_vertical = .center1, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, .max_xy_ratio = 0.86, }, 0xe0c6, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .start, - .align_vertical = .center, - .pad_left = 0.03, - .pad_right = 0.03, - .pad_top = 0.03, - .pad_bottom = 0.03, + .align_vertical = .center1, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, .max_xy_ratio = 0.78, }, 0xe0c7, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .end, - .align_vertical = .center, - .pad_left = 0.03, - .pad_right = 0.03, - .pad_top = 0.03, - .pad_bottom = 0.03, + .align_vertical = .center1, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, .max_xy_ratio = 0.78, }, 0xe0cc, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .start, - .align_vertical = .center, - .pad_left = -0.02, - .pad_right = -0.02, - .pad_top = -0.01, - .pad_bottom = -0.01, + .align_vertical = .center1, + .pad_left = -0.01, + .pad_right = -0.01, + .pad_top = -0.005, + .pad_bottom = -0.005, .max_xy_ratio = 0.85, }, 0xe0cd, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .align_horizontal = .start, - .align_vertical = .center, + .align_vertical = .center1, .max_xy_ratio = 0.865, }, 0xe0ce, 0xe0d0...0xe0d1, => .{ - .size_horizontal = .cover, - .size_vertical = .fit, + .size = .fit_cover1, .align_horizontal = .start, - .align_vertical = .center, - }, - 0xe0cf, - 0xe0d3, - 0xe0d5, - => .{ - .size_horizontal = .cover, - .size_vertical = .fit, - .align_horizontal = .center, - .align_vertical = .center, + .align_vertical = .center1, }, 0xe0d2, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, - .pad_left = -0.02, - .pad_right = -0.02, - .pad_top = -0.01, - .pad_bottom = -0.01, + .align_vertical = .center1, + .pad_left = -0.01, + .pad_right = -0.01, + .pad_top = -0.005, + .pad_bottom = -0.005, .max_xy_ratio = 0.7, }, 0xe0d4, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, - .pad_left = -0.02, - .pad_right = -0.02, - .pad_top = -0.01, - .pad_bottom = -0.01, + .align_vertical = .center1, + .pad_left = -0.01, + .pad_right = -0.01, + .pad_top = -0.005, + .pad_bottom = -0.005, .max_xy_ratio = 0.7, }, 0xe0d6, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .start, - .align_vertical = .center, - .pad_left = -0.05, - .pad_right = -0.05, - .pad_top = -0.01, - .pad_bottom = -0.01, + .align_vertical = .center1, + .pad_left = -0.025, + .pad_right = -0.025, + .pad_top = -0.005, + .pad_bottom = -0.005, .max_xy_ratio = 0.7, }, 0xe0d7, => .{ - .size_horizontal = .stretch, - .size_vertical = .stretch, + .size = .stretch, .max_constraint_width = 1, .align_horizontal = .end, - .align_vertical = .center, - .pad_left = -0.05, - .pad_right = -0.05, - .pad_top = -0.01, - .pad_bottom = -0.01, + .align_vertical = .center1, + .pad_left = -0.025, + .pad_right = -0.025, + .pad_top = -0.005, + .pad_bottom = -0.005, .max_xy_ratio = 0.7, }, + 0xe300, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8984375000000000, + .relative_y = 0.0986328125000000, + }, + 0xe301, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8798828125000000, + .relative_y = 0.1171875000000000, + }, + 0xe302, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7646484375000000, + .relative_y = 0.2314453125000000, + }, + 0xe303, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8789062500000000, + .relative_y = 0.1171875000000000, + }, + 0xe304, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9755859375000000, + .relative_y = 0.0244140625000000, + }, + 0xe305, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9960937500000000, + .relative_y = 0.0019531250000000, + }, + 0xe306, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9863281250000000, + .relative_y = 0.0097656250000000, + }, + 0xe307, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9951171875000000, + .relative_y = 0.0039062500000000, + }, + 0xe308, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9785156250000000, + .relative_y = 0.0195312500000000, + }, + 0xe309, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9736328125000000, + .relative_y = 0.0214843750000000, + }, + 0xe30a, + 0xe35f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9648437500000000, + .relative_y = 0.0302734375000000, + }, + 0xe30b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8437500000000000, + .relative_y = 0.1513671875000000, + }, + 0xe30c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8027343750000000, + .relative_y = 0.1835937500000000, + }, + 0xe30d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7753906250000000, + .relative_y = 0.1083984375000000, + }, + 0xe30e, + 0xe365, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9833984375000000, + .relative_y = 0.0166015625000000, + }, + 0xe30f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9716796875000000, + .relative_y = 0.0263671875000000, + }, + 0xe310, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6621093750000000, + .relative_y = 0.0986328125000000, + }, + 0xe311, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6425781250000000, + .relative_y = 0.1171875000000000, + }, + 0xe312, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5322265625000000, + .relative_y = 0.2314453125000000, + }, + 0xe313, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6416015625000000, + .relative_y = 0.1181640625000000, + }, + 0xe314, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7382812500000000, + .relative_y = 0.0195312500000000, + }, + 0xe315, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6787109375000000, + .relative_y = 0.1357421875000000, + }, + 0xe316, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7480468750000000, + .relative_y = 0.0097656250000000, + }, + 0xe317, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7529296875000000, + .relative_y = 0.0048828125000000, + }, + 0xe318, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7314453125000000, + .relative_y = 0.0263671875000000, + }, + 0xe319, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7402343750000000, + .relative_y = 0.0195312500000000, + }, + 0xe31a, + 0xe35e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7294921875000000, + .relative_y = 0.0283203125000000, + }, + 0xe31b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6074218750000000, + .relative_y = 0.1503906250000000, + }, + 0xe31c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7363281250000000, + .relative_y = 0.0224609375000000, + }, + 0xe31d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7460937500000000, + .relative_y = 0.0126953125000000, + }, + 0xe31e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.2675781250000000, + .relative_y = 0.3310546875000000, + }, + 0xe31f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7363281250000000, + .relative_y = 0.0986328125000000, + }, + 0xe320, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7177734375000000, + .relative_y = 0.1171875000000000, + }, + 0xe321, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8085937500000000, + .relative_y = 0.0253906250000000, + }, + 0xe322, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7509765625000000, + .relative_y = 0.0839843750000000, + }, + 0xe323, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8281250000000000, + .relative_y = 0.0097656250000000, + }, + 0xe324, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8349609375000000, + }, + 0xe325, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8154296875000000, + .relative_y = 0.0214843750000000, + }, + 0xe326, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8144531250000000, + .relative_y = 0.0195312500000000, + }, + 0xe327, + 0xe361, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8076171875000000, + .relative_y = 0.0273437500000000, + }, + 0xe328, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6845703125000000, + .relative_y = 0.1503906250000000, + }, + 0xe329, + 0xe367, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8173828125000000, + .relative_y = 0.0175781250000000, + }, + 0xe32a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8105468750000000, + .relative_y = 0.0263671875000000, + }, + 0xe32b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5175781250000000, + .relative_y = 0.2421875000000000, + }, + 0xe32c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6992187500000000, + .relative_y = 0.1005859375000000, + }, + 0xe32d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6787109375000000, + .relative_y = 0.1201171875000000, + }, + 0xe32e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5654296875000000, + .relative_y = 0.2324218750000000, + }, + 0xe32f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7714843750000000, + .relative_y = 0.0273437500000000, + }, + 0xe330, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7148437500000000, + .relative_y = 0.0830078125000000, + }, + 0xe331, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7919921875000000, + .relative_y = 0.0097656250000000, + }, + 0xe332, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7871093750000000, + .relative_y = 0.0126953125000000, + }, + 0xe333, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7714843750000000, + .relative_y = 0.0263671875000000, + }, + 0xe334, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7773437500000000, + .relative_y = 0.0195312500000000, + }, + 0xe335, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7714843750000000, + .relative_y = 0.0283203125000000, + }, + 0xe336, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6503906250000000, + .relative_y = 0.1503906250000000, + }, + 0xe337, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7753906250000000, + .relative_y = 0.0234375000000000, + }, + 0xe338, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7792968750000000, + .relative_y = 0.0185546875000000, + }, + 0xe339, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8445945945945946, + }, + 0xe33a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5283203125000000, + .relative_y = 0.2324218750000000, + }, + 0xe33b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5449218750000000, + .relative_y = 0.2148437500000000, + }, + 0xe33c...0xe33d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5273437500000000, + .relative_y = 0.2324218750000000, + }, + 0xe33e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.3293918918918919, + .relative_y = 0.6706081081081081, + }, + 0xe33f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5200000000000000, + .relative_y = 0.2707692307692308, + }, + 0xe340, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8307692307692308, + .relative_y = 0.0861538461538462, + }, + 0xe341, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8327702702702703, + .relative_y = 0.0050675675675676, + }, + 0xe344, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5307692307692308, + .relative_y = 0.2092307692307692, + }, + 0xe345, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5332112630208333, + .relative_y = 0.2040934244791667, + }, + 0xe347, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8307692307692308, + .relative_y = 0.1246153846153846, + }, + 0xe349, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5307967032967034, + .relative_y = 0.2615384615384616, + }, + 0xe34c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8659995118379302, + .relative_y = 0.1340004881620698, + }, + 0xe34d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9890163534293386, + .relative_y = 0.0002440810349036, + }, + 0xe34f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5751953125000000, + .relative_y = 0.1142578125000000, + }, + 0xe351, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6533203125000000, + .relative_y = 0.1328125000000000, + }, + 0xe352, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5215384615384615, + .relative_y = 0.2846153846153846, + }, + 0xe353, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8308012820512821, + .relative_y = 0.1230448717948718, + }, + 0xe354...0xe356, + 0xe358...0xe359, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9935233160621761, + .relative_y = 0.0025906735751295, + }, + 0xe357, + 0xe3a9, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9961139896373057, + }, + 0xe35a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9935233160621761, + .relative_y = 0.0012953367875648, + }, + 0xe35b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9987046632124352, + .relative_y = 0.0012953367875648, + }, + 0xe360, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7695312500000000, + .relative_y = 0.0302734375000000, + }, + 0xe362, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9902343750000000, + .relative_y = 0.0097656250000000, + }, + 0xe363, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7900390625000000, + .relative_y = 0.0097656250000000, + }, + 0xe364, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8251953125000000, + .relative_y = 0.0097656250000000, + }, + 0xe366, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7832031250000000, + .relative_y = 0.0166015625000000, + }, + 0xe369, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4902343750000000, + .relative_y = 0.2548828125000000, + }, + 0xe36b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9333658774713205, + .relative_y = 0.0266048328044911, + }, + 0xe36c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7076171875000000, + .relative_y = 0.1083984375000000, + }, + 0xe36d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8427734375000000, + .relative_y = 0.0625000000000000, + }, + 0xe36e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7529721467391304, + .relative_y = 0.0956606657608696, + }, + 0xe36f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6835937500000000, + .relative_y = 0.1250000000000000, + }, + 0xe370, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8642578125000000, + .relative_y = 0.0625000000000000, + }, + 0xe371, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6103515625000000, + .relative_y = 0.1933593750000000, + }, + 0xe372, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7949218750000000, + .relative_y = 0.0576171875000000, + }, + 0xe373, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8652343750000000, + .relative_y = 0.0058593750000000, + }, + 0xe374, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.3154296875000000, + .relative_y = 0.2861328125000000, + }, + 0xe375, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6772460937500000, + .relative_y = 0.1303710937500000, + }, + 0xe376, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6992187500000000, + .relative_y = 0.1337890625000000, + }, + 0xe377, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7314453125000000, + .relative_y = 0.1552734375000000, + }, + 0xe378, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7314453125000000, + .relative_y = 0.1542968750000000, + }, + 0xe379, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5751953125000000, + .relative_y = 0.1826171875000000, + }, + 0xe37a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5263671875000000, + .relative_y = 0.2285156250000000, + }, + 0xe37b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5751953125000000, + .relative_y = 0.1835937500000000, + }, + 0xe37d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9003906250000000, + .relative_y = 0.0957031250000000, + }, + 0xe37e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6015625000000000, + .relative_y = 0.2324218750000000, + }, + 0xe37f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5200000000000000, + .relative_y = 0.2784615384615385, + }, + 0xe380, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5200000000000000, + .relative_y = 0.2630769230769231, + }, + 0xe38e...0xe391, + 0xe394, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4990253411306043, + .relative_height = 0.9987012987012988, + .relative_x = 0.4996751137102014, + }, + 0xe392...0xe393, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4996751137102014, + .relative_height = 0.9987012987012988, + .relative_x = 0.4990253411306043, + }, + 0xe395, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5471085120207927, + .relative_height = 0.9987012987012988, + .relative_x = 0.4515919428200130, + }, + 0xe396, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5945419103313840, + .relative_height = 0.9987012987012988, + .relative_x = 0.4041585445094217, + }, + 0xe397, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6426250812215725, + .relative_x = 0.3573749187784275, + }, + 0xe398, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6900584795321637, + .relative_x = 0.3099415204678362, + }, + 0xe399, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7381416504223521, + .relative_x = 0.2618583495776478, + }, + 0xe39a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7855750487329435, + .relative_x = 0.2144249512670565, + }, + 0xe39b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9987004548408057, + .relative_height = 0.9987012987012988, + }, + 0xe39c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8323586744639376, + .relative_height = 0.9935064935064936, + }, + 0xe39d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7855750487329435, + .relative_height = 0.9948051948051948, + }, + 0xe39e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7381416504223521, + .relative_height = 0.9961038961038962, + }, + 0xe39f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6907082521117609, + .relative_height = 0.9961038961038962, + }, + 0xe3a0, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6426250812215725, + .relative_height = 0.9961038961038962, + }, + 0xe3a1, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5945419103313840, + .relative_height = 0.9974025974025974, + }, + 0xe3a2...0xe3a3, + 0xe3a5, + 0xe3a7...0xe3a8, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4990253411306043, + .relative_height = 0.9987012987012988, + }, + 0xe3a4, + 0xe3a6, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4996751137102014, + .relative_height = 0.9987012987012988, + }, + 0xe3aa, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9902343750000000, + .relative_y = 0.0078125000000000, + }, + 0xe3ab, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7900390625000000, + .relative_y = 0.0058593750000000, + }, + 0xe3ac, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8251953125000000, + .relative_y = 0.0078125000000000, + }, + 0xe3ad, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7519531250000000, + .relative_y = 0.0068359375000000, + }, + 0xe3ae, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6152343750000000, + .relative_y = 0.2324218750000000, + }, + 0xe3af, + 0xe3b3, + 0xe3b5...0xe3bb, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9986072423398329, + .relative_y = 0.0013927576601671, + }, + 0xe3b0...0xe3b2, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9958217270194986, + .relative_y = 0.0041782729805014, + }, + 0xe3c1, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6590187942396876, + .relative_y = 0.1349768123016842, + }, + 0xe3c2, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7939956065413717, + }, 0x23fb...0x23fe, 0x2665, 0x26a1, 0x2b58, - 0xe000...0xe0a9, - 0xe4fa...0xe7ef, + 0xe000...0xe00a, + 0xe200...0xe2a9, + 0xe342...0xe343, + 0xe346, + 0xe348, + 0xe34a...0xe34b, + 0xe34e, + 0xe350, + 0xe35c...0xe35d, + 0xe368, + 0xe36a, + 0xe37c, + 0xe381...0xe38d, + 0xe3b4, + 0xe3bc...0xe3c0, + 0xe3c3...0xe3e3, + 0xe5fa...0xe6b8, + 0xe700...0xe8ef, 0xea60, 0xea62...0xea7c, - 0xea7e...0xea98, + 0xea7e...0xea88, + 0xea8a...0xea8c, + 0xea8f...0xea98, 0xeaa3...0xeab3, - 0xeab8...0xead3, - 0xead7...0xeb42, - 0xeb44...0xeb6d, + 0xeab8...0xeac7, + 0xeac9, + 0xeacc...0xead3, + 0xead7...0xeb09, + 0xeb0b...0xeb42, + 0xeb44...0xeb4e, + 0xeb50...0xeb6d, 0xeb72...0xeb89, 0xeb8b...0xeb99, 0xeb9b...0xebd4, 0xebd7...0xec06, 0xec08...0xec0a, 0xec0d...0xec1e, - 0xed00...0xf018, - 0xf01a...0xf02f, - 0xf031...0xf03c, - 0xf041...0xf043, - 0xf045...0xf049, - 0xf04b...0xf050, - 0xf054...0xf059, - 0xf05c...0xf070, - 0xf072...0xf077, - 0xf079...0xf07a, - 0xf07c...0xf080, - 0xf082...0xf08b, - 0xf08d...0xf091, - 0xf093...0xf09e, - 0xf0a0, - 0xf0a5...0xf0a9, - 0xf0ab...0xf0c9, - 0xf0cb...0xf0d5, - 0xf0d7...0xf0dd, - 0xf0df...0xf0e6, - 0xf0e8...0xf295, - 0xf297...0xf2c3, - 0xf2c6...0xf2ef, - 0xf2f1...0xf305, - 0xf307...0xf847, + 0xed00...0xedff, + 0xee0c...0xefce, + 0xf000...0xf004, + 0xf006...0xf025, + 0xf028...0xf02a, + 0xf02c...0xf030, + 0xf034, + 0xf036...0xf043, + 0xf045, + 0xf047, + 0xf053...0xf05f, + 0xf062, + 0xf064...0xf076, + 0xf079...0xf07d, + 0xf07f...0xf088, + 0xf08a...0xf0a3, + 0xf0a6...0xf0d6, + 0xf0db, + 0xf0df...0xf0ff, + 0xf108...0xf12f, + 0xf131...0xf140, + 0xf142...0xf152, + 0xf155, + 0xf15a...0xf174, + 0xf176, + 0xf179...0xf181, + 0xf183...0xf220, + 0xf223, + 0xf22e...0xf254, + 0xf259, + 0xf25c...0xf381, + 0xf400...0xf415, + 0xf417...0xf423, + 0xf425...0xf430, + 0xf435...0xf437, + 0xf439...0xf43d, + 0xf43f...0xf442, + 0xf446...0xf449, + 0xf44c...0xf45b, + 0xf45d...0xf45f, + 0xf462...0xf466, + 0xf468...0xf46b, + 0xf46d...0xf46f, + 0xf471...0xf475, + 0xf477...0xf479, + 0xf47f...0xf48a, + 0xf48c...0xf492, + 0xf494...0xf499, + 0xf49b...0xf4c2, + 0xf4c4...0xf4ee, + 0xf4f3...0xf51c, + 0xf51e...0xf533, 0xf0001...0xf1af0, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, + .align_horizontal = .center1, + .align_vertical = .center1, }, 0xea61, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3310225303292895, - .group_height = 1.0762439807383628, - .group_x = 0.0846354166666667, - .group_y = 0.0708426547352722, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7513020833333334, + .relative_height = 0.9291573452647278, + .relative_x = 0.0846354166666667, + .relative_y = 0.0708426547352722, }, 0xea7d, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1912058627581612, - .group_height = 1.1426759670259987, - .group_x = 0.0917225950782998, - .group_y = 0.0416204217536071, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8394854586129754, + .relative_height = 0.8751387347391787, + .relative_x = 0.0917225950782998, + .relative_y = 0.0416204217536071, }, 0xea99, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0642857142857143, - .group_height = 2.0929152148664345, - .group_x = 0.0302013422818792, - .group_y = 0.2269700332963374, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9395973154362416, + .relative_height = 0.4778024417314096, + .relative_x = 0.0302013422818792, + .relative_y = 0.2269700332963374, }, 0xea9a, 0xeaa1, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3032069970845481, - .group_height = 1.1731770833333333, - .group_x = 0.1526845637583893, - .group_y = 0.0754716981132075, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7673378076062640, + .relative_height = 0.8523862375138734, + .relative_x = 0.1526845637583893, + .relative_y = 0.0754716981132075, }, 0xea9b, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1640625000000000, - .group_height = 1.3134110787172011, - .group_x = 0.0721476510067114, - .group_y = 0.0871254162042175, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8590604026845637, + .relative_height = 0.7613762486126526, + .relative_x = 0.0721476510067114, + .relative_y = 0.0871254162042175, }, 0xea9c, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1640625000000000, - .group_height = 1.3201465201465201, - .group_x = 0.0721476510067114, - .group_y = 0.0832408435072142, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8590604026845637, + .relative_height = 0.7574916759156493, + .relative_x = 0.0721476510067114, + .relative_y = 0.0832408435072142, }, 0xea9d, 0xeaa0, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 2.4493150684931506, - .group_height = 1.9693989071038251, - .group_x = 0.2863534675615212, - .group_y = 0.2763596004439512, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4082774049217002, + .relative_height = 0.5077691453940066, + .relative_x = 0.2863534675615212, + .relative_y = 0.2763596004439512, }, 0xea9e...0xea9f, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.9540983606557376, - .group_height = 2.4684931506849317, - .group_x = 0.2136465324384788, - .group_y = 0.3068812430632630, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5117449664429530, + .relative_height = 0.4051054384017758, + .relative_x = 0.2136465324384788, + .relative_y = 0.3068812430632630, }, 0xeaa2, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.2405228758169935, - .group_height = 1.0595187680461982, - .group_x = 0.0679662802950474, - .group_y = 0.0147523709167545, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8061116965226555, + .relative_height = 0.9438247156716689, + .relative_x = 0.0679662802950474, + .relative_y = 0.0147523709167545, }, 0xeab4, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0054815974941269, - .group_height = 1.8994082840236686, - .group_y = 0.2024922118380062, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9945482866043613, + .relative_height = 0.5264797507788161, + .relative_y = 0.2024922118380062, }, 0xeab5, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.8994082840236686, - .group_height = 1.0054815974941269, - .group_x = 0.2024922118380062, - .group_y = 0.0054517133956386, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5264797507788161, + .relative_height = 0.9945482866043613, + .relative_x = 0.2024922118380062, + .relative_y = 0.0054517133956386, }, 0xeab6, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.8994082840236686, - .group_height = 1.0054815974941269, - .group_x = 0.2710280373831775, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5264797507788161, + .relative_height = 0.9945482866043613, + .relative_x = 0.2710280373831775, }, 0xeab7, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0054815974941269, - .group_height = 1.8994082840236686, - .group_x = 0.0054517133956386, - .group_y = 0.2710280373831775, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9945482866043613, + .relative_height = 0.5264797507788161, + .relative_x = 0.0054517133956386, + .relative_y = 0.2710280373831775, }, 0xead4...0xead5, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.4144620811287478, - .group_x = 0.1483790523690773, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7069825436408977, + .relative_x = 0.1483790523690773, }, 0xead6, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_height = 1.1388535031847133, - .group_y = 0.0687919463087248, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8780760626398211, + .relative_y = 0.0687919463087248, }, 0xeb43, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3631840796019901, - .group_height = 1.0003813300793167, - .group_x = 0.1991657977059437, - .group_y = 0.0003811847221163, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7335766423357665, + .relative_height = 0.9996188152778837, + .relative_x = 0.1991657977059437, + .relative_y = 0.0003811847221163, }, 0xeb6e, 0xeb71, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_height = 2.0183246073298431, - .group_y = 0.2522697795071336, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4954604409857328, + .relative_y = 0.2522697795071336, }, 0xeb6f, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 2.0104712041884816, - .group_x = 0.2493489583333333, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4973958333333333, + .relative_x = 0.2493489583333333, }, 0xeb70, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 2.0104712041884816, - .group_height = 1.0039062500000000, - .group_x = 0.2493489583333333, - .group_y = 0.0038910505836576, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4973958333333333, + .relative_height = 0.9961089494163424, + .relative_x = 0.2493489583333333, + .relative_y = 0.0038910505836576, }, 0xeb8a, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 2.8828125000000000, - .group_height = 2.9818561935339356, - .group_x = 0.2642276422764228, - .group_y = 0.3313050881410256, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.3468834688346883, + .relative_height = 0.3353615785256410, + .relative_x = 0.2642276422764228, + .relative_y = 0.3313050881410256, }, 0xeb9a, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1440626883664857, - .group_height = 1.0595187680461982, - .group_x = 0.0679662802950474, - .group_y = 0.0147523709167545, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8740779768177028, + .relative_height = 0.9438247156716689, + .relative_x = 0.0679662802950474, + .relative_y = 0.0147523709167545, }, 0xebd5, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0727069351230425, - .group_height = 1.0730882652023592, - .group_y = 0.0681102082395584, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9322210636079249, + .relative_height = 0.9318897917604415, + .relative_y = 0.0681102082395584, }, 0xebd6, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_height = 1.0003554839321263, - .group_y = 0.0003553576082064, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9996446423917936, + .relative_y = 0.0003553576082064, }, 0xec07, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 2.8604846818377689, - .group_height = 2.9804665603035656, - .group_x = 0.2615335565120357, - .group_y = 0.3311487268518519, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.3495911047345768, + .relative_height = 0.3355179398148149, + .relative_x = 0.2615335565120357, + .relative_y = 0.3311487268518519, }, 0xec0b, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0721073225265512, - .group_height = 1.0003813300793167, - .group_y = 0.0003811847221163, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9327424400417101, + .relative_height = 0.9996188152778837, + .relative_y = 0.0003811847221163, }, 0xec0c, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.2486979166666667, - .group_x = 0.1991657977059437, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8008342022940563, + .relative_x = 0.1991657977059437, }, - 0xf019, + 0xee00, + 0xee03, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, - .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1253968253968254, + .size = .stretch, + .max_constraint_width = 1, + .align_horizontal = .end, + .align_vertical = .center1, + .relative_width = 0.8681172291296625, + .relative_height = 0.8626692456479691, + .relative_x = 0.1314387211367673, + .relative_y = 0.0686653771760155, + .pad_left = -0.025, + .pad_right = -0.025, + .pad_top = -0.005, + .pad_bottom = -0.005, }, - 0xf030, - 0xf03e, + 0xee01, + 0xee04, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, - .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1253968253968254, - .group_height = 1.1426844014510278, - .group_y = 0.0624338624338624, + .size = .stretch, + .max_constraint_width = 1, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8626692456479691, + .relative_y = 0.0686653771760155, + .pad_left = -0.05, + .pad_right = -0.05, + .pad_top = -0.005, + .pad_bottom = -0.005, }, - 0xf03d, + 0xee02, + 0xee05, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, - .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_height = 1.3328631875881523, - .group_y = 0.1248677248677249, + .size = .stretch, + .max_constraint_width = 1, + .align_horizontal = .start, + .align_vertical = .center1, + .relative_width = 0.8685612788632326, + .relative_height = 0.8626692456479691, + .relative_y = 0.0686653771760155, + .pad_left = -0.025, + .pad_right = -0.025, + .pad_top = -0.005, + .pad_bottom = -0.005, }, - 0xf03f, + 0xee06, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, - .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.8003104407193382, - .group_x = 0.0005406676069582, + .size = .cover, + .max_constraint_width = 1, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7059415911379657, + .relative_height = 0.2234524408656266, + .relative_x = 0.1470292044310171, + .relative_y = 0.7765475591343735, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, }, - 0xf040, + 0xee07, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .cover, + .max_constraint_width = 1, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5000000000000000, + .relative_height = 0.7498741821841973, + .relative_x = 0.5000000000000000, + .relative_y = 0.2501258178158027, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, + }, + 0xee08, + => .{ + .size = .cover, + .max_constraint_width = 1, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6299093655589124, + .relative_height = 0.8535480624056366, + .relative_x = 0.3700906344410876, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, + }, + 0xee09, + => .{ + .size = .cover, + .max_constraint_width = 1, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4997483643683945, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, + }, + 0xee0a, + => .{ + .size = .cover, + .max_constraint_width = 1, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6299093655589124, + .relative_height = 0.8535480624056366, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, + }, + 0xee0b, + => .{ + .size = .cover, + .max_constraint_width = 1, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5000000000000000, + .relative_height = 0.7498741821841973, + .relative_y = 0.2501258178158027, + .pad_left = 0.015, + .pad_right = 0.015, + .pad_top = 0.015, + .pad_bottom = 0.015, + }, + 0xf005, + => .{ + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1263939384681190, - .group_height = 1.0007255897868335, - .group_x = 0.0003164442515641, - .group_y = 0.0001959631589261, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9999664113932554, + .relative_y = 0.0000335886067446, + }, + 0xf026...0xf027, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9786184354605580, + .relative_y = 0.0103951316192896, + }, + 0xf02b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9758052740827267, + .relative_y = 0.0238869355863696, + }, + 0xf031...0xf033, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9987922705314010, + .relative_y = 0.0006038647342995, + }, + 0xf035, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9989935587761675, + .relative_y = 0.0004025764895330, }, 0xf044, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0087313432835820, - .group_height = 1.0077472527472529, - .group_y = 0.0002010014265405, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9925925925925926, }, - 0xf04a, + 0xf046, + 0xf153...0xf154, + 0xf158, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1253968253968254, - .group_height = 1.3321224771947897, - .group_y = 0.1247354497354497, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8751322751322751, + .relative_y = 0.0624338624338624, }, + 0xf048, + 0xf04a, + 0xf04e, 0xf051, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.7994923857868019, - .group_height = 1.3321224771947897, - .group_y = 0.1247354497354497, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8577706898990622, + .relative_y = 0.0711892586341537, + }, + 0xf049, + 0xf050, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8579450878868969, + .relative_y = 0.0710148606463189, + }, + 0xf04b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9997041418532618, + .relative_y = 0.0002958581467381, + }, + 0xf04c...0xf04d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8572940020656472, + .relative_y = 0.0713404035569438, + }, + 0xf04f, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7138835298072554, + .relative_y = 0.1433479295317200, }, 0xf052, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1439802384724422, - .group_height = 1.1430071621244535, - .group_y = 0.0626172338785870, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9999748091795350, }, - 0xf053, + 0xf060...0xf061, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 2.0025185185185186, - .group_height = 1.1416267186919362, - .group_y = 0.0620882827561120, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8567975830815709, + .relative_y = 0.0719033232628399, }, - 0xf05a...0xf05b, - 0xf0aa, + 0xf063, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0012592592592593, - .group_height = 1.0002824582824583, - .group_y = 0.0002010014265405, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9987915407854985, + .relative_y = 0.0006042296072508, }, - 0xf071, + 0xf077, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1253968253968254, - .group_height = 1.1426844014510278, - .group_x = 0.0004701457451810, - .group_y = 0.0624338624338624, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5700483091787439, + .relative_y = 0.2862318840579710, }, 0xf078, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1434320241691844, - .group_height = 2.0026841590612778, - .group_y = 0.1879786499051550, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5700483091787439, + .relative_y = 0.1437198067632850, }, - 0xf07b, + 0xf07e, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1253968253968254, - .group_height = 1.2285368802902055, - .group_y = 0.0930118110236220, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4989429175475687, + .relative_y = 0.2505285412262157, }, - 0xf081, - 0xf092, + 0xf089, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1441233373639663, - .group_height = 1.1430071621244535, - .group_y = 0.0626172338785870, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9998488512696494, + .relative_y = 0.0001511487303507, }, - 0xf08c, + 0xf0a4...0xf0a5, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.2859733978234582, - .group_height = 1.1426844014510278, - .group_y = 0.0624338624338624, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7502645502645503, + .relative_y = 0.1248677248677249, }, - 0xf09f, + 0xf0d7, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.7489690176588770, - .group_x = 0.0006952841596131, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4281400966183575, + .relative_y = 0.2053140096618357, }, - 0xf0a1, + 0xf0d8, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1253968253968254, - .group_height = 1.0749103295228757, - .group_y = 0.0349409448818898, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.4281400966183575, + .relative_y = 0.3472222222222222, }, - 0xf0a2, + 0xf0d9, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.1429529187840552, - .group_height = 1.0002824582824583, - .group_x = 0.0001253913778381, - .group_y = 0.0002010014265405, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7140772371750631, + .relative_y = 0.1333462732919255, }, - 0xf0a3, + 0xf0da, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0005921977940631, - .group_height = 1.0001448722153810, - .group_x = 0.0005918473033957, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7140396210163651, + .relative_y = 0.1333838894506235, }, - 0xf0a4, + 0xf0dc, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0012592592592593, - .group_height = 1.3332396658348704, - .group_y = 0.1250334663306335, + .align_horizontal = .center1, }, - 0xf0ca, + 0xf0dd, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0335226652102676, - .group_height = 1.2308163060897437, - .group_y = 0.0938253501046103, - }, - 0xf0d6, - => .{ - .size_horizontal = .fit, - .size_vertical = .fit, - .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_height = 1.4330042313117066, - .group_y = 0.1510826771653543, + .align_horizontal = .center1, + .relative_height = 0.4275362318840580, + .relative_y = 0.0012077294685990, }, 0xf0de, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3984670905653893, - .group_height = 2.6619718309859155, - .group_x = 0.0004030632809351, - .group_y = 0.5708994708994709, + .align_horizontal = .center1, + .relative_height = 0.4287439613526570, + .relative_y = 0.5712560386473430, }, - 0xf0e7, + 0xf100...0xf101, + 0xf104...0xf105, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3348918927786344, - .group_height = 1.0001196386424678, - .group_x = 0.0006021702214782, - .group_y = 0.0001196243307751, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8573155985489722, + .relative_y = 0.0713422007255139, }, - 0xf296, + 0xf102, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0005202277820979, - .group_height = 1.0386597451628128, - .group_x = 0.0001795653226322, - .group_y = 0.0187142907131644, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9286577992744861, + .relative_y = 0.0713422007255139, }, - 0xf2c4, + 0xf103, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3292088488938882, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9286577992744861, }, - 0xf2c5, + 0xf106...0xf107, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0118264574212998, - .group_height = 1.1664315937940761, - .group_x = 0.0004377219006858, - .group_y = 0.0713422007255139, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.5000000000000000, + .relative_y = 0.2853688029020556, }, - 0xf2f0, + 0xf130, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0012592592592593, - .group_height = 1.0342088873926949, - .group_y = 0.0165984862232646, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9998602571268865, }, - 0xf306, + 0xf141, => .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit_cover1, .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.3001222493887530, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.2593984962406015, + .relative_y = 0.3696741854636592, + }, + 0xf156, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8752505446623093, + .relative_y = 0.0623155929038282, + }, + 0xf157, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8756468797564688, + .relative_y = 0.0624338624338624, + }, + 0xf159, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8756067947646895, + .relative_y = 0.0623492063492063, + }, + 0xf175, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9989423585404548, + .relative_y = 0.0005288207297726, + }, + 0xf177...0xf178, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6250661025912215, + .relative_y = 0.1877313590692755, + }, + 0xf182, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9998046921689268, + }, + 0xf221, + 0xf224...0xf226, + 0xf228, + 0xf22a, + 0xf22c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9994854643684076, + }, + 0xf222, + 0xf227, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.8746819883943630, + .relative_y = 0.0624017379870223, + }, + 0xf229, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9370837263813853, + .relative_y = 0.0624017379870223, + }, + 0xf22b, + 0xf22d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6874767744332962, + .relative_y = 0.1560043449675557, + }, + 0xf255...0xf256, + 0xf25a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9993997599039616, + }, + 0xf257, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7810124049619848, + .relative_y = 0.0935945806894186, + }, + 0xf258, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.7498142113988452, + .relative_y = 0.1247927742525582, + }, + 0xf25b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.9975006099019084, + }, + 0xf416, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_height = 0.6090604026845637, + .relative_y = 0.2119686800894855, + }, + 0xf424, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5019531250000000, + .relative_height = 0.5755033557046980, + .relative_x = 0.2480468750000000, + .relative_y = 0.2108501118568233, + }, + 0xf431, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6240234375000000, + .relative_height = 0.7695749440715883, + .relative_x = 0.2031250000000000, + .relative_y = 0.1420581655480984, + }, + 0xf432, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6718750000000000, + .relative_height = 0.7147651006711410, + .relative_x = 0.1875000000000000, + .relative_y = 0.1610738255033557, + }, + 0xf433, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6240234375000000, + .relative_height = 0.7695749440715883, + .relative_x = 0.2041015625000000, + .relative_y = 0.0883668903803132, + }, + 0xf434, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6718750000000000, + .relative_height = 0.7147651006711410, + .relative_x = 0.1406250000000000, + .relative_y = 0.1599552572706935, + }, + 0xf438, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.2436523437500000, + .relative_height = 0.4560546875000000, + .relative_x = 0.3813476562500000, + .relative_y = 0.2719726562500000, + }, + 0xf43e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5029296875000000, + .relative_height = 0.5755033557046980, + .relative_x = 0.2500000000000000, + .relative_y = 0.2136465324384788, + }, + 0xf443, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7500000000000000, + .relative_x = 0.1250000000000000, + }, + 0xf444...0xf445, + 0xf4c3, + 0xf51d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5000000000000000, + .relative_height = 0.5000000000000000, + .relative_x = 0.2500000000000000, + .relative_y = 0.2500000000000000, + }, + 0xf44a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.2436523437500000, + .relative_height = 0.4560546875000000, + .relative_x = 0.3750000000000000, + .relative_y = 0.2719726562500000, + }, + 0xf44b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4560546875000000, + .relative_height = 0.2436523437500000, + .relative_x = 0.2719726562500000, + .relative_y = 0.3188476562500000, + }, + 0xf45c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5019531250000000, + .relative_height = 0.5749440715883669, + .relative_x = 0.2480468750000000, + .relative_y = 0.2114093959731544, + }, + 0xf460, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.3593750000000000, + .relative_height = 0.6240234375000000, + .relative_x = 0.3750000000000000, + .relative_y = 0.1884765625000000, + }, + 0xf461, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6237816764132553, + .relative_height = 0.9988851727982163, + .relative_x = 0.1881091617933723, + }, + 0xf467, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5639648437500000, + .relative_height = 0.5649414062500000, + .relative_x = 0.2187500000000000, + .relative_y = 0.2177734375000000, + }, + 0xf46c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5039062500000000, + .relative_height = 0.5771812080536913, + .relative_x = 0.2490234375000000, + .relative_y = 0.2091722595078300, + }, + 0xf470, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9926757812500000, + .relative_height = 0.2690429687500000, + .relative_y = 0.6865234375000000, + }, + 0xf476, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8732325694783033, + .relative_x = 0.0633837152608484, + }, + 0xf47a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.5843079922027290, + .relative_height = 0.9509476031215162, + .relative_x = 0.2066276803118908, + .relative_y = 0.0234113712374582, + }, + 0xf47b...0xf47c, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.6250000000000000, + .relative_height = 0.3593750000000000, + .relative_x = 0.1875000000000000, + .relative_y = 0.3281250000000000, + }, + 0xf47d, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.3593750000000000, + .relative_height = 0.6240234375000000, + .relative_x = 0.2656250000000000, + .relative_y = 0.1875000000000000, + }, + 0xf47e, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.4560546875000000, + .relative_height = 0.2436523437500000, + .relative_x = 0.2719726562500000, + .relative_y = 0.3750000000000000, + }, + 0xf48b, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7187500000000000, + .relative_height = 0.0937500000000000, + .relative_x = 0.1250000000000000, + .relative_y = 0.4687500000000000, + }, + 0xf493, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8313840155945419, + .relative_height = 0.9509476031215162, + .relative_x = 0.0843079922027290, + .relative_y = 0.0234113712374582, + }, + 0xf49a, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.8727450024378351, + .relative_x = 0.0633837152608484, + }, + 0xf4ef, + 0xf4f2, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.7142857142857143, + .relative_x = 0.1428571428571428, + }, + 0xf4f0, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9642857142857143, + .relative_height = 0.7407407407407407, + .relative_y = 0.1111111111111111, + }, + 0xf4f1, + => .{ + .size = .fit_cover1, + .height = .icon, + .align_horizontal = .center1, + .align_vertical = .center1, + .relative_width = 0.9642857142857143, + .relative_height = 0.7407407407407407, + .relative_x = 0.0357142857142857, + .relative_y = 0.1111111111111111, }, else => null, }; diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py index a103a30ac..8ddc0c113 100644 --- a/src/font/nerd_font_codegen.py +++ b/src/font/nerd_font_codegen.py @@ -15,13 +15,14 @@ SymbolsNerdFont (not Mono!) font is passed as the first argument to it. import ast import sys import math -from fontTools.ttLib import TTFont +from fontTools.ttLib import TTFont, TTLibError from fontTools.pens.boundsPen import BoundsPen from collections import defaultdict from contextlib import suppress from pathlib import Path from types import SimpleNamespace from typing import Literal, TypedDict, cast +from urllib.request import urlretrieve type PatchSetAttributes = dict[Literal["default"] | int, PatchSetAttributeEntry] type AttributeHash = tuple[ @@ -50,13 +51,16 @@ class PatchSetAttributeEntry(TypedDict): stretch: str params: dict[str, float | bool] - group_x: float - group_y: float - group_width: float - group_height: float + relative_x: float + relative_y: float + relative_width: float + relative_height: float class PatchSet(TypedDict): + Name: str + Filename: str + Exact: bool SymStart: int SymEnd: int SrcStart: int | None @@ -68,6 +72,18 @@ class PatchSetExtractor(ast.NodeVisitor): def __init__(self) -> None: self.symbol_table: dict[str, ast.expr] = {} self.patch_set_values: list[PatchSet] = [] + self.nf_version: str = "" + + def visit_Assign(self, node): + if ( + node.col_offset == 0 # top-level assignment + and len(node.targets) == 1 # no funny destructuring business + and isinstance(node.targets[0], ast.Name) # no setitem et cetera + and node.targets[0].id == "version" # it's the version string! + ): + self.nf_version = ast.literal_eval(node.value) + else: + return self.generic_visit(node) def visit_ClassDef(self, node: ast.ClassDef) -> None: if node.name != "font_patcher": @@ -113,37 +129,56 @@ class PatchSetExtractor(ast.NodeVisitor): if hasattr(ast, "unparse"): return eval( ast.unparse(node), - {"box_keep": True}, - {"self": SimpleNamespace(args=SimpleNamespace(careful=True))}, + {"box_enabled": False, "box_keep": False}, + { + "self": SimpleNamespace( + args=SimpleNamespace( + careful=False, + custom=False, + fontawesome=True, + fontawesomeextension=True, + fontlogos=True, + octicons=True, + codicons=True, + powersymbols=True, + pomicons=True, + powerline=True, + powerlineextra=True, + material=True, + weather=True, + ) + ), + }, ) msg = f"" raise ValueError(msg) from None def process_patch_entry(self, dict_node: ast.Dict) -> None: entry = {} - disallowed_key_nodes = frozenset({"Enabled", "Name", "Filename", "Exact"}) for key_node, value_node in zip(dict_node.keys, dict_node.values): - if ( - isinstance(key_node, ast.Constant) - and key_node.value not in disallowed_key_nodes - ): + if isinstance(key_node, ast.Constant): + if key_node.value == "Enabled": + if self.safe_literal_eval(value_node): + continue # This patch set is enabled, continue to next key + else: + return # This patch set is disabled, skip key = ast.literal_eval(cast("ast.Constant", key_node)) entry[key] = self.resolve_symbol(value_node) self.patch_set_values.append(cast("PatchSet", entry)) -def extract_patch_set_values(source_code: str) -> list[PatchSet]: +def extract_patch_set_values(source_code: str) -> tuple[list[PatchSet], str]: tree = ast.parse(source_code) extractor = PatchSetExtractor() extractor.visit(tree) - return extractor.patch_set_values + return extractor.patch_set_values, extractor.nf_version def parse_alignment(val: str) -> str | None: return { "l": ".start", "r": ".end", - "c": ".center", + "c": ".center1", # font-patcher specific centering rule, see face.zig "": None, }.get(val, ".none") @@ -158,10 +193,10 @@ def attr_key(attr: PatchSetAttributeEntry) -> AttributeHash: float(params.get("overlap", 0.0)), float(params.get("xy-ratio", -1.0)), float(params.get("ypadding", 0.0)), - float(attr.get("group_x", 0.0)), - float(attr.get("group_y", 0.0)), - float(attr.get("group_width", 1.0)), - float(attr.get("group_height", 1.0)), + float(attr.get("relative_x", 0.0)), + float(attr.get("relative_y", 0.0)), + float(attr.get("relative_width", 1.0)), + float(attr.get("relative_height", 1.0)), ) @@ -187,10 +222,10 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) stretch = attr.get("stretch", "") params = attr.get("params", {}) - group_x = attr.get("group_x", 0.0) - group_y = attr.get("group_y", 0.0) - group_width = attr.get("group_width", 1.0) - group_height = attr.get("group_height", 1.0) + relative_x = attr.get("relative_x", 0.0) + relative_y = attr.get("relative_y", 0.0) + relative_width = attr.get("relative_width", 1.0) + relative_height = attr.get("relative_height", 1.0) overlap = params.get("overlap", 0.0) xy_ratio = params.get("xy-ratio", -1.0) @@ -204,28 +239,30 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) s = f"{keys}\n => .{{\n" - # These translations don't quite capture the way - # the actual patcher does scaling, but they're a - # good enough compromise. - if "xy" in stretch: - s += " .size_horizontal = .stretch,\n" - s += " .size_vertical = .stretch,\n" - elif "!" in stretch or "^" in stretch: - s += " .size_horizontal = .cover,\n" - s += " .size_vertical = .fit,\n" + # This maps the font_patcher stretch rules to a Constrain instance + # NOTE: some comments in font_patcher indicate that only x or y + # would also be a valid spec, but no icons use it, so we won't + # support it until we have to. + if "pa" in stretch: + if "!" in stretch or overlap: + s += " .size = .cover,\n" + else: + s += " .size = .fit_cover1,\n" + elif "xy" in stretch: + s += " .size = .stretch,\n" else: - s += " .size_horizontal = .fit,\n" - s += " .size_vertical = .fit,\n" + print(f"Warning: Unknown stretch rule {stretch}") - # `^` indicates that scaling should fill - # the whole cell, not just the icon height. + # `^` indicates that scaling should use the + # full cell height, not just the icon height, + # even when the constraint width is 1 if "^" not in stretch: s += " .height = .icon,\n" # There are two cases where we want to limit the constraint width to 1: # - If there's a `1` in the stretch mode string. - # - If the stretch mode is `xy` and there's not an explicit `2`. - if "1" in stretch or ("xy" in stretch and "2" not in stretch): + # - If the stretch mode is not `pa` and there's not an explicit `2`. + if "1" in stretch or ("pa" not in stretch and "2" not in stretch): s += " .max_constraint_width = 1,\n" if align is not None: @@ -233,24 +270,24 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) if valign is not None: s += f" .align_vertical = {valign},\n" - if group_width != 1.0: - s += f" .group_width = {group_width:.16f},\n" - if group_height != 1.0: - s += f" .group_height = {group_height:.16f},\n" - if group_x != 0.0: - s += f" .group_x = {group_x:.16f},\n" - if group_y != 0.0: - s += f" .group_y = {group_y:.16f},\n" + if relative_width != 1.0: + s += f" .relative_width = {relative_width:.16f},\n" + if relative_height != 1.0: + s += f" .relative_height = {relative_height:.16f},\n" + if relative_x != 0.0: + s += f" .relative_x = {relative_x:.16f},\n" + if relative_y != 0.0: + s += f" .relative_y = {relative_y:.16f},\n" # `overlap` and `ypadding` are mutually exclusive, # this is asserted in the nerd fonts patcher itself. if overlap: - pad = -overlap + pad = -overlap / 2 s += f" .pad_left = {pad},\n" s += f" .pad_right = {pad},\n" # In the nerd fonts patcher, overlap values # are capped at 0.01 in the vertical direction. - v_pad = -min(0.01, overlap) + v_pad = -min(0.01, overlap) / 2 s += f" .pad_top = {v_pad},\n" s += f" .pad_bottom = {v_pad},\n" elif y_padding: @@ -264,54 +301,236 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) return s +def generate_codepoint_tables( + patch_sets: list[PatchSet], + nerd_font: TTFont, + nf_version: str, +) -> dict[str, dict[int, int]]: + # We may already have the table saved from a previous run. + if Path("nerd_font_codepoint_tables.py").exists(): + import nerd_font_codepoint_tables + + if nerd_font_codepoint_tables.version == nf_version: + return nerd_font_codepoint_tables.cp_tables + + cp_tables: dict[str, dict[int, int]] = {} + cp_nerdfont_used: set[int] = set() + cmap = nerd_font.getBestCmap() + for entry in patch_sets: + patch_set_name = entry["Name"] + print(f"Info: Extracting codepoint table from patch set '{patch_set_name}'") + + # Extract codepoint map from original font file; download if needed + source_filename = entry["Filename"] + target_folder = Path("nerd_font_symbol_fonts") + target_folder.mkdir(exist_ok=True) + target_file = target_folder / Path(source_filename).name + if not target_file.exists(): + print(f"Info: Downloading '{source_filename}'") + urlretrieve( + f"https://github.com/ryanoasis/nerd-fonts/raw/refs/tags/v{nf_version}/src/glyphs/{source_filename}", + target_file, + ) + try: + with TTFont(target_file) as patchfont: + patch_cmap = patchfont.getBestCmap() + except TTLibError: + # Not a TTF/OTF font. This is OK if this patch set is exact, so we + # let if pass. If there's a problem, later checks will catch it. + patch_cmap = None + + # A glyph's scale rules are specified using its codepoint in + # the original font, which is sometimes different from its + # Nerd Font codepoint. If entry["Exact"] is False, the codepoints are + # mapped according to the following rules: + # * entry["SymStart"] and entry["SymEnd"] denote the patch set's codepoint + # range in the original font. + # * entry["SrcStart"] is the starting point of the patch set's mapped + # codepoint range. It must not be None if entry["Exact"] is False. + # * The destination codepoint range is packed; that is, while there may be + # gaps without glyphs in the original font's codepoint range, there are + # none in the Nerd Font range. Hence there is no constant codepoint + # offset; instead we must iterate through the range and increment the + # destination codepoint every time we encounter a glyph in the original + # font. + # If entry["Exact"] is True, the origin and Nerd Font codepoints are the + # same, gaps included, and entry["SrcStart"] must be None. + if entry["Exact"]: + assert entry["SrcStart"] is None + cp_nerdfont = 0 + else: + assert entry["SrcStart"] + assert patch_cmap is not None + cp_nerdfont = entry["SrcStart"] - 1 + + if patch_set_name not in cp_tables: + # There are several patch sets with the same name, representing + # different codepoint ranges within the same original font. Merging + # these into a single table is OK. However, we need to keep separate + # tables for the different fonts to correctly deal with cases where + # they fill in each other's gaps. + cp_tables[patch_set_name] = {} + for cp_original in range(entry["SymStart"], entry["SymEnd"] + 1): + if patch_cmap and cp_original not in patch_cmap: + continue + if not entry["Exact"]: + cp_nerdfont += 1 + else: + cp_nerdfont = cp_original + if cp_nerdfont not in cmap: + raise ValueError( + f"Missing codepoint in Symbols Only Font: {hex(cp_nerdfont)} in patch set '{patch_set_name}'" + ) + elif cp_nerdfont in cp_nerdfont_used: + raise ValueError( + f"Overlap for codepoint {hex(cp_nerdfont)} in patch set '{patch_set_name}'" + ) + cp_tables[patch_set_name][cp_original] = cp_nerdfont + cp_nerdfont_used.add(cp_nerdfont) + + # Store the table and corresponding Nerd Fonts version together in a module. + with open("nerd_font_codepoint_tables.py", "w") as f: + print( + """#! This is a generated file, produced by nerd_font_codegen.py +#! DO NOT EDIT BY HAND! +#! +#! This file specifies the mapping of codepoints in the original symbol +#! fonts to codepoints in a patched Nerd Font. This is extracted from +#! the nerd fonts patcher script and the symbol font files.""", + file=f, + ) + print(f'version = "{nf_version}"', file=f) + print("cp_tables = {", file=f) + for name, table in cp_tables.items(): + print(f' "{name}": {{', file=f) + for key, value in table.items(): + print(f" {hex(key)}: {hex(value)},", file=f) + print(" },", file=f) + print("}", file=f) + + return cp_tables + + def generate_zig_switch_arms( patch_sets: list[PatchSet], nerd_font: TTFont, + nf_version: str, ) -> str: cmap = nerd_font.getBestCmap() glyphs = nerd_font.getGlyphSet() + cp_tables = generate_codepoint_tables(patch_sets, nerd_font, nf_version) entries: dict[int, PatchSetAttributeEntry] = {} for entry in patch_sets: + patch_set_name = entry["Name"] + print(f"Info: Extracting rules from patch set '{patch_set_name}'") + attributes = entry["Attributes"] + patch_set_entries: dict[int, PatchSetAttributeEntry] = {} - for cp in range(entry["SymStart"], entry["SymEnd"] + 1): - entries[cp] = attributes["default"].copy() + cp_table = cp_tables[patch_set_name] + for cp_original in range(entry["SymStart"], entry["SymEnd"] + 1): + if cp_original not in cp_table: + continue + cp_nerdfont = cp_table[cp_original] + if cp_nerdfont in entries: + raise ValueError( + f"Overlap for codepoint {hex(cp_nerdfont)} in patch set '{patch_set_name}'" + ) + if cp_original in attributes: + patch_set_entries[cp_nerdfont] = attributes[cp_original].copy() + else: + patch_set_entries[cp_nerdfont] = attributes["default"].copy() - entries |= {k: v for k, v in attributes.items() if isinstance(k, int)} - - if entry["ScaleRules"] is not None and "ScaleGroups" in entry["ScaleRules"]: + if entry["ScaleRules"] is not None: for group in entry["ScaleRules"]["ScaleGroups"]: xMin = math.inf yMin = math.inf xMax = -math.inf yMax = -math.inf - individual_bounds: dict[int, tuple[int, int, int ,int]] = {} - for cp in group: - if cp not in cmap: + individual_bounds: dict[int, tuple[int, int, int, int]] = {} + individual_advances: set[float] = set() + for cp_original in group: + if cp_original not in cp_table: + # There is one special case where a scale group includes + # a glyph from the original font that's not in any patch + # set, and hence not in the Symbols Only font. The point + # of this glyph is to add extra vertical padding to a + # stretched (^xy) scale group, which means that its + # scaled and aligned position would span the line height + # plus overlap. Thus, we can use any other stretched + # glyph with overlap as stand-in to get the vertical + # bounds, such as as 0xE0B0 (powerline left hard + # divider). We don't worry about the horizontal bounds, + # as they by design should not affect the group's + # bounding box. + if ( + patch_set_name == "Progress Indicators" + and cp_original == 0xEDFF + ): + glyph = glyphs[cmap[0xE0B0]] + bounds = BoundsPen(glyphSet=glyphs) + glyph.draw(bounds) + yMin = min(bounds.bounds[1], yMin) + yMax = max(bounds.bounds[3], yMax) + else: + # Other cases are due to lazily specified scale + # groups with gaps in the codepoint range. + print( + f"Info: Skipping scale group codepoint {hex(cp_original)}, which does not exist in patch set '{patch_set_name}'" + ) continue - glyph = glyphs[cmap[cp]] + + cp_nerdfont = cp_table[cp_original] + glyph = glyphs[cmap[cp_nerdfont]] + individual_advances.add(glyph.width) bounds = BoundsPen(glyphSet=glyphs) glyph.draw(bounds) - individual_bounds[cp] = bounds.bounds + individual_bounds[cp_nerdfont] = bounds.bounds xMin = min(bounds.bounds[0], xMin) yMin = min(bounds.bounds[1], yMin) xMax = max(bounds.bounds[2], xMax) yMax = max(bounds.bounds[3], yMax) group_width = xMax - xMin group_height = yMax - yMin - for cp in group: - if cp not in cmap or cp not in entries: + group_is_monospace = (len(individual_bounds) > 1) and ( + len(individual_advances) == 1 + ) + for cp_original in group: + if cp_original not in cp_table: continue - this_bounds = individual_bounds[cp] - this_width = this_bounds[2] - this_bounds[0] + cp_nerdfont = cp_table[cp_original] + if ( + # Scale groups may cut across patch sets, but we're only + # updating a single patch set at a time, so we skip + # codepoints not in it. + cp_nerdfont not in patch_set_entries + # Codepoints may contribute to the bounding box of multiple groups, + # but should be scaled according to the first group they are found + # in. Hence, to avoid overwriting, we need to skip codepoints that + # have already been assigned a scale group. + or "relative_height" in patch_set_entries[cp_nerdfont] + ): + continue + this_bounds = individual_bounds[cp_nerdfont] this_height = this_bounds[3] - this_bounds[1] - entries[cp]["group_width"] = group_width / this_width - entries[cp]["group_height"] = group_height / this_height - entries[cp]["group_x"] = (this_bounds[0] - xMin) / group_width - entries[cp]["group_y"] = (this_bounds[1] - yMin) / group_height - - del entries[0] + patch_set_entries[cp_nerdfont]["relative_height"] = ( + this_height / group_height + ) + patch_set_entries[cp_nerdfont]["relative_y"] = ( + this_bounds[1] - yMin + ) / group_height + # Horizontal alignment should only be grouped if the group is monospace, + # that is, if all glyphs in the group have the same advance width. + if group_is_monospace: + this_width = this_bounds[2] - this_bounds[0] + patch_set_entries[cp_nerdfont]["relative_width"] = ( + this_width / group_width + ) + patch_set_entries[cp_nerdfont]["relative_x"] = ( + this_bounds[0] - xMin + ) / group_width + entries |= patch_set_entries # Group codepoints by attribute key grouped = defaultdict[AttributeHash, list[int]](list) @@ -337,7 +556,7 @@ if __name__ == "__main__": patcher_path = project_root / "vendor" / "nerd-fonts" / "font-patcher.py" source = patcher_path.read_text(encoding="utf-8") - patch_set = extract_patch_set_values(source) + patch_set, nf_version = extract_patch_set_values(source) out_path = project_root / "src" / "font" / "nerd_font_attributes.zig" @@ -350,9 +569,9 @@ if __name__ == "__main__": const Constraint = @import("face.zig").RenderOptions.Constraint; -/// Get the a constraints for the provided codepoint. +/// Get the constraints for the provided codepoint. pub fn getConstraint(cp: u21) ?Constraint { return switch (cp) { """) - f.write(generate_zig_switch_arms(patch_set, nerd_font)) + f.write(generate_zig_switch_arms(patch_set, nerd_font, nf_version)) f.write("\n else => null,\n };\n}\n") diff --git a/src/font/nerd_font_codepoint_tables.py b/src/font/nerd_font_codepoint_tables.py new file mode 100644 index 000000000..89a623f1c --- /dev/null +++ b/src/font/nerd_font_codepoint_tables.py @@ -0,0 +1,10449 @@ +#! This is a generated file, produced by nerd_font_codegen.py +#! DO NOT EDIT BY HAND! +#! +#! This file specifies the mapping of codepoints in the original symbol +#! fonts to codepoints in a patched Nerd Font. This is extracted from +#! the nerd fonts patcher script and the symbol font files. +version = "3.4.0" +cp_tables = { + "Seti-UI + Custom": { + 0xe4fa: 0xe5fa, + 0xe4fb: 0xe5fb, + 0xe4fc: 0xe5fc, + 0xe4fd: 0xe5fd, + 0xe4fe: 0xe5fe, + 0xe4ff: 0xe5ff, + 0xe500: 0xe600, + 0xe501: 0xe601, + 0xe502: 0xe602, + 0xe503: 0xe603, + 0xe504: 0xe604, + 0xe505: 0xe605, + 0xe506: 0xe606, + 0xe507: 0xe607, + 0xe508: 0xe608, + 0xe509: 0xe609, + 0xe50a: 0xe60a, + 0xe50b: 0xe60b, + 0xe50c: 0xe60c, + 0xe50d: 0xe60d, + 0xe50e: 0xe60e, + 0xe50f: 0xe60f, + 0xe510: 0xe610, + 0xe511: 0xe611, + 0xe512: 0xe612, + 0xe513: 0xe613, + 0xe514: 0xe614, + 0xe515: 0xe615, + 0xe516: 0xe616, + 0xe517: 0xe617, + 0xe518: 0xe618, + 0xe519: 0xe619, + 0xe51a: 0xe61a, + 0xe51b: 0xe61b, + 0xe51c: 0xe61c, + 0xe51d: 0xe61d, + 0xe51e: 0xe61e, + 0xe51f: 0xe61f, + 0xe520: 0xe620, + 0xe521: 0xe621, + 0xe522: 0xe622, + 0xe523: 0xe623, + 0xe524: 0xe624, + 0xe525: 0xe625, + 0xe526: 0xe626, + 0xe527: 0xe627, + 0xe528: 0xe628, + 0xe529: 0xe629, + 0xe52a: 0xe62a, + 0xe52b: 0xe62b, + 0xe52c: 0xe62c, + 0xe52d: 0xe62d, + 0xe52e: 0xe62e, + 0xe52f: 0xe62f, + 0xe530: 0xe630, + 0xe531: 0xe631, + 0xe532: 0xe632, + 0xe533: 0xe633, + 0xe534: 0xe634, + 0xe535: 0xe635, + 0xe536: 0xe636, + 0xe537: 0xe637, + 0xe538: 0xe638, + 0xe539: 0xe639, + 0xe53a: 0xe63a, + 0xe53b: 0xe63b, + 0xe53c: 0xe63c, + 0xe53d: 0xe63d, + 0xe53e: 0xe63e, + 0xe53f: 0xe63f, + 0xe540: 0xe640, + 0xe541: 0xe641, + 0xe542: 0xe642, + 0xe543: 0xe643, + 0xe544: 0xe644, + 0xe545: 0xe645, + 0xe546: 0xe646, + 0xe547: 0xe647, + 0xe548: 0xe648, + 0xe549: 0xe649, + 0xe54a: 0xe64a, + 0xe54b: 0xe64b, + 0xe54c: 0xe64c, + 0xe54d: 0xe64d, + 0xe54e: 0xe64e, + 0xe54f: 0xe64f, + 0xe550: 0xe650, + 0xe551: 0xe651, + 0xe552: 0xe652, + 0xe553: 0xe653, + 0xe554: 0xe654, + 0xe555: 0xe655, + 0xe556: 0xe656, + 0xe557: 0xe657, + 0xe558: 0xe658, + 0xe559: 0xe659, + 0xe55a: 0xe65a, + 0xe55b: 0xe65b, + 0xe55c: 0xe65c, + 0xe55d: 0xe65d, + 0xe55e: 0xe65e, + 0xe55f: 0xe65f, + 0xe560: 0xe660, + 0xe561: 0xe661, + 0xe562: 0xe662, + 0xe563: 0xe663, + 0xe564: 0xe664, + 0xe565: 0xe665, + 0xe566: 0xe666, + 0xe567: 0xe667, + 0xe568: 0xe668, + 0xe569: 0xe669, + 0xe56a: 0xe66a, + 0xe56b: 0xe66b, + 0xe56c: 0xe66c, + 0xe56d: 0xe66d, + 0xe56e: 0xe66e, + 0xe56f: 0xe66f, + 0xe570: 0xe670, + 0xe571: 0xe671, + 0xe572: 0xe672, + 0xe573: 0xe673, + 0xe574: 0xe674, + 0xe575: 0xe675, + 0xe576: 0xe676, + 0xe577: 0xe677, + 0xe578: 0xe678, + 0xe579: 0xe679, + 0xe57a: 0xe67a, + 0xe57b: 0xe67b, + 0xe57c: 0xe67c, + 0xe57d: 0xe67d, + 0xe57e: 0xe67e, + 0xe57f: 0xe67f, + 0xe580: 0xe680, + 0xe581: 0xe681, + 0xe582: 0xe682, + 0xe583: 0xe683, + 0xe584: 0xe684, + 0xe585: 0xe685, + 0xe586: 0xe686, + 0xe587: 0xe687, + 0xe588: 0xe688, + 0xe589: 0xe689, + 0xe58a: 0xe68a, + 0xe58b: 0xe68b, + 0xe58c: 0xe68c, + 0xe58d: 0xe68d, + 0xe58e: 0xe68e, + 0xe58f: 0xe68f, + 0xe590: 0xe690, + 0xe591: 0xe691, + 0xe592: 0xe692, + 0xe593: 0xe693, + 0xe594: 0xe694, + 0xe595: 0xe695, + 0xe596: 0xe696, + 0xe597: 0xe697, + 0xe598: 0xe698, + 0xe599: 0xe699, + 0xe59a: 0xe69a, + 0xe59b: 0xe69b, + 0xe59c: 0xe69c, + 0xe59d: 0xe69d, + 0xe59e: 0xe69e, + 0xe59f: 0xe69f, + 0xe5a0: 0xe6a0, + 0xe5a1: 0xe6a1, + 0xe5a2: 0xe6a2, + 0xe5a3: 0xe6a3, + 0xe5a4: 0xe6a4, + 0xe5a5: 0xe6a5, + 0xe5a6: 0xe6a6, + 0xe5a7: 0xe6a7, + 0xe5a8: 0xe6a8, + 0xe5a9: 0xe6a9, + 0xe5aa: 0xe6aa, + 0xe5ab: 0xe6ab, + 0xe5ac: 0xe6ac, + 0xe5ad: 0xe6ad, + 0xe5ae: 0xe6ae, + 0xe5af: 0xe6af, + 0xe5b0: 0xe6b0, + 0xe5b1: 0xe6b1, + 0xe5b2: 0xe6b2, + 0xe5b3: 0xe6b3, + 0xe5b4: 0xe6b4, + 0xe5b5: 0xe6b5, + 0xe5b6: 0xe6b6, + 0xe5b7: 0xe6b7, + 0xe5b8: 0xe6b8, + }, + "Heavy Angle Brackets": { + 0x276c: 0x276c, + 0x276d: 0x276d, + 0x276e: 0x276e, + 0x276f: 0x276f, + 0x2770: 0x2770, + 0x2771: 0x2771, + }, + "Progress Indicators": { + 0xee00: 0xee00, + 0xee01: 0xee01, + 0xee02: 0xee02, + 0xee03: 0xee03, + 0xee04: 0xee04, + 0xee05: 0xee05, + 0xee06: 0xee06, + 0xee07: 0xee07, + 0xee08: 0xee08, + 0xee09: 0xee09, + 0xee0a: 0xee0a, + 0xee0b: 0xee0b, + }, + "Devicons": { + 0xe600: 0xe700, + 0xe601: 0xe701, + 0xe602: 0xe702, + 0xe603: 0xe703, + 0xe604: 0xe704, + 0xe605: 0xe705, + 0xe606: 0xe706, + 0xe607: 0xe707, + 0xe608: 0xe708, + 0xe609: 0xe709, + 0xe60a: 0xe70a, + 0xe60b: 0xe70b, + 0xe60c: 0xe70c, + 0xe60d: 0xe70d, + 0xe60e: 0xe70e, + 0xe60f: 0xe70f, + 0xe610: 0xe710, + 0xe611: 0xe711, + 0xe612: 0xe712, + 0xe613: 0xe713, + 0xe614: 0xe714, + 0xe615: 0xe715, + 0xe616: 0xe716, + 0xe617: 0xe717, + 0xe618: 0xe718, + 0xe619: 0xe719, + 0xe61a: 0xe71a, + 0xe61b: 0xe71b, + 0xe61c: 0xe71c, + 0xe61d: 0xe71d, + 0xe61e: 0xe71e, + 0xe61f: 0xe71f, + 0xe620: 0xe720, + 0xe621: 0xe721, + 0xe622: 0xe722, + 0xe623: 0xe723, + 0xe624: 0xe724, + 0xe625: 0xe725, + 0xe626: 0xe726, + 0xe627: 0xe727, + 0xe628: 0xe728, + 0xe629: 0xe729, + 0xe62a: 0xe72a, + 0xe62b: 0xe72b, + 0xe62c: 0xe72c, + 0xe62d: 0xe72d, + 0xe62e: 0xe72e, + 0xe62f: 0xe72f, + 0xe630: 0xe730, + 0xe631: 0xe731, + 0xe632: 0xe732, + 0xe633: 0xe733, + 0xe634: 0xe734, + 0xe635: 0xe735, + 0xe636: 0xe736, + 0xe637: 0xe737, + 0xe638: 0xe738, + 0xe639: 0xe739, + 0xe63a: 0xe73a, + 0xe63b: 0xe73b, + 0xe63c: 0xe73c, + 0xe63d: 0xe73d, + 0xe63e: 0xe73e, + 0xe63f: 0xe73f, + 0xe640: 0xe740, + 0xe641: 0xe741, + 0xe642: 0xe742, + 0xe643: 0xe743, + 0xe644: 0xe744, + 0xe645: 0xe745, + 0xe646: 0xe746, + 0xe647: 0xe747, + 0xe648: 0xe748, + 0xe649: 0xe749, + 0xe64a: 0xe74a, + 0xe64b: 0xe74b, + 0xe64c: 0xe74c, + 0xe64d: 0xe74d, + 0xe64e: 0xe74e, + 0xe64f: 0xe74f, + 0xe650: 0xe750, + 0xe651: 0xe751, + 0xe652: 0xe752, + 0xe653: 0xe753, + 0xe654: 0xe754, + 0xe655: 0xe755, + 0xe656: 0xe756, + 0xe657: 0xe757, + 0xe658: 0xe758, + 0xe659: 0xe759, + 0xe65a: 0xe75a, + 0xe65b: 0xe75b, + 0xe65c: 0xe75c, + 0xe65d: 0xe75d, + 0xe65e: 0xe75e, + 0xe65f: 0xe75f, + 0xe660: 0xe760, + 0xe661: 0xe761, + 0xe662: 0xe762, + 0xe663: 0xe763, + 0xe664: 0xe764, + 0xe665: 0xe765, + 0xe666: 0xe766, + 0xe667: 0xe767, + 0xe668: 0xe768, + 0xe669: 0xe769, + 0xe66a: 0xe76a, + 0xe66b: 0xe76b, + 0xe66c: 0xe76c, + 0xe66d: 0xe76d, + 0xe66e: 0xe76e, + 0xe66f: 0xe76f, + 0xe670: 0xe770, + 0xe671: 0xe771, + 0xe672: 0xe772, + 0xe673: 0xe773, + 0xe674: 0xe774, + 0xe675: 0xe775, + 0xe676: 0xe776, + 0xe677: 0xe777, + 0xe678: 0xe778, + 0xe679: 0xe779, + 0xe67a: 0xe77a, + 0xe67b: 0xe77b, + 0xe67c: 0xe77c, + 0xe67d: 0xe77d, + 0xe67e: 0xe77e, + 0xe67f: 0xe77f, + 0xe680: 0xe780, + 0xe681: 0xe781, + 0xe682: 0xe782, + 0xe683: 0xe783, + 0xe684: 0xe784, + 0xe685: 0xe785, + 0xe686: 0xe786, + 0xe687: 0xe787, + 0xe688: 0xe788, + 0xe689: 0xe789, + 0xe68a: 0xe78a, + 0xe68b: 0xe78b, + 0xe68c: 0xe78c, + 0xe68d: 0xe78d, + 0xe68e: 0xe78e, + 0xe68f: 0xe78f, + 0xe690: 0xe790, + 0xe691: 0xe791, + 0xe692: 0xe792, + 0xe693: 0xe793, + 0xe694: 0xe794, + 0xe695: 0xe795, + 0xe696: 0xe796, + 0xe697: 0xe797, + 0xe698: 0xe798, + 0xe699: 0xe799, + 0xe69a: 0xe79a, + 0xe69b: 0xe79b, + 0xe69c: 0xe79c, + 0xe69d: 0xe79d, + 0xe69e: 0xe79e, + 0xe69f: 0xe79f, + 0xe6a0: 0xe7a0, + 0xe6a1: 0xe7a1, + 0xe6a2: 0xe7a2, + 0xe6a3: 0xe7a3, + 0xe6a4: 0xe7a4, + 0xe6a5: 0xe7a5, + 0xe6a6: 0xe7a6, + 0xe6a7: 0xe7a7, + 0xe6a8: 0xe7a8, + 0xe6a9: 0xe7a9, + 0xe6aa: 0xe7aa, + 0xe6ab: 0xe7ab, + 0xe6ac: 0xe7ac, + 0xe6ad: 0xe7ad, + 0xe6ae: 0xe7ae, + 0xe6af: 0xe7af, + 0xe6b0: 0xe7b0, + 0xe6b1: 0xe7b1, + 0xe6b2: 0xe7b2, + 0xe6b3: 0xe7b3, + 0xe6b4: 0xe7b4, + 0xe6b5: 0xe7b5, + 0xe6b6: 0xe7b6, + 0xe6b7: 0xe7b7, + 0xe6b8: 0xe7b8, + 0xe6b9: 0xe7b9, + 0xe6ba: 0xe7ba, + 0xe6bb: 0xe7bb, + 0xe6bc: 0xe7bc, + 0xe6bd: 0xe7bd, + 0xe6be: 0xe7be, + 0xe6bf: 0xe7bf, + 0xe6c0: 0xe7c0, + 0xe6c1: 0xe7c1, + 0xe6c2: 0xe7c2, + 0xe6c3: 0xe7c3, + 0xe6c4: 0xe7c4, + 0xe6c5: 0xe7c5, + 0xe6c6: 0xe7c6, + 0xe6c7: 0xe7c7, + 0xe6c8: 0xe7c8, + 0xe6c9: 0xe7c9, + 0xe6ca: 0xe7ca, + 0xe6cb: 0xe7cb, + 0xe6cc: 0xe7cc, + 0xe6cd: 0xe7cd, + 0xe6ce: 0xe7ce, + 0xe6cf: 0xe7cf, + 0xe6d0: 0xe7d0, + 0xe6d1: 0xe7d1, + 0xe6d2: 0xe7d2, + 0xe6d3: 0xe7d3, + 0xe6d4: 0xe7d4, + 0xe6d5: 0xe7d5, + 0xe6d6: 0xe7d6, + 0xe6d7: 0xe7d7, + 0xe6d8: 0xe7d8, + 0xe6d9: 0xe7d9, + 0xe6da: 0xe7da, + 0xe6db: 0xe7db, + 0xe6dc: 0xe7dc, + 0xe6dd: 0xe7dd, + 0xe6de: 0xe7de, + 0xe6df: 0xe7df, + 0xe6e0: 0xe7e0, + 0xe6e1: 0xe7e1, + 0xe6e2: 0xe7e2, + 0xe6e3: 0xe7e3, + 0xe6e4: 0xe7e4, + 0xe6e5: 0xe7e5, + 0xe6e6: 0xe7e6, + 0xe6e7: 0xe7e7, + 0xe6e8: 0xe7e8, + 0xe6e9: 0xe7e9, + 0xe6ea: 0xe7ea, + 0xe6eb: 0xe7eb, + 0xe6ec: 0xe7ec, + 0xe6ed: 0xe7ed, + 0xe6ee: 0xe7ee, + 0xe6ef: 0xe7ef, + 0xe6f0: 0xe7f0, + 0xe6f1: 0xe7f1, + 0xe6f2: 0xe7f2, + 0xe6f3: 0xe7f3, + 0xe6f4: 0xe7f4, + 0xe6f5: 0xe7f5, + 0xe6f6: 0xe7f6, + 0xe6f7: 0xe7f7, + 0xe6f8: 0xe7f8, + 0xe6f9: 0xe7f9, + 0xe6fa: 0xe7fa, + 0xe6fb: 0xe7fb, + 0xe6fc: 0xe7fc, + 0xe6fd: 0xe7fd, + 0xe6fe: 0xe7fe, + 0xe6ff: 0xe7ff, + 0xe700: 0xe800, + 0xe701: 0xe801, + 0xe702: 0xe802, + 0xe703: 0xe803, + 0xe704: 0xe804, + 0xe705: 0xe805, + 0xe706: 0xe806, + 0xe707: 0xe807, + 0xe708: 0xe808, + 0xe709: 0xe809, + 0xe70a: 0xe80a, + 0xe70b: 0xe80b, + 0xe70c: 0xe80c, + 0xe70d: 0xe80d, + 0xe70e: 0xe80e, + 0xe70f: 0xe80f, + 0xe710: 0xe810, + 0xe711: 0xe811, + 0xe712: 0xe812, + 0xe713: 0xe813, + 0xe714: 0xe814, + 0xe715: 0xe815, + 0xe716: 0xe816, + 0xe717: 0xe817, + 0xe718: 0xe818, + 0xe719: 0xe819, + 0xe71a: 0xe81a, + 0xe71b: 0xe81b, + 0xe71c: 0xe81c, + 0xe71d: 0xe81d, + 0xe71e: 0xe81e, + 0xe71f: 0xe81f, + 0xe720: 0xe820, + 0xe721: 0xe821, + 0xe722: 0xe822, + 0xe723: 0xe823, + 0xe724: 0xe824, + 0xe725: 0xe825, + 0xe726: 0xe826, + 0xe727: 0xe827, + 0xe728: 0xe828, + 0xe729: 0xe829, + 0xe72a: 0xe82a, + 0xe72b: 0xe82b, + 0xe72c: 0xe82c, + 0xe72d: 0xe82d, + 0xe72e: 0xe82e, + 0xe72f: 0xe82f, + 0xe730: 0xe830, + 0xe731: 0xe831, + 0xe732: 0xe832, + 0xe733: 0xe833, + 0xe734: 0xe834, + 0xe735: 0xe835, + 0xe736: 0xe836, + 0xe737: 0xe837, + 0xe738: 0xe838, + 0xe739: 0xe839, + 0xe73a: 0xe83a, + 0xe73b: 0xe83b, + 0xe73c: 0xe83c, + 0xe73d: 0xe83d, + 0xe73e: 0xe83e, + 0xe73f: 0xe83f, + 0xe740: 0xe840, + 0xe741: 0xe841, + 0xe742: 0xe842, + 0xe743: 0xe843, + 0xe744: 0xe844, + 0xe745: 0xe845, + 0xe746: 0xe846, + 0xe747: 0xe847, + 0xe748: 0xe848, + 0xe749: 0xe849, + 0xe74a: 0xe84a, + 0xe74b: 0xe84b, + 0xe74c: 0xe84c, + 0xe74d: 0xe84d, + 0xe74e: 0xe84e, + 0xe74f: 0xe84f, + 0xe750: 0xe850, + 0xe751: 0xe851, + 0xe752: 0xe852, + 0xe753: 0xe853, + 0xe754: 0xe854, + 0xe755: 0xe855, + 0xe756: 0xe856, + 0xe757: 0xe857, + 0xe758: 0xe858, + 0xe759: 0xe859, + 0xe75a: 0xe85a, + 0xe75b: 0xe85b, + 0xe75c: 0xe85c, + 0xe75d: 0xe85d, + 0xe75e: 0xe85e, + 0xe75f: 0xe85f, + 0xe760: 0xe860, + 0xe761: 0xe861, + 0xe762: 0xe862, + 0xe763: 0xe863, + 0xe764: 0xe864, + 0xe765: 0xe865, + 0xe766: 0xe866, + 0xe767: 0xe867, + 0xe768: 0xe868, + 0xe769: 0xe869, + 0xe76a: 0xe86a, + 0xe76b: 0xe86b, + 0xe76c: 0xe86c, + 0xe76d: 0xe86d, + 0xe76e: 0xe86e, + 0xe76f: 0xe86f, + 0xe770: 0xe870, + 0xe771: 0xe871, + 0xe772: 0xe872, + 0xe773: 0xe873, + 0xe774: 0xe874, + 0xe775: 0xe875, + 0xe776: 0xe876, + 0xe777: 0xe877, + 0xe778: 0xe878, + 0xe779: 0xe879, + 0xe77a: 0xe87a, + 0xe77b: 0xe87b, + 0xe77c: 0xe87c, + 0xe77d: 0xe87d, + 0xe77e: 0xe87e, + 0xe77f: 0xe87f, + 0xe780: 0xe880, + 0xe781: 0xe881, + 0xe782: 0xe882, + 0xe783: 0xe883, + 0xe784: 0xe884, + 0xe785: 0xe885, + 0xe786: 0xe886, + 0xe787: 0xe887, + 0xe788: 0xe888, + 0xe789: 0xe889, + 0xe78a: 0xe88a, + 0xe78b: 0xe88b, + 0xe78c: 0xe88c, + 0xe78d: 0xe88d, + 0xe78e: 0xe88e, + 0xe78f: 0xe88f, + 0xe790: 0xe890, + 0xe791: 0xe891, + 0xe792: 0xe892, + 0xe793: 0xe893, + 0xe794: 0xe894, + 0xe795: 0xe895, + 0xe796: 0xe896, + 0xe797: 0xe897, + 0xe798: 0xe898, + 0xe799: 0xe899, + 0xe79a: 0xe89a, + 0xe79b: 0xe89b, + 0xe79c: 0xe89c, + 0xe79d: 0xe89d, + 0xe79e: 0xe89e, + 0xe79f: 0xe89f, + 0xe7a0: 0xe8a0, + 0xe7a1: 0xe8a1, + 0xe7a2: 0xe8a2, + 0xe7a3: 0xe8a3, + 0xe7a4: 0xe8a4, + 0xe7a5: 0xe8a5, + 0xe7a6: 0xe8a6, + 0xe7a7: 0xe8a7, + 0xe7a8: 0xe8a8, + 0xe7a9: 0xe8a9, + 0xe7aa: 0xe8aa, + 0xe7ab: 0xe8ab, + 0xe7ac: 0xe8ac, + 0xe7ad: 0xe8ad, + 0xe7ae: 0xe8ae, + 0xe7af: 0xe8af, + 0xe7b0: 0xe8b0, + 0xe7b1: 0xe8b1, + 0xe7b2: 0xe8b2, + 0xe7b3: 0xe8b3, + 0xe7b4: 0xe8b4, + 0xe7b5: 0xe8b5, + 0xe7b6: 0xe8b6, + 0xe7b7: 0xe8b7, + 0xe7b8: 0xe8b8, + 0xe7b9: 0xe8b9, + 0xe7ba: 0xe8ba, + 0xe7bb: 0xe8bb, + 0xe7bc: 0xe8bc, + 0xe7bd: 0xe8bd, + 0xe7be: 0xe8be, + 0xe7bf: 0xe8bf, + 0xe7c0: 0xe8c0, + 0xe7c1: 0xe8c1, + 0xe7c2: 0xe8c2, + 0xe7c3: 0xe8c3, + 0xe7c4: 0xe8c4, + 0xe7c5: 0xe8c5, + 0xe7c6: 0xe8c6, + 0xe7c7: 0xe8c7, + 0xe7c8: 0xe8c8, + 0xe7c9: 0xe8c9, + 0xe7ca: 0xe8ca, + 0xe7cb: 0xe8cb, + 0xe7cc: 0xe8cc, + 0xe7cd: 0xe8cd, + 0xe7ce: 0xe8ce, + 0xe7cf: 0xe8cf, + 0xe7d0: 0xe8d0, + 0xe7d1: 0xe8d1, + 0xe7d2: 0xe8d2, + 0xe7d3: 0xe8d3, + 0xe7d4: 0xe8d4, + 0xe7d5: 0xe8d5, + 0xe7d6: 0xe8d6, + 0xe7d7: 0xe8d7, + 0xe7d8: 0xe8d8, + 0xe7d9: 0xe8d9, + 0xe7da: 0xe8da, + 0xe7db: 0xe8db, + 0xe7dc: 0xe8dc, + 0xe7dd: 0xe8dd, + 0xe7de: 0xe8de, + 0xe7df: 0xe8df, + 0xe7e0: 0xe8e0, + 0xe7e1: 0xe8e1, + 0xe7e2: 0xe8e2, + 0xe7e3: 0xe8e3, + 0xe7e4: 0xe8e4, + 0xe7e5: 0xe8e5, + 0xe7e6: 0xe8e6, + 0xe7e7: 0xe8e7, + 0xe7e8: 0xe8e8, + 0xe7e9: 0xe8e9, + 0xe7ea: 0xe8ea, + 0xe7eb: 0xe8eb, + 0xe7ec: 0xe8ec, + 0xe7ed: 0xe8ed, + 0xe7ee: 0xe8ee, + 0xe7ef: 0xe8ef, + }, + "Powerline Symbols": { + 0xe0a0: 0xe0a0, + 0xe0a1: 0xe0a1, + 0xe0a2: 0xe0a2, + 0xe0b0: 0xe0b0, + 0xe0b1: 0xe0b1, + 0xe0b2: 0xe0b2, + 0xe0b3: 0xe0b3, + }, + "Powerline Extra Symbols": { + 0xe0a3: 0xe0a3, + 0xe0b4: 0xe0b4, + 0xe0b5: 0xe0b5, + 0xe0b6: 0xe0b6, + 0xe0b7: 0xe0b7, + 0xe0b8: 0xe0b8, + 0xe0b9: 0xe0b9, + 0xe0ba: 0xe0ba, + 0xe0bb: 0xe0bb, + 0xe0bc: 0xe0bc, + 0xe0bd: 0xe0bd, + 0xe0be: 0xe0be, + 0xe0bf: 0xe0bf, + 0xe0c0: 0xe0c0, + 0xe0c1: 0xe0c1, + 0xe0c2: 0xe0c2, + 0xe0c3: 0xe0c3, + 0xe0c4: 0xe0c4, + 0xe0c5: 0xe0c5, + 0xe0c6: 0xe0c6, + 0xe0c7: 0xe0c7, + 0xe0c8: 0xe0c8, + 0xe0ca: 0xe0ca, + 0xe0cc: 0xe0cc, + 0xe0cd: 0xe0cd, + 0xe0ce: 0xe0ce, + 0xe0cf: 0xe0cf, + 0xe0d0: 0xe0d0, + 0xe0d1: 0xe0d1, + 0xe0d2: 0xe0d2, + 0xe0d4: 0xe0d4, + 0xe0d6: 0xe0d6, + 0xe0d7: 0xe0d7, + 0x2630: 0x2630, + }, + "Pomicons": { + 0xe000: 0xe000, + 0xe001: 0xe001, + 0xe002: 0xe002, + 0xe003: 0xe003, + 0xe004: 0xe004, + 0xe005: 0xe005, + 0xe006: 0xe006, + 0xe007: 0xe007, + 0xe008: 0xe008, + 0xe009: 0xe009, + 0xe00a: 0xe00a, + }, + "Font Awesome": { + 0xed00: 0xed00, + 0xed01: 0xed01, + 0xed02: 0xed02, + 0xed03: 0xed03, + 0xed04: 0xed04, + 0xed05: 0xed05, + 0xed06: 0xed06, + 0xed07: 0xed07, + 0xed08: 0xed08, + 0xed09: 0xed09, + 0xed0a: 0xed0a, + 0xed0b: 0xed0b, + 0xed0c: 0xed0c, + 0xed0d: 0xed0d, + 0xed0e: 0xed0e, + 0xed0f: 0xed0f, + 0xed10: 0xed10, + 0xed11: 0xed11, + 0xed12: 0xed12, + 0xed13: 0xed13, + 0xed14: 0xed14, + 0xed15: 0xed15, + 0xed16: 0xed16, + 0xed17: 0xed17, + 0xed18: 0xed18, + 0xed19: 0xed19, + 0xed1a: 0xed1a, + 0xed1b: 0xed1b, + 0xed1c: 0xed1c, + 0xed1d: 0xed1d, + 0xed1e: 0xed1e, + 0xed1f: 0xed1f, + 0xed20: 0xed20, + 0xed21: 0xed21, + 0xed22: 0xed22, + 0xed23: 0xed23, + 0xed24: 0xed24, + 0xed25: 0xed25, + 0xed26: 0xed26, + 0xed27: 0xed27, + 0xed28: 0xed28, + 0xed29: 0xed29, + 0xed2a: 0xed2a, + 0xed2b: 0xed2b, + 0xed2c: 0xed2c, + 0xed2d: 0xed2d, + 0xed2e: 0xed2e, + 0xed2f: 0xed2f, + 0xed30: 0xed30, + 0xed31: 0xed31, + 0xed32: 0xed32, + 0xed33: 0xed33, + 0xed34: 0xed34, + 0xed35: 0xed35, + 0xed36: 0xed36, + 0xed37: 0xed37, + 0xed38: 0xed38, + 0xed39: 0xed39, + 0xed3a: 0xed3a, + 0xed3b: 0xed3b, + 0xed3c: 0xed3c, + 0xed3d: 0xed3d, + 0xed3e: 0xed3e, + 0xed3f: 0xed3f, + 0xed40: 0xed40, + 0xed41: 0xed41, + 0xed42: 0xed42, + 0xed43: 0xed43, + 0xed44: 0xed44, + 0xed45: 0xed45, + 0xed46: 0xed46, + 0xed47: 0xed47, + 0xed48: 0xed48, + 0xed49: 0xed49, + 0xed4a: 0xed4a, + 0xed4b: 0xed4b, + 0xed4c: 0xed4c, + 0xed4d: 0xed4d, + 0xed4e: 0xed4e, + 0xed4f: 0xed4f, + 0xed50: 0xed50, + 0xed51: 0xed51, + 0xed52: 0xed52, + 0xed53: 0xed53, + 0xed54: 0xed54, + 0xed55: 0xed55, + 0xed56: 0xed56, + 0xed57: 0xed57, + 0xed58: 0xed58, + 0xed59: 0xed59, + 0xed5a: 0xed5a, + 0xed5b: 0xed5b, + 0xed5c: 0xed5c, + 0xed5d: 0xed5d, + 0xed5e: 0xed5e, + 0xed5f: 0xed5f, + 0xed60: 0xed60, + 0xed61: 0xed61, + 0xed62: 0xed62, + 0xed63: 0xed63, + 0xed64: 0xed64, + 0xed65: 0xed65, + 0xed66: 0xed66, + 0xed67: 0xed67, + 0xed68: 0xed68, + 0xed69: 0xed69, + 0xed6a: 0xed6a, + 0xed6b: 0xed6b, + 0xed6c: 0xed6c, + 0xed6d: 0xed6d, + 0xed6e: 0xed6e, + 0xed6f: 0xed6f, + 0xed70: 0xed70, + 0xed71: 0xed71, + 0xed72: 0xed72, + 0xed73: 0xed73, + 0xed74: 0xed74, + 0xed75: 0xed75, + 0xed76: 0xed76, + 0xed77: 0xed77, + 0xed78: 0xed78, + 0xed79: 0xed79, + 0xed7a: 0xed7a, + 0xed7b: 0xed7b, + 0xed7c: 0xed7c, + 0xed7d: 0xed7d, + 0xed7e: 0xed7e, + 0xed7f: 0xed7f, + 0xed80: 0xed80, + 0xed81: 0xed81, + 0xed82: 0xed82, + 0xed83: 0xed83, + 0xed84: 0xed84, + 0xed85: 0xed85, + 0xed86: 0xed86, + 0xed87: 0xed87, + 0xed88: 0xed88, + 0xed89: 0xed89, + 0xed8a: 0xed8a, + 0xed8b: 0xed8b, + 0xed8c: 0xed8c, + 0xed8d: 0xed8d, + 0xed8e: 0xed8e, + 0xed8f: 0xed8f, + 0xed90: 0xed90, + 0xed91: 0xed91, + 0xed92: 0xed92, + 0xed93: 0xed93, + 0xed94: 0xed94, + 0xed95: 0xed95, + 0xed96: 0xed96, + 0xed97: 0xed97, + 0xed98: 0xed98, + 0xed99: 0xed99, + 0xed9a: 0xed9a, + 0xed9b: 0xed9b, + 0xed9c: 0xed9c, + 0xed9d: 0xed9d, + 0xed9e: 0xed9e, + 0xed9f: 0xed9f, + 0xeda0: 0xeda0, + 0xeda1: 0xeda1, + 0xeda2: 0xeda2, + 0xeda3: 0xeda3, + 0xeda4: 0xeda4, + 0xeda5: 0xeda5, + 0xeda6: 0xeda6, + 0xeda7: 0xeda7, + 0xeda8: 0xeda8, + 0xeda9: 0xeda9, + 0xedaa: 0xedaa, + 0xedab: 0xedab, + 0xedac: 0xedac, + 0xedad: 0xedad, + 0xedae: 0xedae, + 0xedaf: 0xedaf, + 0xedb0: 0xedb0, + 0xedb1: 0xedb1, + 0xedb2: 0xedb2, + 0xedb3: 0xedb3, + 0xedb4: 0xedb4, + 0xedb5: 0xedb5, + 0xedb6: 0xedb6, + 0xedb7: 0xedb7, + 0xedb8: 0xedb8, + 0xedb9: 0xedb9, + 0xedba: 0xedba, + 0xedbb: 0xedbb, + 0xedbc: 0xedbc, + 0xedbd: 0xedbd, + 0xedbe: 0xedbe, + 0xedbf: 0xedbf, + 0xedc0: 0xedc0, + 0xedc1: 0xedc1, + 0xedc2: 0xedc2, + 0xedc3: 0xedc3, + 0xedc4: 0xedc4, + 0xedc5: 0xedc5, + 0xedc6: 0xedc6, + 0xedc7: 0xedc7, + 0xedc8: 0xedc8, + 0xedc9: 0xedc9, + 0xedca: 0xedca, + 0xedcb: 0xedcb, + 0xedcc: 0xedcc, + 0xedcd: 0xedcd, + 0xedce: 0xedce, + 0xedcf: 0xedcf, + 0xedd0: 0xedd0, + 0xedd1: 0xedd1, + 0xedd2: 0xedd2, + 0xedd3: 0xedd3, + 0xedd4: 0xedd4, + 0xedd5: 0xedd5, + 0xedd6: 0xedd6, + 0xedd7: 0xedd7, + 0xedd8: 0xedd8, + 0xedd9: 0xedd9, + 0xedda: 0xedda, + 0xeddb: 0xeddb, + 0xeddc: 0xeddc, + 0xeddd: 0xeddd, + 0xedde: 0xedde, + 0xeddf: 0xeddf, + 0xede0: 0xede0, + 0xede1: 0xede1, + 0xede2: 0xede2, + 0xede3: 0xede3, + 0xede4: 0xede4, + 0xede5: 0xede5, + 0xede6: 0xede6, + 0xede7: 0xede7, + 0xede8: 0xede8, + 0xede9: 0xede9, + 0xedea: 0xedea, + 0xedeb: 0xedeb, + 0xedec: 0xedec, + 0xeded: 0xeded, + 0xedee: 0xedee, + 0xedef: 0xedef, + 0xedf0: 0xedf0, + 0xedf1: 0xedf1, + 0xedf2: 0xedf2, + 0xedf3: 0xedf3, + 0xedf4: 0xedf4, + 0xedf5: 0xedf5, + 0xedf6: 0xedf6, + 0xedf7: 0xedf7, + 0xedf8: 0xedf8, + 0xedf9: 0xedf9, + 0xedfa: 0xedfa, + 0xedfb: 0xedfb, + 0xedfc: 0xedfc, + 0xedfd: 0xedfd, + 0xedfe: 0xedfe, + 0xedff: 0xedff, + 0xee0c: 0xee0c, + 0xee0d: 0xee0d, + 0xee0e: 0xee0e, + 0xee0f: 0xee0f, + 0xee10: 0xee10, + 0xee11: 0xee11, + 0xee12: 0xee12, + 0xee13: 0xee13, + 0xee14: 0xee14, + 0xee15: 0xee15, + 0xee16: 0xee16, + 0xee17: 0xee17, + 0xee18: 0xee18, + 0xee19: 0xee19, + 0xee1a: 0xee1a, + 0xee1b: 0xee1b, + 0xee1c: 0xee1c, + 0xee1d: 0xee1d, + 0xee1e: 0xee1e, + 0xee1f: 0xee1f, + 0xee20: 0xee20, + 0xee21: 0xee21, + 0xee22: 0xee22, + 0xee23: 0xee23, + 0xee24: 0xee24, + 0xee25: 0xee25, + 0xee26: 0xee26, + 0xee27: 0xee27, + 0xee28: 0xee28, + 0xee29: 0xee29, + 0xee2a: 0xee2a, + 0xee2b: 0xee2b, + 0xee2c: 0xee2c, + 0xee2d: 0xee2d, + 0xee2e: 0xee2e, + 0xee2f: 0xee2f, + 0xee30: 0xee30, + 0xee31: 0xee31, + 0xee32: 0xee32, + 0xee33: 0xee33, + 0xee34: 0xee34, + 0xee35: 0xee35, + 0xee36: 0xee36, + 0xee37: 0xee37, + 0xee38: 0xee38, + 0xee39: 0xee39, + 0xee3a: 0xee3a, + 0xee3b: 0xee3b, + 0xee3c: 0xee3c, + 0xee3d: 0xee3d, + 0xee3e: 0xee3e, + 0xee3f: 0xee3f, + 0xee40: 0xee40, + 0xee41: 0xee41, + 0xee42: 0xee42, + 0xee43: 0xee43, + 0xee44: 0xee44, + 0xee45: 0xee45, + 0xee46: 0xee46, + 0xee47: 0xee47, + 0xee48: 0xee48, + 0xee49: 0xee49, + 0xee4a: 0xee4a, + 0xee4b: 0xee4b, + 0xee4c: 0xee4c, + 0xee4d: 0xee4d, + 0xee4e: 0xee4e, + 0xee4f: 0xee4f, + 0xee50: 0xee50, + 0xee51: 0xee51, + 0xee52: 0xee52, + 0xee53: 0xee53, + 0xee54: 0xee54, + 0xee55: 0xee55, + 0xee56: 0xee56, + 0xee57: 0xee57, + 0xee58: 0xee58, + 0xee59: 0xee59, + 0xee5a: 0xee5a, + 0xee5b: 0xee5b, + 0xee5c: 0xee5c, + 0xee5d: 0xee5d, + 0xee5e: 0xee5e, + 0xee5f: 0xee5f, + 0xee60: 0xee60, + 0xee61: 0xee61, + 0xee62: 0xee62, + 0xee63: 0xee63, + 0xee64: 0xee64, + 0xee65: 0xee65, + 0xee66: 0xee66, + 0xee67: 0xee67, + 0xee68: 0xee68, + 0xee69: 0xee69, + 0xee6a: 0xee6a, + 0xee6b: 0xee6b, + 0xee6c: 0xee6c, + 0xee6d: 0xee6d, + 0xee6e: 0xee6e, + 0xee6f: 0xee6f, + 0xee70: 0xee70, + 0xee71: 0xee71, + 0xee72: 0xee72, + 0xee73: 0xee73, + 0xee74: 0xee74, + 0xee75: 0xee75, + 0xee76: 0xee76, + 0xee77: 0xee77, + 0xee78: 0xee78, + 0xee79: 0xee79, + 0xee7a: 0xee7a, + 0xee7b: 0xee7b, + 0xee7c: 0xee7c, + 0xee7d: 0xee7d, + 0xee7e: 0xee7e, + 0xee7f: 0xee7f, + 0xee80: 0xee80, + 0xee81: 0xee81, + 0xee82: 0xee82, + 0xee83: 0xee83, + 0xee84: 0xee84, + 0xee85: 0xee85, + 0xee86: 0xee86, + 0xee87: 0xee87, + 0xee88: 0xee88, + 0xee89: 0xee89, + 0xee8a: 0xee8a, + 0xee8b: 0xee8b, + 0xee8c: 0xee8c, + 0xee8d: 0xee8d, + 0xee8e: 0xee8e, + 0xee8f: 0xee8f, + 0xee90: 0xee90, + 0xee91: 0xee91, + 0xee92: 0xee92, + 0xee93: 0xee93, + 0xee94: 0xee94, + 0xee95: 0xee95, + 0xee96: 0xee96, + 0xee97: 0xee97, + 0xee98: 0xee98, + 0xee99: 0xee99, + 0xee9a: 0xee9a, + 0xee9b: 0xee9b, + 0xee9c: 0xee9c, + 0xee9d: 0xee9d, + 0xee9e: 0xee9e, + 0xee9f: 0xee9f, + 0xeea0: 0xeea0, + 0xeea1: 0xeea1, + 0xeea2: 0xeea2, + 0xeea3: 0xeea3, + 0xeea4: 0xeea4, + 0xeea5: 0xeea5, + 0xeea6: 0xeea6, + 0xeea7: 0xeea7, + 0xeea8: 0xeea8, + 0xeea9: 0xeea9, + 0xeeaa: 0xeeaa, + 0xeeab: 0xeeab, + 0xeeac: 0xeeac, + 0xeead: 0xeead, + 0xeeae: 0xeeae, + 0xeeaf: 0xeeaf, + 0xeeb0: 0xeeb0, + 0xeeb1: 0xeeb1, + 0xeeb2: 0xeeb2, + 0xeeb3: 0xeeb3, + 0xeeb4: 0xeeb4, + 0xeeb5: 0xeeb5, + 0xeeb6: 0xeeb6, + 0xeeb7: 0xeeb7, + 0xeeb8: 0xeeb8, + 0xeeb9: 0xeeb9, + 0xeeba: 0xeeba, + 0xeebb: 0xeebb, + 0xeebc: 0xeebc, + 0xeebd: 0xeebd, + 0xeebe: 0xeebe, + 0xeebf: 0xeebf, + 0xeec0: 0xeec0, + 0xeec1: 0xeec1, + 0xeec2: 0xeec2, + 0xeec3: 0xeec3, + 0xeec4: 0xeec4, + 0xeec5: 0xeec5, + 0xeec6: 0xeec6, + 0xeec7: 0xeec7, + 0xeec8: 0xeec8, + 0xeec9: 0xeec9, + 0xeeca: 0xeeca, + 0xeecb: 0xeecb, + 0xeecc: 0xeecc, + 0xeecd: 0xeecd, + 0xeece: 0xeece, + 0xeecf: 0xeecf, + 0xeed0: 0xeed0, + 0xeed1: 0xeed1, + 0xeed2: 0xeed2, + 0xeed3: 0xeed3, + 0xeed4: 0xeed4, + 0xeed5: 0xeed5, + 0xeed6: 0xeed6, + 0xeed7: 0xeed7, + 0xeed8: 0xeed8, + 0xeed9: 0xeed9, + 0xeeda: 0xeeda, + 0xeedb: 0xeedb, + 0xeedc: 0xeedc, + 0xeedd: 0xeedd, + 0xeede: 0xeede, + 0xeedf: 0xeedf, + 0xeee0: 0xeee0, + 0xeee1: 0xeee1, + 0xeee2: 0xeee2, + 0xeee3: 0xeee3, + 0xeee4: 0xeee4, + 0xeee5: 0xeee5, + 0xeee6: 0xeee6, + 0xeee7: 0xeee7, + 0xeee8: 0xeee8, + 0xeee9: 0xeee9, + 0xeeea: 0xeeea, + 0xeeeb: 0xeeeb, + 0xeeec: 0xeeec, + 0xeeed: 0xeeed, + 0xeeee: 0xeeee, + 0xeeef: 0xeeef, + 0xeef0: 0xeef0, + 0xeef1: 0xeef1, + 0xeef2: 0xeef2, + 0xeef3: 0xeef3, + 0xeef4: 0xeef4, + 0xeef5: 0xeef5, + 0xeef6: 0xeef6, + 0xeef7: 0xeef7, + 0xeef8: 0xeef8, + 0xeef9: 0xeef9, + 0xeefa: 0xeefa, + 0xeefb: 0xeefb, + 0xeefc: 0xeefc, + 0xeefd: 0xeefd, + 0xeefe: 0xeefe, + 0xeeff: 0xeeff, + 0xef00: 0xef00, + 0xef01: 0xef01, + 0xef02: 0xef02, + 0xef03: 0xef03, + 0xef04: 0xef04, + 0xef05: 0xef05, + 0xef06: 0xef06, + 0xef07: 0xef07, + 0xef08: 0xef08, + 0xef09: 0xef09, + 0xef0a: 0xef0a, + 0xef0b: 0xef0b, + 0xef0c: 0xef0c, + 0xef0d: 0xef0d, + 0xef0e: 0xef0e, + 0xef0f: 0xef0f, + 0xef10: 0xef10, + 0xef11: 0xef11, + 0xef12: 0xef12, + 0xef13: 0xef13, + 0xef14: 0xef14, + 0xef15: 0xef15, + 0xef16: 0xef16, + 0xef17: 0xef17, + 0xef18: 0xef18, + 0xef19: 0xef19, + 0xef1a: 0xef1a, + 0xef1b: 0xef1b, + 0xef1c: 0xef1c, + 0xef1d: 0xef1d, + 0xef1e: 0xef1e, + 0xef1f: 0xef1f, + 0xef20: 0xef20, + 0xef21: 0xef21, + 0xef22: 0xef22, + 0xef23: 0xef23, + 0xef24: 0xef24, + 0xef25: 0xef25, + 0xef26: 0xef26, + 0xef27: 0xef27, + 0xef28: 0xef28, + 0xef29: 0xef29, + 0xef2a: 0xef2a, + 0xef2b: 0xef2b, + 0xef2c: 0xef2c, + 0xef2d: 0xef2d, + 0xef2e: 0xef2e, + 0xef2f: 0xef2f, + 0xef30: 0xef30, + 0xef31: 0xef31, + 0xef32: 0xef32, + 0xef33: 0xef33, + 0xef34: 0xef34, + 0xef35: 0xef35, + 0xef36: 0xef36, + 0xef37: 0xef37, + 0xef38: 0xef38, + 0xef39: 0xef39, + 0xef3a: 0xef3a, + 0xef3b: 0xef3b, + 0xef3c: 0xef3c, + 0xef3d: 0xef3d, + 0xef3e: 0xef3e, + 0xef3f: 0xef3f, + 0xef40: 0xef40, + 0xef41: 0xef41, + 0xef42: 0xef42, + 0xef43: 0xef43, + 0xef44: 0xef44, + 0xef45: 0xef45, + 0xef46: 0xef46, + 0xef47: 0xef47, + 0xef48: 0xef48, + 0xef49: 0xef49, + 0xef4a: 0xef4a, + 0xef4b: 0xef4b, + 0xef4c: 0xef4c, + 0xef4d: 0xef4d, + 0xef4e: 0xef4e, + 0xef4f: 0xef4f, + 0xef50: 0xef50, + 0xef51: 0xef51, + 0xef52: 0xef52, + 0xef53: 0xef53, + 0xef54: 0xef54, + 0xef55: 0xef55, + 0xef56: 0xef56, + 0xef57: 0xef57, + 0xef58: 0xef58, + 0xef59: 0xef59, + 0xef5a: 0xef5a, + 0xef5b: 0xef5b, + 0xef5c: 0xef5c, + 0xef5d: 0xef5d, + 0xef5e: 0xef5e, + 0xef5f: 0xef5f, + 0xef60: 0xef60, + 0xef61: 0xef61, + 0xef62: 0xef62, + 0xef63: 0xef63, + 0xef64: 0xef64, + 0xef65: 0xef65, + 0xef66: 0xef66, + 0xef67: 0xef67, + 0xef68: 0xef68, + 0xef69: 0xef69, + 0xef6a: 0xef6a, + 0xef6b: 0xef6b, + 0xef6c: 0xef6c, + 0xef6d: 0xef6d, + 0xef6e: 0xef6e, + 0xef6f: 0xef6f, + 0xef70: 0xef70, + 0xef71: 0xef71, + 0xef72: 0xef72, + 0xef73: 0xef73, + 0xef74: 0xef74, + 0xef75: 0xef75, + 0xef76: 0xef76, + 0xef77: 0xef77, + 0xef78: 0xef78, + 0xef79: 0xef79, + 0xef7a: 0xef7a, + 0xef7b: 0xef7b, + 0xef7c: 0xef7c, + 0xef7d: 0xef7d, + 0xef7e: 0xef7e, + 0xef7f: 0xef7f, + 0xef80: 0xef80, + 0xef81: 0xef81, + 0xef82: 0xef82, + 0xef83: 0xef83, + 0xef84: 0xef84, + 0xef85: 0xef85, + 0xef86: 0xef86, + 0xef87: 0xef87, + 0xef88: 0xef88, + 0xef89: 0xef89, + 0xef8a: 0xef8a, + 0xef8b: 0xef8b, + 0xef8c: 0xef8c, + 0xef8d: 0xef8d, + 0xef8e: 0xef8e, + 0xef8f: 0xef8f, + 0xef90: 0xef90, + 0xef91: 0xef91, + 0xef92: 0xef92, + 0xef93: 0xef93, + 0xef94: 0xef94, + 0xef95: 0xef95, + 0xef96: 0xef96, + 0xef97: 0xef97, + 0xef98: 0xef98, + 0xef99: 0xef99, + 0xef9a: 0xef9a, + 0xef9b: 0xef9b, + 0xef9c: 0xef9c, + 0xef9d: 0xef9d, + 0xef9e: 0xef9e, + 0xef9f: 0xef9f, + 0xefa0: 0xefa0, + 0xefa1: 0xefa1, + 0xefa2: 0xefa2, + 0xefa3: 0xefa3, + 0xefa4: 0xefa4, + 0xefa5: 0xefa5, + 0xefa6: 0xefa6, + 0xefa7: 0xefa7, + 0xefa8: 0xefa8, + 0xefa9: 0xefa9, + 0xefaa: 0xefaa, + 0xefab: 0xefab, + 0xefac: 0xefac, + 0xefad: 0xefad, + 0xefae: 0xefae, + 0xefaf: 0xefaf, + 0xefb0: 0xefb0, + 0xefb1: 0xefb1, + 0xefb2: 0xefb2, + 0xefb3: 0xefb3, + 0xefb4: 0xefb4, + 0xefb5: 0xefb5, + 0xefb6: 0xefb6, + 0xefb7: 0xefb7, + 0xefb8: 0xefb8, + 0xefb9: 0xefb9, + 0xefba: 0xefba, + 0xefbb: 0xefbb, + 0xefbc: 0xefbc, + 0xefbd: 0xefbd, + 0xefbe: 0xefbe, + 0xefbf: 0xefbf, + 0xefc0: 0xefc0, + 0xefc1: 0xefc1, + 0xefc2: 0xefc2, + 0xefc3: 0xefc3, + 0xefc4: 0xefc4, + 0xefc5: 0xefc5, + 0xefc6: 0xefc6, + 0xefc7: 0xefc7, + 0xefc8: 0xefc8, + 0xefc9: 0xefc9, + 0xefca: 0xefca, + 0xefcb: 0xefcb, + 0xefcc: 0xefcc, + 0xefcd: 0xefcd, + 0xefce: 0xefce, + 0xf000: 0xf000, + 0xf001: 0xf001, + 0xf002: 0xf002, + 0xf003: 0xf003, + 0xf004: 0xf004, + 0xf005: 0xf005, + 0xf006: 0xf006, + 0xf007: 0xf007, + 0xf008: 0xf008, + 0xf009: 0xf009, + 0xf00a: 0xf00a, + 0xf00b: 0xf00b, + 0xf00c: 0xf00c, + 0xf00d: 0xf00d, + 0xf00e: 0xf00e, + 0xf00f: 0xf00f, + 0xf010: 0xf010, + 0xf011: 0xf011, + 0xf012: 0xf012, + 0xf013: 0xf013, + 0xf014: 0xf014, + 0xf015: 0xf015, + 0xf016: 0xf016, + 0xf017: 0xf017, + 0xf018: 0xf018, + 0xf019: 0xf019, + 0xf01a: 0xf01a, + 0xf01b: 0xf01b, + 0xf01c: 0xf01c, + 0xf01d: 0xf01d, + 0xf01e: 0xf01e, + 0xf01f: 0xf01f, + 0xf020: 0xf020, + 0xf021: 0xf021, + 0xf022: 0xf022, + 0xf023: 0xf023, + 0xf024: 0xf024, + 0xf025: 0xf025, + 0xf026: 0xf026, + 0xf027: 0xf027, + 0xf028: 0xf028, + 0xf029: 0xf029, + 0xf02a: 0xf02a, + 0xf02b: 0xf02b, + 0xf02c: 0xf02c, + 0xf02d: 0xf02d, + 0xf02e: 0xf02e, + 0xf02f: 0xf02f, + 0xf030: 0xf030, + 0xf031: 0xf031, + 0xf032: 0xf032, + 0xf033: 0xf033, + 0xf034: 0xf034, + 0xf035: 0xf035, + 0xf036: 0xf036, + 0xf037: 0xf037, + 0xf038: 0xf038, + 0xf039: 0xf039, + 0xf03a: 0xf03a, + 0xf03b: 0xf03b, + 0xf03c: 0xf03c, + 0xf03d: 0xf03d, + 0xf03e: 0xf03e, + 0xf03f: 0xf03f, + 0xf040: 0xf040, + 0xf041: 0xf041, + 0xf042: 0xf042, + 0xf043: 0xf043, + 0xf044: 0xf044, + 0xf045: 0xf045, + 0xf046: 0xf046, + 0xf047: 0xf047, + 0xf048: 0xf048, + 0xf049: 0xf049, + 0xf04a: 0xf04a, + 0xf04b: 0xf04b, + 0xf04c: 0xf04c, + 0xf04d: 0xf04d, + 0xf04e: 0xf04e, + 0xf04f: 0xf04f, + 0xf050: 0xf050, + 0xf051: 0xf051, + 0xf052: 0xf052, + 0xf053: 0xf053, + 0xf054: 0xf054, + 0xf055: 0xf055, + 0xf056: 0xf056, + 0xf057: 0xf057, + 0xf058: 0xf058, + 0xf059: 0xf059, + 0xf05a: 0xf05a, + 0xf05b: 0xf05b, + 0xf05c: 0xf05c, + 0xf05d: 0xf05d, + 0xf05e: 0xf05e, + 0xf05f: 0xf05f, + 0xf060: 0xf060, + 0xf061: 0xf061, + 0xf062: 0xf062, + 0xf063: 0xf063, + 0xf064: 0xf064, + 0xf065: 0xf065, + 0xf066: 0xf066, + 0xf067: 0xf067, + 0xf068: 0xf068, + 0xf069: 0xf069, + 0xf06a: 0xf06a, + 0xf06b: 0xf06b, + 0xf06c: 0xf06c, + 0xf06d: 0xf06d, + 0xf06e: 0xf06e, + 0xf06f: 0xf06f, + 0xf070: 0xf070, + 0xf071: 0xf071, + 0xf072: 0xf072, + 0xf073: 0xf073, + 0xf074: 0xf074, + 0xf075: 0xf075, + 0xf076: 0xf076, + 0xf077: 0xf077, + 0xf078: 0xf078, + 0xf079: 0xf079, + 0xf07a: 0xf07a, + 0xf07b: 0xf07b, + 0xf07c: 0xf07c, + 0xf07d: 0xf07d, + 0xf07e: 0xf07e, + 0xf07f: 0xf07f, + 0xf080: 0xf080, + 0xf081: 0xf081, + 0xf082: 0xf082, + 0xf083: 0xf083, + 0xf084: 0xf084, + 0xf085: 0xf085, + 0xf086: 0xf086, + 0xf087: 0xf087, + 0xf088: 0xf088, + 0xf089: 0xf089, + 0xf08a: 0xf08a, + 0xf08b: 0xf08b, + 0xf08c: 0xf08c, + 0xf08d: 0xf08d, + 0xf08e: 0xf08e, + 0xf08f: 0xf08f, + 0xf090: 0xf090, + 0xf091: 0xf091, + 0xf092: 0xf092, + 0xf093: 0xf093, + 0xf094: 0xf094, + 0xf095: 0xf095, + 0xf096: 0xf096, + 0xf097: 0xf097, + 0xf098: 0xf098, + 0xf099: 0xf099, + 0xf09a: 0xf09a, + 0xf09b: 0xf09b, + 0xf09c: 0xf09c, + 0xf09d: 0xf09d, + 0xf09e: 0xf09e, + 0xf09f: 0xf09f, + 0xf0a0: 0xf0a0, + 0xf0a1: 0xf0a1, + 0xf0a2: 0xf0a2, + 0xf0a3: 0xf0a3, + 0xf0a4: 0xf0a4, + 0xf0a5: 0xf0a5, + 0xf0a6: 0xf0a6, + 0xf0a7: 0xf0a7, + 0xf0a8: 0xf0a8, + 0xf0a9: 0xf0a9, + 0xf0aa: 0xf0aa, + 0xf0ab: 0xf0ab, + 0xf0ac: 0xf0ac, + 0xf0ad: 0xf0ad, + 0xf0ae: 0xf0ae, + 0xf0af: 0xf0af, + 0xf0b0: 0xf0b0, + 0xf0b1: 0xf0b1, + 0xf0b2: 0xf0b2, + 0xf0b3: 0xf0b3, + 0xf0b4: 0xf0b4, + 0xf0b5: 0xf0b5, + 0xf0b6: 0xf0b6, + 0xf0b7: 0xf0b7, + 0xf0b8: 0xf0b8, + 0xf0b9: 0xf0b9, + 0xf0ba: 0xf0ba, + 0xf0bb: 0xf0bb, + 0xf0bc: 0xf0bc, + 0xf0bd: 0xf0bd, + 0xf0be: 0xf0be, + 0xf0bf: 0xf0bf, + 0xf0c0: 0xf0c0, + 0xf0c1: 0xf0c1, + 0xf0c2: 0xf0c2, + 0xf0c3: 0xf0c3, + 0xf0c4: 0xf0c4, + 0xf0c5: 0xf0c5, + 0xf0c6: 0xf0c6, + 0xf0c7: 0xf0c7, + 0xf0c8: 0xf0c8, + 0xf0c9: 0xf0c9, + 0xf0ca: 0xf0ca, + 0xf0cb: 0xf0cb, + 0xf0cc: 0xf0cc, + 0xf0cd: 0xf0cd, + 0xf0ce: 0xf0ce, + 0xf0cf: 0xf0cf, + 0xf0d0: 0xf0d0, + 0xf0d1: 0xf0d1, + 0xf0d2: 0xf0d2, + 0xf0d3: 0xf0d3, + 0xf0d4: 0xf0d4, + 0xf0d5: 0xf0d5, + 0xf0d6: 0xf0d6, + 0xf0d7: 0xf0d7, + 0xf0d8: 0xf0d8, + 0xf0d9: 0xf0d9, + 0xf0da: 0xf0da, + 0xf0db: 0xf0db, + 0xf0dc: 0xf0dc, + 0xf0dd: 0xf0dd, + 0xf0de: 0xf0de, + 0xf0df: 0xf0df, + 0xf0e0: 0xf0e0, + 0xf0e1: 0xf0e1, + 0xf0e2: 0xf0e2, + 0xf0e3: 0xf0e3, + 0xf0e4: 0xf0e4, + 0xf0e5: 0xf0e5, + 0xf0e6: 0xf0e6, + 0xf0e7: 0xf0e7, + 0xf0e8: 0xf0e8, + 0xf0e9: 0xf0e9, + 0xf0ea: 0xf0ea, + 0xf0eb: 0xf0eb, + 0xf0ec: 0xf0ec, + 0xf0ed: 0xf0ed, + 0xf0ee: 0xf0ee, + 0xf0ef: 0xf0ef, + 0xf0f0: 0xf0f0, + 0xf0f1: 0xf0f1, + 0xf0f2: 0xf0f2, + 0xf0f3: 0xf0f3, + 0xf0f4: 0xf0f4, + 0xf0f5: 0xf0f5, + 0xf0f6: 0xf0f6, + 0xf0f7: 0xf0f7, + 0xf0f8: 0xf0f8, + 0xf0f9: 0xf0f9, + 0xf0fa: 0xf0fa, + 0xf0fb: 0xf0fb, + 0xf0fc: 0xf0fc, + 0xf0fd: 0xf0fd, + 0xf0fe: 0xf0fe, + 0xf0ff: 0xf0ff, + 0xf100: 0xf100, + 0xf101: 0xf101, + 0xf102: 0xf102, + 0xf103: 0xf103, + 0xf104: 0xf104, + 0xf105: 0xf105, + 0xf106: 0xf106, + 0xf107: 0xf107, + 0xf108: 0xf108, + 0xf109: 0xf109, + 0xf10a: 0xf10a, + 0xf10b: 0xf10b, + 0xf10c: 0xf10c, + 0xf10d: 0xf10d, + 0xf10e: 0xf10e, + 0xf10f: 0xf10f, + 0xf110: 0xf110, + 0xf111: 0xf111, + 0xf112: 0xf112, + 0xf113: 0xf113, + 0xf114: 0xf114, + 0xf115: 0xf115, + 0xf116: 0xf116, + 0xf117: 0xf117, + 0xf118: 0xf118, + 0xf119: 0xf119, + 0xf11a: 0xf11a, + 0xf11b: 0xf11b, + 0xf11c: 0xf11c, + 0xf11d: 0xf11d, + 0xf11e: 0xf11e, + 0xf11f: 0xf11f, + 0xf120: 0xf120, + 0xf121: 0xf121, + 0xf122: 0xf122, + 0xf123: 0xf123, + 0xf124: 0xf124, + 0xf125: 0xf125, + 0xf126: 0xf126, + 0xf127: 0xf127, + 0xf128: 0xf128, + 0xf129: 0xf129, + 0xf12a: 0xf12a, + 0xf12b: 0xf12b, + 0xf12c: 0xf12c, + 0xf12d: 0xf12d, + 0xf12e: 0xf12e, + 0xf12f: 0xf12f, + 0xf130: 0xf130, + 0xf131: 0xf131, + 0xf132: 0xf132, + 0xf133: 0xf133, + 0xf134: 0xf134, + 0xf135: 0xf135, + 0xf136: 0xf136, + 0xf137: 0xf137, + 0xf138: 0xf138, + 0xf139: 0xf139, + 0xf13a: 0xf13a, + 0xf13b: 0xf13b, + 0xf13c: 0xf13c, + 0xf13d: 0xf13d, + 0xf13e: 0xf13e, + 0xf13f: 0xf13f, + 0xf140: 0xf140, + 0xf141: 0xf141, + 0xf142: 0xf142, + 0xf143: 0xf143, + 0xf144: 0xf144, + 0xf145: 0xf145, + 0xf146: 0xf146, + 0xf147: 0xf147, + 0xf148: 0xf148, + 0xf149: 0xf149, + 0xf14a: 0xf14a, + 0xf14b: 0xf14b, + 0xf14c: 0xf14c, + 0xf14d: 0xf14d, + 0xf14e: 0xf14e, + 0xf14f: 0xf14f, + 0xf150: 0xf150, + 0xf151: 0xf151, + 0xf152: 0xf152, + 0xf153: 0xf153, + 0xf154: 0xf154, + 0xf155: 0xf155, + 0xf156: 0xf156, + 0xf157: 0xf157, + 0xf158: 0xf158, + 0xf159: 0xf159, + 0xf15a: 0xf15a, + 0xf15b: 0xf15b, + 0xf15c: 0xf15c, + 0xf15d: 0xf15d, + 0xf15e: 0xf15e, + 0xf15f: 0xf15f, + 0xf160: 0xf160, + 0xf161: 0xf161, + 0xf162: 0xf162, + 0xf163: 0xf163, + 0xf164: 0xf164, + 0xf165: 0xf165, + 0xf166: 0xf166, + 0xf167: 0xf167, + 0xf168: 0xf168, + 0xf169: 0xf169, + 0xf16a: 0xf16a, + 0xf16b: 0xf16b, + 0xf16c: 0xf16c, + 0xf16d: 0xf16d, + 0xf16e: 0xf16e, + 0xf16f: 0xf16f, + 0xf170: 0xf170, + 0xf171: 0xf171, + 0xf172: 0xf172, + 0xf173: 0xf173, + 0xf174: 0xf174, + 0xf175: 0xf175, + 0xf176: 0xf176, + 0xf177: 0xf177, + 0xf178: 0xf178, + 0xf179: 0xf179, + 0xf17a: 0xf17a, + 0xf17b: 0xf17b, + 0xf17c: 0xf17c, + 0xf17d: 0xf17d, + 0xf17e: 0xf17e, + 0xf17f: 0xf17f, + 0xf180: 0xf180, + 0xf181: 0xf181, + 0xf182: 0xf182, + 0xf183: 0xf183, + 0xf184: 0xf184, + 0xf185: 0xf185, + 0xf186: 0xf186, + 0xf187: 0xf187, + 0xf188: 0xf188, + 0xf189: 0xf189, + 0xf18a: 0xf18a, + 0xf18b: 0xf18b, + 0xf18c: 0xf18c, + 0xf18d: 0xf18d, + 0xf18e: 0xf18e, + 0xf18f: 0xf18f, + 0xf190: 0xf190, + 0xf191: 0xf191, + 0xf192: 0xf192, + 0xf193: 0xf193, + 0xf194: 0xf194, + 0xf195: 0xf195, + 0xf196: 0xf196, + 0xf197: 0xf197, + 0xf198: 0xf198, + 0xf199: 0xf199, + 0xf19a: 0xf19a, + 0xf19b: 0xf19b, + 0xf19c: 0xf19c, + 0xf19d: 0xf19d, + 0xf19e: 0xf19e, + 0xf19f: 0xf19f, + 0xf1a0: 0xf1a0, + 0xf1a1: 0xf1a1, + 0xf1a2: 0xf1a2, + 0xf1a3: 0xf1a3, + 0xf1a4: 0xf1a4, + 0xf1a5: 0xf1a5, + 0xf1a6: 0xf1a6, + 0xf1a7: 0xf1a7, + 0xf1a8: 0xf1a8, + 0xf1a9: 0xf1a9, + 0xf1aa: 0xf1aa, + 0xf1ab: 0xf1ab, + 0xf1ac: 0xf1ac, + 0xf1ad: 0xf1ad, + 0xf1ae: 0xf1ae, + 0xf1af: 0xf1af, + 0xf1b0: 0xf1b0, + 0xf1b1: 0xf1b1, + 0xf1b2: 0xf1b2, + 0xf1b3: 0xf1b3, + 0xf1b4: 0xf1b4, + 0xf1b5: 0xf1b5, + 0xf1b6: 0xf1b6, + 0xf1b7: 0xf1b7, + 0xf1b8: 0xf1b8, + 0xf1b9: 0xf1b9, + 0xf1ba: 0xf1ba, + 0xf1bb: 0xf1bb, + 0xf1bc: 0xf1bc, + 0xf1bd: 0xf1bd, + 0xf1be: 0xf1be, + 0xf1bf: 0xf1bf, + 0xf1c0: 0xf1c0, + 0xf1c1: 0xf1c1, + 0xf1c2: 0xf1c2, + 0xf1c3: 0xf1c3, + 0xf1c4: 0xf1c4, + 0xf1c5: 0xf1c5, + 0xf1c6: 0xf1c6, + 0xf1c7: 0xf1c7, + 0xf1c8: 0xf1c8, + 0xf1c9: 0xf1c9, + 0xf1ca: 0xf1ca, + 0xf1cb: 0xf1cb, + 0xf1cc: 0xf1cc, + 0xf1cd: 0xf1cd, + 0xf1ce: 0xf1ce, + 0xf1cf: 0xf1cf, + 0xf1d0: 0xf1d0, + 0xf1d1: 0xf1d1, + 0xf1d2: 0xf1d2, + 0xf1d3: 0xf1d3, + 0xf1d4: 0xf1d4, + 0xf1d5: 0xf1d5, + 0xf1d6: 0xf1d6, + 0xf1d7: 0xf1d7, + 0xf1d8: 0xf1d8, + 0xf1d9: 0xf1d9, + 0xf1da: 0xf1da, + 0xf1db: 0xf1db, + 0xf1dc: 0xf1dc, + 0xf1dd: 0xf1dd, + 0xf1de: 0xf1de, + 0xf1df: 0xf1df, + 0xf1e0: 0xf1e0, + 0xf1e1: 0xf1e1, + 0xf1e2: 0xf1e2, + 0xf1e3: 0xf1e3, + 0xf1e4: 0xf1e4, + 0xf1e5: 0xf1e5, + 0xf1e6: 0xf1e6, + 0xf1e7: 0xf1e7, + 0xf1e8: 0xf1e8, + 0xf1e9: 0xf1e9, + 0xf1ea: 0xf1ea, + 0xf1eb: 0xf1eb, + 0xf1ec: 0xf1ec, + 0xf1ed: 0xf1ed, + 0xf1ee: 0xf1ee, + 0xf1ef: 0xf1ef, + 0xf1f0: 0xf1f0, + 0xf1f1: 0xf1f1, + 0xf1f2: 0xf1f2, + 0xf1f3: 0xf1f3, + 0xf1f4: 0xf1f4, + 0xf1f5: 0xf1f5, + 0xf1f6: 0xf1f6, + 0xf1f7: 0xf1f7, + 0xf1f8: 0xf1f8, + 0xf1f9: 0xf1f9, + 0xf1fa: 0xf1fa, + 0xf1fb: 0xf1fb, + 0xf1fc: 0xf1fc, + 0xf1fd: 0xf1fd, + 0xf1fe: 0xf1fe, + 0xf1ff: 0xf1ff, + 0xf200: 0xf200, + 0xf201: 0xf201, + 0xf202: 0xf202, + 0xf203: 0xf203, + 0xf204: 0xf204, + 0xf205: 0xf205, + 0xf206: 0xf206, + 0xf207: 0xf207, + 0xf208: 0xf208, + 0xf209: 0xf209, + 0xf20a: 0xf20a, + 0xf20b: 0xf20b, + 0xf20c: 0xf20c, + 0xf20d: 0xf20d, + 0xf20e: 0xf20e, + 0xf20f: 0xf20f, + 0xf210: 0xf210, + 0xf211: 0xf211, + 0xf212: 0xf212, + 0xf213: 0xf213, + 0xf214: 0xf214, + 0xf215: 0xf215, + 0xf216: 0xf216, + 0xf217: 0xf217, + 0xf218: 0xf218, + 0xf219: 0xf219, + 0xf21a: 0xf21a, + 0xf21b: 0xf21b, + 0xf21c: 0xf21c, + 0xf21d: 0xf21d, + 0xf21e: 0xf21e, + 0xf21f: 0xf21f, + 0xf220: 0xf220, + 0xf221: 0xf221, + 0xf222: 0xf222, + 0xf223: 0xf223, + 0xf224: 0xf224, + 0xf225: 0xf225, + 0xf226: 0xf226, + 0xf227: 0xf227, + 0xf228: 0xf228, + 0xf229: 0xf229, + 0xf22a: 0xf22a, + 0xf22b: 0xf22b, + 0xf22c: 0xf22c, + 0xf22d: 0xf22d, + 0xf22e: 0xf22e, + 0xf22f: 0xf22f, + 0xf230: 0xf230, + 0xf231: 0xf231, + 0xf232: 0xf232, + 0xf233: 0xf233, + 0xf234: 0xf234, + 0xf235: 0xf235, + 0xf236: 0xf236, + 0xf237: 0xf237, + 0xf238: 0xf238, + 0xf239: 0xf239, + 0xf23a: 0xf23a, + 0xf23b: 0xf23b, + 0xf23c: 0xf23c, + 0xf23d: 0xf23d, + 0xf23e: 0xf23e, + 0xf23f: 0xf23f, + 0xf240: 0xf240, + 0xf241: 0xf241, + 0xf242: 0xf242, + 0xf243: 0xf243, + 0xf244: 0xf244, + 0xf245: 0xf245, + 0xf246: 0xf246, + 0xf247: 0xf247, + 0xf248: 0xf248, + 0xf249: 0xf249, + 0xf24a: 0xf24a, + 0xf24b: 0xf24b, + 0xf24c: 0xf24c, + 0xf24d: 0xf24d, + 0xf24e: 0xf24e, + 0xf24f: 0xf24f, + 0xf250: 0xf250, + 0xf251: 0xf251, + 0xf252: 0xf252, + 0xf253: 0xf253, + 0xf254: 0xf254, + 0xf255: 0xf255, + 0xf256: 0xf256, + 0xf257: 0xf257, + 0xf258: 0xf258, + 0xf259: 0xf259, + 0xf25a: 0xf25a, + 0xf25b: 0xf25b, + 0xf25c: 0xf25c, + 0xf25d: 0xf25d, + 0xf25e: 0xf25e, + 0xf25f: 0xf25f, + 0xf260: 0xf260, + 0xf261: 0xf261, + 0xf262: 0xf262, + 0xf263: 0xf263, + 0xf264: 0xf264, + 0xf265: 0xf265, + 0xf266: 0xf266, + 0xf267: 0xf267, + 0xf268: 0xf268, + 0xf269: 0xf269, + 0xf26a: 0xf26a, + 0xf26b: 0xf26b, + 0xf26c: 0xf26c, + 0xf26d: 0xf26d, + 0xf26e: 0xf26e, + 0xf26f: 0xf26f, + 0xf270: 0xf270, + 0xf271: 0xf271, + 0xf272: 0xf272, + 0xf273: 0xf273, + 0xf274: 0xf274, + 0xf275: 0xf275, + 0xf276: 0xf276, + 0xf277: 0xf277, + 0xf278: 0xf278, + 0xf279: 0xf279, + 0xf27a: 0xf27a, + 0xf27b: 0xf27b, + 0xf27c: 0xf27c, + 0xf27d: 0xf27d, + 0xf27e: 0xf27e, + 0xf27f: 0xf27f, + 0xf280: 0xf280, + 0xf281: 0xf281, + 0xf282: 0xf282, + 0xf283: 0xf283, + 0xf284: 0xf284, + 0xf285: 0xf285, + 0xf286: 0xf286, + 0xf287: 0xf287, + 0xf288: 0xf288, + 0xf289: 0xf289, + 0xf28a: 0xf28a, + 0xf28b: 0xf28b, + 0xf28c: 0xf28c, + 0xf28d: 0xf28d, + 0xf28e: 0xf28e, + 0xf28f: 0xf28f, + 0xf290: 0xf290, + 0xf291: 0xf291, + 0xf292: 0xf292, + 0xf293: 0xf293, + 0xf294: 0xf294, + 0xf295: 0xf295, + 0xf296: 0xf296, + 0xf297: 0xf297, + 0xf298: 0xf298, + 0xf299: 0xf299, + 0xf29a: 0xf29a, + 0xf29b: 0xf29b, + 0xf29c: 0xf29c, + 0xf29d: 0xf29d, + 0xf29e: 0xf29e, + 0xf29f: 0xf29f, + 0xf2a0: 0xf2a0, + 0xf2a1: 0xf2a1, + 0xf2a2: 0xf2a2, + 0xf2a3: 0xf2a3, + 0xf2a4: 0xf2a4, + 0xf2a5: 0xf2a5, + 0xf2a6: 0xf2a6, + 0xf2a7: 0xf2a7, + 0xf2a8: 0xf2a8, + 0xf2a9: 0xf2a9, + 0xf2aa: 0xf2aa, + 0xf2ab: 0xf2ab, + 0xf2ac: 0xf2ac, + 0xf2ad: 0xf2ad, + 0xf2ae: 0xf2ae, + 0xf2af: 0xf2af, + 0xf2b0: 0xf2b0, + 0xf2b1: 0xf2b1, + 0xf2b2: 0xf2b2, + 0xf2b3: 0xf2b3, + 0xf2b4: 0xf2b4, + 0xf2b5: 0xf2b5, + 0xf2b6: 0xf2b6, + 0xf2b7: 0xf2b7, + 0xf2b8: 0xf2b8, + 0xf2b9: 0xf2b9, + 0xf2ba: 0xf2ba, + 0xf2bb: 0xf2bb, + 0xf2bc: 0xf2bc, + 0xf2bd: 0xf2bd, + 0xf2be: 0xf2be, + 0xf2bf: 0xf2bf, + 0xf2c0: 0xf2c0, + 0xf2c1: 0xf2c1, + 0xf2c2: 0xf2c2, + 0xf2c3: 0xf2c3, + 0xf2c4: 0xf2c4, + 0xf2c5: 0xf2c5, + 0xf2c6: 0xf2c6, + 0xf2c7: 0xf2c7, + 0xf2c8: 0xf2c8, + 0xf2c9: 0xf2c9, + 0xf2ca: 0xf2ca, + 0xf2cb: 0xf2cb, + 0xf2cc: 0xf2cc, + 0xf2cd: 0xf2cd, + 0xf2ce: 0xf2ce, + 0xf2cf: 0xf2cf, + 0xf2d0: 0xf2d0, + 0xf2d1: 0xf2d1, + 0xf2d2: 0xf2d2, + 0xf2d3: 0xf2d3, + 0xf2d4: 0xf2d4, + 0xf2d5: 0xf2d5, + 0xf2d6: 0xf2d6, + 0xf2d7: 0xf2d7, + 0xf2d8: 0xf2d8, + 0xf2d9: 0xf2d9, + 0xf2da: 0xf2da, + 0xf2db: 0xf2db, + 0xf2dc: 0xf2dc, + 0xf2dd: 0xf2dd, + 0xf2de: 0xf2de, + 0xf2df: 0xf2df, + 0xf2e0: 0xf2e0, + 0xf2e1: 0xf2e1, + 0xf2e2: 0xf2e2, + 0xf2e3: 0xf2e3, + 0xf2e4: 0xf2e4, + 0xf2e5: 0xf2e5, + 0xf2e6: 0xf2e6, + 0xf2e7: 0xf2e7, + 0xf2e8: 0xf2e8, + 0xf2e9: 0xf2e9, + 0xf2ea: 0xf2ea, + 0xf2eb: 0xf2eb, + 0xf2ec: 0xf2ec, + 0xf2ed: 0xf2ed, + 0xf2ee: 0xf2ee, + 0xf2ef: 0xf2ef, + 0xf2f0: 0xf2f0, + 0xf2f1: 0xf2f1, + 0xf2f2: 0xf2f2, + 0xf2f3: 0xf2f3, + 0xf2f4: 0xf2f4, + 0xf2f5: 0xf2f5, + 0xf2f6: 0xf2f6, + 0xf2f7: 0xf2f7, + 0xf2f8: 0xf2f8, + 0xf2f9: 0xf2f9, + 0xf2fa: 0xf2fa, + 0xf2fb: 0xf2fb, + 0xf2fc: 0xf2fc, + 0xf2fd: 0xf2fd, + 0xf2fe: 0xf2fe, + 0xf2ff: 0xf2ff, + }, + "Font Awesome Extension": { + 0xe000: 0xe200, + 0xe001: 0xe201, + 0xe002: 0xe202, + 0xe003: 0xe203, + 0xe004: 0xe204, + 0xe005: 0xe205, + 0xe006: 0xe206, + 0xe007: 0xe207, + 0xe008: 0xe208, + 0xe009: 0xe209, + 0xe00a: 0xe20a, + 0xe00b: 0xe20b, + 0xe00c: 0xe20c, + 0xe00d: 0xe20d, + 0xe00e: 0xe20e, + 0xe00f: 0xe20f, + 0xe010: 0xe210, + 0xe011: 0xe211, + 0xe012: 0xe212, + 0xe013: 0xe213, + 0xe014: 0xe214, + 0xe015: 0xe215, + 0xe016: 0xe216, + 0xe017: 0xe217, + 0xe018: 0xe218, + 0xe019: 0xe219, + 0xe01a: 0xe21a, + 0xe01b: 0xe21b, + 0xe01c: 0xe21c, + 0xe01d: 0xe21d, + 0xe01e: 0xe21e, + 0xe01f: 0xe21f, + 0xe020: 0xe220, + 0xe021: 0xe221, + 0xe022: 0xe222, + 0xe023: 0xe223, + 0xe024: 0xe224, + 0xe025: 0xe225, + 0xe026: 0xe226, + 0xe027: 0xe227, + 0xe028: 0xe228, + 0xe029: 0xe229, + 0xe02a: 0xe22a, + 0xe02b: 0xe22b, + 0xe02c: 0xe22c, + 0xe02d: 0xe22d, + 0xe02e: 0xe22e, + 0xe02f: 0xe22f, + 0xe030: 0xe230, + 0xe031: 0xe231, + 0xe032: 0xe232, + 0xe033: 0xe233, + 0xe034: 0xe234, + 0xe035: 0xe235, + 0xe036: 0xe236, + 0xe037: 0xe237, + 0xe038: 0xe238, + 0xe039: 0xe239, + 0xe03a: 0xe23a, + 0xe03b: 0xe23b, + 0xe03c: 0xe23c, + 0xe03d: 0xe23d, + 0xe03e: 0xe23e, + 0xe03f: 0xe23f, + 0xe040: 0xe240, + 0xe041: 0xe241, + 0xe042: 0xe242, + 0xe043: 0xe243, + 0xe044: 0xe244, + 0xe045: 0xe245, + 0xe046: 0xe246, + 0xe047: 0xe247, + 0xe048: 0xe248, + 0xe049: 0xe249, + 0xe04a: 0xe24a, + 0xe04b: 0xe24b, + 0xe04c: 0xe24c, + 0xe04d: 0xe24d, + 0xe04e: 0xe24e, + 0xe04f: 0xe24f, + 0xe050: 0xe250, + 0xe051: 0xe251, + 0xe052: 0xe252, + 0xe053: 0xe253, + 0xe054: 0xe254, + 0xe055: 0xe255, + 0xe056: 0xe256, + 0xe057: 0xe257, + 0xe058: 0xe258, + 0xe059: 0xe259, + 0xe05a: 0xe25a, + 0xe05b: 0xe25b, + 0xe05c: 0xe25c, + 0xe05d: 0xe25d, + 0xe05e: 0xe25e, + 0xe05f: 0xe25f, + 0xe060: 0xe260, + 0xe061: 0xe261, + 0xe062: 0xe262, + 0xe063: 0xe263, + 0xe064: 0xe264, + 0xe065: 0xe265, + 0xe066: 0xe266, + 0xe067: 0xe267, + 0xe068: 0xe268, + 0xe069: 0xe269, + 0xe06a: 0xe26a, + 0xe06b: 0xe26b, + 0xe06c: 0xe26c, + 0xe06d: 0xe26d, + 0xe06e: 0xe26e, + 0xe06f: 0xe26f, + 0xe070: 0xe270, + 0xe071: 0xe271, + 0xe072: 0xe272, + 0xe073: 0xe273, + 0xe074: 0xe274, + 0xe075: 0xe275, + 0xe076: 0xe276, + 0xe077: 0xe277, + 0xe078: 0xe278, + 0xe079: 0xe279, + 0xe07a: 0xe27a, + 0xe07b: 0xe27b, + 0xe07c: 0xe27c, + 0xe07d: 0xe27d, + 0xe07e: 0xe27e, + 0xe07f: 0xe27f, + 0xe080: 0xe280, + 0xe081: 0xe281, + 0xe082: 0xe282, + 0xe083: 0xe283, + 0xe084: 0xe284, + 0xe085: 0xe285, + 0xe086: 0xe286, + 0xe087: 0xe287, + 0xe088: 0xe288, + 0xe089: 0xe289, + 0xe08a: 0xe28a, + 0xe08b: 0xe28b, + 0xe08c: 0xe28c, + 0xe08d: 0xe28d, + 0xe08e: 0xe28e, + 0xe08f: 0xe28f, + 0xe090: 0xe290, + 0xe091: 0xe291, + 0xe092: 0xe292, + 0xe093: 0xe293, + 0xe094: 0xe294, + 0xe095: 0xe295, + 0xe096: 0xe296, + 0xe097: 0xe297, + 0xe098: 0xe298, + 0xe099: 0xe299, + 0xe09a: 0xe29a, + 0xe09b: 0xe29b, + 0xe09c: 0xe29c, + 0xe09d: 0xe29d, + 0xe09e: 0xe29e, + 0xe09f: 0xe29f, + 0xe0a0: 0xe2a0, + 0xe0a1: 0xe2a1, + 0xe0a2: 0xe2a2, + 0xe0a3: 0xe2a3, + 0xe0a4: 0xe2a4, + 0xe0a5: 0xe2a5, + 0xe0a6: 0xe2a6, + 0xe0a7: 0xe2a7, + 0xe0a8: 0xe2a8, + 0xe0a9: 0xe2a9, + }, + "Power Symbols": { + 0x23fb: 0x23fb, + 0x23fc: 0x23fc, + 0x23fd: 0x23fd, + 0x23fe: 0x23fe, + 0x2b58: 0x2b58, + }, + "Material": { + 0xf0001: 0xf0001, + 0xf0002: 0xf0002, + 0xf0003: 0xf0003, + 0xf0004: 0xf0004, + 0xf0005: 0xf0005, + 0xf0006: 0xf0006, + 0xf0007: 0xf0007, + 0xf0008: 0xf0008, + 0xf0009: 0xf0009, + 0xf000a: 0xf000a, + 0xf000b: 0xf000b, + 0xf000c: 0xf000c, + 0xf000d: 0xf000d, + 0xf000e: 0xf000e, + 0xf000f: 0xf000f, + 0xf0010: 0xf0010, + 0xf0011: 0xf0011, + 0xf0012: 0xf0012, + 0xf0013: 0xf0013, + 0xf0014: 0xf0014, + 0xf0015: 0xf0015, + 0xf0016: 0xf0016, + 0xf0017: 0xf0017, + 0xf0018: 0xf0018, + 0xf0019: 0xf0019, + 0xf001a: 0xf001a, + 0xf001b: 0xf001b, + 0xf001c: 0xf001c, + 0xf001d: 0xf001d, + 0xf001e: 0xf001e, + 0xf001f: 0xf001f, + 0xf0020: 0xf0020, + 0xf0021: 0xf0021, + 0xf0022: 0xf0022, + 0xf0023: 0xf0023, + 0xf0024: 0xf0024, + 0xf0025: 0xf0025, + 0xf0026: 0xf0026, + 0xf0027: 0xf0027, + 0xf0028: 0xf0028, + 0xf0029: 0xf0029, + 0xf002a: 0xf002a, + 0xf002b: 0xf002b, + 0xf002c: 0xf002c, + 0xf002d: 0xf002d, + 0xf002e: 0xf002e, + 0xf002f: 0xf002f, + 0xf0030: 0xf0030, + 0xf0031: 0xf0031, + 0xf0032: 0xf0032, + 0xf0033: 0xf0033, + 0xf0034: 0xf0034, + 0xf0035: 0xf0035, + 0xf0036: 0xf0036, + 0xf0037: 0xf0037, + 0xf0038: 0xf0038, + 0xf0039: 0xf0039, + 0xf003a: 0xf003a, + 0xf003b: 0xf003b, + 0xf003c: 0xf003c, + 0xf003d: 0xf003d, + 0xf003e: 0xf003e, + 0xf003f: 0xf003f, + 0xf0040: 0xf0040, + 0xf0041: 0xf0041, + 0xf0042: 0xf0042, + 0xf0043: 0xf0043, + 0xf0044: 0xf0044, + 0xf0045: 0xf0045, + 0xf0046: 0xf0046, + 0xf0047: 0xf0047, + 0xf0048: 0xf0048, + 0xf0049: 0xf0049, + 0xf004a: 0xf004a, + 0xf004b: 0xf004b, + 0xf004c: 0xf004c, + 0xf004d: 0xf004d, + 0xf004e: 0xf004e, + 0xf004f: 0xf004f, + 0xf0050: 0xf0050, + 0xf0051: 0xf0051, + 0xf0052: 0xf0052, + 0xf0053: 0xf0053, + 0xf0054: 0xf0054, + 0xf0055: 0xf0055, + 0xf0056: 0xf0056, + 0xf0057: 0xf0057, + 0xf0058: 0xf0058, + 0xf0059: 0xf0059, + 0xf005a: 0xf005a, + 0xf005b: 0xf005b, + 0xf005c: 0xf005c, + 0xf005d: 0xf005d, + 0xf005e: 0xf005e, + 0xf005f: 0xf005f, + 0xf0060: 0xf0060, + 0xf0061: 0xf0061, + 0xf0062: 0xf0062, + 0xf0063: 0xf0063, + 0xf0064: 0xf0064, + 0xf0065: 0xf0065, + 0xf0066: 0xf0066, + 0xf0067: 0xf0067, + 0xf0068: 0xf0068, + 0xf0069: 0xf0069, + 0xf006a: 0xf006a, + 0xf006b: 0xf006b, + 0xf006c: 0xf006c, + 0xf006d: 0xf006d, + 0xf006e: 0xf006e, + 0xf006f: 0xf006f, + 0xf0070: 0xf0070, + 0xf0071: 0xf0071, + 0xf0072: 0xf0072, + 0xf0073: 0xf0073, + 0xf0074: 0xf0074, + 0xf0075: 0xf0075, + 0xf0076: 0xf0076, + 0xf0077: 0xf0077, + 0xf0078: 0xf0078, + 0xf0079: 0xf0079, + 0xf007a: 0xf007a, + 0xf007b: 0xf007b, + 0xf007c: 0xf007c, + 0xf007d: 0xf007d, + 0xf007e: 0xf007e, + 0xf007f: 0xf007f, + 0xf0080: 0xf0080, + 0xf0081: 0xf0081, + 0xf0082: 0xf0082, + 0xf0083: 0xf0083, + 0xf0084: 0xf0084, + 0xf0085: 0xf0085, + 0xf0086: 0xf0086, + 0xf0087: 0xf0087, + 0xf0088: 0xf0088, + 0xf0089: 0xf0089, + 0xf008a: 0xf008a, + 0xf008b: 0xf008b, + 0xf008c: 0xf008c, + 0xf008d: 0xf008d, + 0xf008e: 0xf008e, + 0xf008f: 0xf008f, + 0xf0090: 0xf0090, + 0xf0091: 0xf0091, + 0xf0092: 0xf0092, + 0xf0093: 0xf0093, + 0xf0094: 0xf0094, + 0xf0095: 0xf0095, + 0xf0096: 0xf0096, + 0xf0097: 0xf0097, + 0xf0098: 0xf0098, + 0xf0099: 0xf0099, + 0xf009a: 0xf009a, + 0xf009b: 0xf009b, + 0xf009c: 0xf009c, + 0xf009d: 0xf009d, + 0xf009e: 0xf009e, + 0xf009f: 0xf009f, + 0xf00a0: 0xf00a0, + 0xf00a1: 0xf00a1, + 0xf00a2: 0xf00a2, + 0xf00a3: 0xf00a3, + 0xf00a4: 0xf00a4, + 0xf00a5: 0xf00a5, + 0xf00a6: 0xf00a6, + 0xf00a7: 0xf00a7, + 0xf00a8: 0xf00a8, + 0xf00a9: 0xf00a9, + 0xf00aa: 0xf00aa, + 0xf00ab: 0xf00ab, + 0xf00ac: 0xf00ac, + 0xf00ad: 0xf00ad, + 0xf00ae: 0xf00ae, + 0xf00af: 0xf00af, + 0xf00b0: 0xf00b0, + 0xf00b1: 0xf00b1, + 0xf00b2: 0xf00b2, + 0xf00b3: 0xf00b3, + 0xf00b4: 0xf00b4, + 0xf00b5: 0xf00b5, + 0xf00b6: 0xf00b6, + 0xf00b7: 0xf00b7, + 0xf00b8: 0xf00b8, + 0xf00b9: 0xf00b9, + 0xf00ba: 0xf00ba, + 0xf00bb: 0xf00bb, + 0xf00bc: 0xf00bc, + 0xf00bd: 0xf00bd, + 0xf00be: 0xf00be, + 0xf00bf: 0xf00bf, + 0xf00c0: 0xf00c0, + 0xf00c1: 0xf00c1, + 0xf00c2: 0xf00c2, + 0xf00c3: 0xf00c3, + 0xf00c4: 0xf00c4, + 0xf00c5: 0xf00c5, + 0xf00c6: 0xf00c6, + 0xf00c7: 0xf00c7, + 0xf00c8: 0xf00c8, + 0xf00c9: 0xf00c9, + 0xf00ca: 0xf00ca, + 0xf00cb: 0xf00cb, + 0xf00cc: 0xf00cc, + 0xf00cd: 0xf00cd, + 0xf00ce: 0xf00ce, + 0xf00cf: 0xf00cf, + 0xf00d0: 0xf00d0, + 0xf00d1: 0xf00d1, + 0xf00d2: 0xf00d2, + 0xf00d3: 0xf00d3, + 0xf00d4: 0xf00d4, + 0xf00d5: 0xf00d5, + 0xf00d6: 0xf00d6, + 0xf00d7: 0xf00d7, + 0xf00d8: 0xf00d8, + 0xf00d9: 0xf00d9, + 0xf00da: 0xf00da, + 0xf00db: 0xf00db, + 0xf00dc: 0xf00dc, + 0xf00dd: 0xf00dd, + 0xf00de: 0xf00de, + 0xf00df: 0xf00df, + 0xf00e0: 0xf00e0, + 0xf00e1: 0xf00e1, + 0xf00e2: 0xf00e2, + 0xf00e3: 0xf00e3, + 0xf00e4: 0xf00e4, + 0xf00e5: 0xf00e5, + 0xf00e6: 0xf00e6, + 0xf00e7: 0xf00e7, + 0xf00e8: 0xf00e8, + 0xf00e9: 0xf00e9, + 0xf00ea: 0xf00ea, + 0xf00eb: 0xf00eb, + 0xf00ec: 0xf00ec, + 0xf00ed: 0xf00ed, + 0xf00ee: 0xf00ee, + 0xf00ef: 0xf00ef, + 0xf00f0: 0xf00f0, + 0xf00f1: 0xf00f1, + 0xf00f2: 0xf00f2, + 0xf00f3: 0xf00f3, + 0xf00f4: 0xf00f4, + 0xf00f5: 0xf00f5, + 0xf00f6: 0xf00f6, + 0xf00f7: 0xf00f7, + 0xf00f8: 0xf00f8, + 0xf00f9: 0xf00f9, + 0xf00fa: 0xf00fa, + 0xf00fb: 0xf00fb, + 0xf00fc: 0xf00fc, + 0xf00fd: 0xf00fd, + 0xf00fe: 0xf00fe, + 0xf00ff: 0xf00ff, + 0xf0100: 0xf0100, + 0xf0101: 0xf0101, + 0xf0102: 0xf0102, + 0xf0103: 0xf0103, + 0xf0104: 0xf0104, + 0xf0105: 0xf0105, + 0xf0106: 0xf0106, + 0xf0107: 0xf0107, + 0xf0108: 0xf0108, + 0xf0109: 0xf0109, + 0xf010a: 0xf010a, + 0xf010b: 0xf010b, + 0xf010c: 0xf010c, + 0xf010d: 0xf010d, + 0xf010e: 0xf010e, + 0xf010f: 0xf010f, + 0xf0110: 0xf0110, + 0xf0111: 0xf0111, + 0xf0112: 0xf0112, + 0xf0113: 0xf0113, + 0xf0114: 0xf0114, + 0xf0115: 0xf0115, + 0xf0116: 0xf0116, + 0xf0117: 0xf0117, + 0xf0118: 0xf0118, + 0xf0119: 0xf0119, + 0xf011a: 0xf011a, + 0xf011b: 0xf011b, + 0xf011c: 0xf011c, + 0xf011d: 0xf011d, + 0xf011e: 0xf011e, + 0xf011f: 0xf011f, + 0xf0120: 0xf0120, + 0xf0121: 0xf0121, + 0xf0122: 0xf0122, + 0xf0123: 0xf0123, + 0xf0124: 0xf0124, + 0xf0125: 0xf0125, + 0xf0126: 0xf0126, + 0xf0127: 0xf0127, + 0xf0128: 0xf0128, + 0xf0129: 0xf0129, + 0xf012a: 0xf012a, + 0xf012b: 0xf012b, + 0xf012c: 0xf012c, + 0xf012d: 0xf012d, + 0xf012e: 0xf012e, + 0xf012f: 0xf012f, + 0xf0130: 0xf0130, + 0xf0131: 0xf0131, + 0xf0132: 0xf0132, + 0xf0133: 0xf0133, + 0xf0134: 0xf0134, + 0xf0135: 0xf0135, + 0xf0136: 0xf0136, + 0xf0137: 0xf0137, + 0xf0138: 0xf0138, + 0xf0139: 0xf0139, + 0xf013a: 0xf013a, + 0xf013b: 0xf013b, + 0xf013c: 0xf013c, + 0xf013d: 0xf013d, + 0xf013e: 0xf013e, + 0xf013f: 0xf013f, + 0xf0140: 0xf0140, + 0xf0141: 0xf0141, + 0xf0142: 0xf0142, + 0xf0143: 0xf0143, + 0xf0144: 0xf0144, + 0xf0145: 0xf0145, + 0xf0146: 0xf0146, + 0xf0147: 0xf0147, + 0xf0148: 0xf0148, + 0xf0149: 0xf0149, + 0xf014a: 0xf014a, + 0xf014b: 0xf014b, + 0xf014c: 0xf014c, + 0xf014d: 0xf014d, + 0xf014e: 0xf014e, + 0xf014f: 0xf014f, + 0xf0150: 0xf0150, + 0xf0151: 0xf0151, + 0xf0152: 0xf0152, + 0xf0153: 0xf0153, + 0xf0154: 0xf0154, + 0xf0155: 0xf0155, + 0xf0156: 0xf0156, + 0xf0157: 0xf0157, + 0xf0158: 0xf0158, + 0xf0159: 0xf0159, + 0xf015a: 0xf015a, + 0xf015b: 0xf015b, + 0xf015c: 0xf015c, + 0xf015d: 0xf015d, + 0xf015e: 0xf015e, + 0xf015f: 0xf015f, + 0xf0160: 0xf0160, + 0xf0161: 0xf0161, + 0xf0162: 0xf0162, + 0xf0163: 0xf0163, + 0xf0164: 0xf0164, + 0xf0165: 0xf0165, + 0xf0166: 0xf0166, + 0xf0167: 0xf0167, + 0xf0168: 0xf0168, + 0xf0169: 0xf0169, + 0xf016a: 0xf016a, + 0xf016b: 0xf016b, + 0xf016c: 0xf016c, + 0xf016d: 0xf016d, + 0xf016e: 0xf016e, + 0xf016f: 0xf016f, + 0xf0170: 0xf0170, + 0xf0171: 0xf0171, + 0xf0172: 0xf0172, + 0xf0173: 0xf0173, + 0xf0174: 0xf0174, + 0xf0175: 0xf0175, + 0xf0176: 0xf0176, + 0xf0177: 0xf0177, + 0xf0178: 0xf0178, + 0xf0179: 0xf0179, + 0xf017a: 0xf017a, + 0xf017b: 0xf017b, + 0xf017c: 0xf017c, + 0xf017d: 0xf017d, + 0xf017e: 0xf017e, + 0xf017f: 0xf017f, + 0xf0180: 0xf0180, + 0xf0181: 0xf0181, + 0xf0182: 0xf0182, + 0xf0183: 0xf0183, + 0xf0184: 0xf0184, + 0xf0185: 0xf0185, + 0xf0186: 0xf0186, + 0xf0187: 0xf0187, + 0xf0188: 0xf0188, + 0xf0189: 0xf0189, + 0xf018a: 0xf018a, + 0xf018b: 0xf018b, + 0xf018c: 0xf018c, + 0xf018d: 0xf018d, + 0xf018e: 0xf018e, + 0xf018f: 0xf018f, + 0xf0190: 0xf0190, + 0xf0191: 0xf0191, + 0xf0192: 0xf0192, + 0xf0193: 0xf0193, + 0xf0194: 0xf0194, + 0xf0195: 0xf0195, + 0xf0196: 0xf0196, + 0xf0197: 0xf0197, + 0xf0198: 0xf0198, + 0xf0199: 0xf0199, + 0xf019a: 0xf019a, + 0xf019b: 0xf019b, + 0xf019c: 0xf019c, + 0xf019d: 0xf019d, + 0xf019e: 0xf019e, + 0xf019f: 0xf019f, + 0xf01a0: 0xf01a0, + 0xf01a1: 0xf01a1, + 0xf01a2: 0xf01a2, + 0xf01a3: 0xf01a3, + 0xf01a4: 0xf01a4, + 0xf01a5: 0xf01a5, + 0xf01a6: 0xf01a6, + 0xf01a7: 0xf01a7, + 0xf01a8: 0xf01a8, + 0xf01a9: 0xf01a9, + 0xf01aa: 0xf01aa, + 0xf01ab: 0xf01ab, + 0xf01ac: 0xf01ac, + 0xf01ad: 0xf01ad, + 0xf01ae: 0xf01ae, + 0xf01af: 0xf01af, + 0xf01b0: 0xf01b0, + 0xf01b1: 0xf01b1, + 0xf01b2: 0xf01b2, + 0xf01b3: 0xf01b3, + 0xf01b4: 0xf01b4, + 0xf01b5: 0xf01b5, + 0xf01b6: 0xf01b6, + 0xf01b7: 0xf01b7, + 0xf01b8: 0xf01b8, + 0xf01b9: 0xf01b9, + 0xf01ba: 0xf01ba, + 0xf01bb: 0xf01bb, + 0xf01bc: 0xf01bc, + 0xf01bd: 0xf01bd, + 0xf01be: 0xf01be, + 0xf01bf: 0xf01bf, + 0xf01c0: 0xf01c0, + 0xf01c1: 0xf01c1, + 0xf01c2: 0xf01c2, + 0xf01c3: 0xf01c3, + 0xf01c4: 0xf01c4, + 0xf01c5: 0xf01c5, + 0xf01c6: 0xf01c6, + 0xf01c7: 0xf01c7, + 0xf01c8: 0xf01c8, + 0xf01c9: 0xf01c9, + 0xf01ca: 0xf01ca, + 0xf01cb: 0xf01cb, + 0xf01cc: 0xf01cc, + 0xf01cd: 0xf01cd, + 0xf01ce: 0xf01ce, + 0xf01cf: 0xf01cf, + 0xf01d0: 0xf01d0, + 0xf01d1: 0xf01d1, + 0xf01d2: 0xf01d2, + 0xf01d3: 0xf01d3, + 0xf01d4: 0xf01d4, + 0xf01d5: 0xf01d5, + 0xf01d6: 0xf01d6, + 0xf01d7: 0xf01d7, + 0xf01d8: 0xf01d8, + 0xf01d9: 0xf01d9, + 0xf01da: 0xf01da, + 0xf01db: 0xf01db, + 0xf01dc: 0xf01dc, + 0xf01dd: 0xf01dd, + 0xf01de: 0xf01de, + 0xf01df: 0xf01df, + 0xf01e0: 0xf01e0, + 0xf01e1: 0xf01e1, + 0xf01e2: 0xf01e2, + 0xf01e3: 0xf01e3, + 0xf01e4: 0xf01e4, + 0xf01e5: 0xf01e5, + 0xf01e6: 0xf01e6, + 0xf01e7: 0xf01e7, + 0xf01e8: 0xf01e8, + 0xf01e9: 0xf01e9, + 0xf01ea: 0xf01ea, + 0xf01eb: 0xf01eb, + 0xf01ec: 0xf01ec, + 0xf01ed: 0xf01ed, + 0xf01ee: 0xf01ee, + 0xf01ef: 0xf01ef, + 0xf01f0: 0xf01f0, + 0xf01f1: 0xf01f1, + 0xf01f2: 0xf01f2, + 0xf01f3: 0xf01f3, + 0xf01f4: 0xf01f4, + 0xf01f5: 0xf01f5, + 0xf01f6: 0xf01f6, + 0xf01f7: 0xf01f7, + 0xf01f8: 0xf01f8, + 0xf01f9: 0xf01f9, + 0xf01fa: 0xf01fa, + 0xf01fb: 0xf01fb, + 0xf01fc: 0xf01fc, + 0xf01fd: 0xf01fd, + 0xf01fe: 0xf01fe, + 0xf01ff: 0xf01ff, + 0xf0200: 0xf0200, + 0xf0201: 0xf0201, + 0xf0202: 0xf0202, + 0xf0203: 0xf0203, + 0xf0204: 0xf0204, + 0xf0205: 0xf0205, + 0xf0206: 0xf0206, + 0xf0207: 0xf0207, + 0xf0208: 0xf0208, + 0xf0209: 0xf0209, + 0xf020a: 0xf020a, + 0xf020b: 0xf020b, + 0xf020c: 0xf020c, + 0xf020d: 0xf020d, + 0xf020e: 0xf020e, + 0xf020f: 0xf020f, + 0xf0210: 0xf0210, + 0xf0211: 0xf0211, + 0xf0212: 0xf0212, + 0xf0213: 0xf0213, + 0xf0214: 0xf0214, + 0xf0215: 0xf0215, + 0xf0216: 0xf0216, + 0xf0217: 0xf0217, + 0xf0218: 0xf0218, + 0xf0219: 0xf0219, + 0xf021a: 0xf021a, + 0xf021b: 0xf021b, + 0xf021c: 0xf021c, + 0xf021d: 0xf021d, + 0xf021e: 0xf021e, + 0xf021f: 0xf021f, + 0xf0220: 0xf0220, + 0xf0221: 0xf0221, + 0xf0222: 0xf0222, + 0xf0223: 0xf0223, + 0xf0224: 0xf0224, + 0xf0225: 0xf0225, + 0xf0226: 0xf0226, + 0xf0227: 0xf0227, + 0xf0228: 0xf0228, + 0xf0229: 0xf0229, + 0xf022a: 0xf022a, + 0xf022b: 0xf022b, + 0xf022c: 0xf022c, + 0xf022d: 0xf022d, + 0xf022e: 0xf022e, + 0xf022f: 0xf022f, + 0xf0230: 0xf0230, + 0xf0231: 0xf0231, + 0xf0232: 0xf0232, + 0xf0233: 0xf0233, + 0xf0234: 0xf0234, + 0xf0235: 0xf0235, + 0xf0236: 0xf0236, + 0xf0237: 0xf0237, + 0xf0238: 0xf0238, + 0xf0239: 0xf0239, + 0xf023a: 0xf023a, + 0xf023b: 0xf023b, + 0xf023c: 0xf023c, + 0xf023d: 0xf023d, + 0xf023e: 0xf023e, + 0xf023f: 0xf023f, + 0xf0240: 0xf0240, + 0xf0241: 0xf0241, + 0xf0242: 0xf0242, + 0xf0243: 0xf0243, + 0xf0244: 0xf0244, + 0xf0245: 0xf0245, + 0xf0246: 0xf0246, + 0xf0247: 0xf0247, + 0xf0248: 0xf0248, + 0xf0249: 0xf0249, + 0xf024a: 0xf024a, + 0xf024b: 0xf024b, + 0xf024c: 0xf024c, + 0xf024d: 0xf024d, + 0xf024e: 0xf024e, + 0xf024f: 0xf024f, + 0xf0250: 0xf0250, + 0xf0251: 0xf0251, + 0xf0252: 0xf0252, + 0xf0253: 0xf0253, + 0xf0254: 0xf0254, + 0xf0255: 0xf0255, + 0xf0256: 0xf0256, + 0xf0257: 0xf0257, + 0xf0258: 0xf0258, + 0xf0259: 0xf0259, + 0xf025a: 0xf025a, + 0xf025b: 0xf025b, + 0xf025c: 0xf025c, + 0xf025d: 0xf025d, + 0xf025e: 0xf025e, + 0xf025f: 0xf025f, + 0xf0260: 0xf0260, + 0xf0261: 0xf0261, + 0xf0262: 0xf0262, + 0xf0263: 0xf0263, + 0xf0264: 0xf0264, + 0xf0265: 0xf0265, + 0xf0266: 0xf0266, + 0xf0267: 0xf0267, + 0xf0268: 0xf0268, + 0xf0269: 0xf0269, + 0xf026a: 0xf026a, + 0xf026b: 0xf026b, + 0xf026c: 0xf026c, + 0xf026d: 0xf026d, + 0xf026e: 0xf026e, + 0xf026f: 0xf026f, + 0xf0270: 0xf0270, + 0xf0271: 0xf0271, + 0xf0272: 0xf0272, + 0xf0273: 0xf0273, + 0xf0274: 0xf0274, + 0xf0275: 0xf0275, + 0xf0276: 0xf0276, + 0xf0277: 0xf0277, + 0xf0278: 0xf0278, + 0xf0279: 0xf0279, + 0xf027a: 0xf027a, + 0xf027b: 0xf027b, + 0xf027c: 0xf027c, + 0xf027d: 0xf027d, + 0xf027e: 0xf027e, + 0xf027f: 0xf027f, + 0xf0280: 0xf0280, + 0xf0281: 0xf0281, + 0xf0282: 0xf0282, + 0xf0283: 0xf0283, + 0xf0284: 0xf0284, + 0xf0285: 0xf0285, + 0xf0286: 0xf0286, + 0xf0287: 0xf0287, + 0xf0288: 0xf0288, + 0xf0289: 0xf0289, + 0xf028a: 0xf028a, + 0xf028b: 0xf028b, + 0xf028c: 0xf028c, + 0xf028d: 0xf028d, + 0xf028e: 0xf028e, + 0xf028f: 0xf028f, + 0xf0290: 0xf0290, + 0xf0291: 0xf0291, + 0xf0292: 0xf0292, + 0xf0293: 0xf0293, + 0xf0294: 0xf0294, + 0xf0295: 0xf0295, + 0xf0296: 0xf0296, + 0xf0297: 0xf0297, + 0xf0298: 0xf0298, + 0xf0299: 0xf0299, + 0xf029a: 0xf029a, + 0xf029b: 0xf029b, + 0xf029c: 0xf029c, + 0xf029d: 0xf029d, + 0xf029e: 0xf029e, + 0xf029f: 0xf029f, + 0xf02a0: 0xf02a0, + 0xf02a1: 0xf02a1, + 0xf02a2: 0xf02a2, + 0xf02a3: 0xf02a3, + 0xf02a4: 0xf02a4, + 0xf02a5: 0xf02a5, + 0xf02a6: 0xf02a6, + 0xf02a7: 0xf02a7, + 0xf02a8: 0xf02a8, + 0xf02a9: 0xf02a9, + 0xf02aa: 0xf02aa, + 0xf02ab: 0xf02ab, + 0xf02ac: 0xf02ac, + 0xf02ad: 0xf02ad, + 0xf02ae: 0xf02ae, + 0xf02af: 0xf02af, + 0xf02b0: 0xf02b0, + 0xf02b1: 0xf02b1, + 0xf02b2: 0xf02b2, + 0xf02b3: 0xf02b3, + 0xf02b4: 0xf02b4, + 0xf02b5: 0xf02b5, + 0xf02b6: 0xf02b6, + 0xf02b7: 0xf02b7, + 0xf02b8: 0xf02b8, + 0xf02b9: 0xf02b9, + 0xf02ba: 0xf02ba, + 0xf02bb: 0xf02bb, + 0xf02bc: 0xf02bc, + 0xf02bd: 0xf02bd, + 0xf02be: 0xf02be, + 0xf02bf: 0xf02bf, + 0xf02c0: 0xf02c0, + 0xf02c1: 0xf02c1, + 0xf02c2: 0xf02c2, + 0xf02c3: 0xf02c3, + 0xf02c4: 0xf02c4, + 0xf02c5: 0xf02c5, + 0xf02c6: 0xf02c6, + 0xf02c7: 0xf02c7, + 0xf02c8: 0xf02c8, + 0xf02c9: 0xf02c9, + 0xf02ca: 0xf02ca, + 0xf02cb: 0xf02cb, + 0xf02cc: 0xf02cc, + 0xf02cd: 0xf02cd, + 0xf02ce: 0xf02ce, + 0xf02cf: 0xf02cf, + 0xf02d0: 0xf02d0, + 0xf02d1: 0xf02d1, + 0xf02d2: 0xf02d2, + 0xf02d3: 0xf02d3, + 0xf02d4: 0xf02d4, + 0xf02d5: 0xf02d5, + 0xf02d6: 0xf02d6, + 0xf02d7: 0xf02d7, + 0xf02d8: 0xf02d8, + 0xf02d9: 0xf02d9, + 0xf02da: 0xf02da, + 0xf02db: 0xf02db, + 0xf02dc: 0xf02dc, + 0xf02dd: 0xf02dd, + 0xf02de: 0xf02de, + 0xf02df: 0xf02df, + 0xf02e0: 0xf02e0, + 0xf02e1: 0xf02e1, + 0xf02e2: 0xf02e2, + 0xf02e3: 0xf02e3, + 0xf02e4: 0xf02e4, + 0xf02e5: 0xf02e5, + 0xf02e6: 0xf02e6, + 0xf02e7: 0xf02e7, + 0xf02e8: 0xf02e8, + 0xf02e9: 0xf02e9, + 0xf02ea: 0xf02ea, + 0xf02eb: 0xf02eb, + 0xf02ec: 0xf02ec, + 0xf02ed: 0xf02ed, + 0xf02ee: 0xf02ee, + 0xf02ef: 0xf02ef, + 0xf02f0: 0xf02f0, + 0xf02f1: 0xf02f1, + 0xf02f2: 0xf02f2, + 0xf02f3: 0xf02f3, + 0xf02f4: 0xf02f4, + 0xf02f5: 0xf02f5, + 0xf02f6: 0xf02f6, + 0xf02f7: 0xf02f7, + 0xf02f8: 0xf02f8, + 0xf02f9: 0xf02f9, + 0xf02fa: 0xf02fa, + 0xf02fb: 0xf02fb, + 0xf02fc: 0xf02fc, + 0xf02fd: 0xf02fd, + 0xf02fe: 0xf02fe, + 0xf02ff: 0xf02ff, + 0xf0300: 0xf0300, + 0xf0301: 0xf0301, + 0xf0302: 0xf0302, + 0xf0303: 0xf0303, + 0xf0304: 0xf0304, + 0xf0305: 0xf0305, + 0xf0306: 0xf0306, + 0xf0307: 0xf0307, + 0xf0308: 0xf0308, + 0xf0309: 0xf0309, + 0xf030a: 0xf030a, + 0xf030b: 0xf030b, + 0xf030c: 0xf030c, + 0xf030d: 0xf030d, + 0xf030e: 0xf030e, + 0xf030f: 0xf030f, + 0xf0310: 0xf0310, + 0xf0311: 0xf0311, + 0xf0312: 0xf0312, + 0xf0313: 0xf0313, + 0xf0314: 0xf0314, + 0xf0315: 0xf0315, + 0xf0316: 0xf0316, + 0xf0317: 0xf0317, + 0xf0318: 0xf0318, + 0xf0319: 0xf0319, + 0xf031a: 0xf031a, + 0xf031b: 0xf031b, + 0xf031c: 0xf031c, + 0xf031d: 0xf031d, + 0xf031e: 0xf031e, + 0xf031f: 0xf031f, + 0xf0320: 0xf0320, + 0xf0321: 0xf0321, + 0xf0322: 0xf0322, + 0xf0323: 0xf0323, + 0xf0324: 0xf0324, + 0xf0325: 0xf0325, + 0xf0326: 0xf0326, + 0xf0327: 0xf0327, + 0xf0328: 0xf0328, + 0xf0329: 0xf0329, + 0xf032a: 0xf032a, + 0xf032b: 0xf032b, + 0xf032c: 0xf032c, + 0xf032d: 0xf032d, + 0xf032e: 0xf032e, + 0xf032f: 0xf032f, + 0xf0330: 0xf0330, + 0xf0331: 0xf0331, + 0xf0332: 0xf0332, + 0xf0333: 0xf0333, + 0xf0334: 0xf0334, + 0xf0335: 0xf0335, + 0xf0336: 0xf0336, + 0xf0337: 0xf0337, + 0xf0338: 0xf0338, + 0xf0339: 0xf0339, + 0xf033a: 0xf033a, + 0xf033b: 0xf033b, + 0xf033c: 0xf033c, + 0xf033d: 0xf033d, + 0xf033e: 0xf033e, + 0xf033f: 0xf033f, + 0xf0340: 0xf0340, + 0xf0341: 0xf0341, + 0xf0342: 0xf0342, + 0xf0343: 0xf0343, + 0xf0344: 0xf0344, + 0xf0345: 0xf0345, + 0xf0346: 0xf0346, + 0xf0347: 0xf0347, + 0xf0348: 0xf0348, + 0xf0349: 0xf0349, + 0xf034a: 0xf034a, + 0xf034b: 0xf034b, + 0xf034c: 0xf034c, + 0xf034d: 0xf034d, + 0xf034e: 0xf034e, + 0xf034f: 0xf034f, + 0xf0350: 0xf0350, + 0xf0351: 0xf0351, + 0xf0352: 0xf0352, + 0xf0353: 0xf0353, + 0xf0354: 0xf0354, + 0xf0355: 0xf0355, + 0xf0356: 0xf0356, + 0xf0357: 0xf0357, + 0xf0358: 0xf0358, + 0xf0359: 0xf0359, + 0xf035a: 0xf035a, + 0xf035b: 0xf035b, + 0xf035c: 0xf035c, + 0xf035d: 0xf035d, + 0xf035e: 0xf035e, + 0xf035f: 0xf035f, + 0xf0360: 0xf0360, + 0xf0361: 0xf0361, + 0xf0362: 0xf0362, + 0xf0363: 0xf0363, + 0xf0364: 0xf0364, + 0xf0365: 0xf0365, + 0xf0366: 0xf0366, + 0xf0367: 0xf0367, + 0xf0368: 0xf0368, + 0xf0369: 0xf0369, + 0xf036a: 0xf036a, + 0xf036b: 0xf036b, + 0xf036c: 0xf036c, + 0xf036d: 0xf036d, + 0xf036e: 0xf036e, + 0xf036f: 0xf036f, + 0xf0370: 0xf0370, + 0xf0371: 0xf0371, + 0xf0372: 0xf0372, + 0xf0373: 0xf0373, + 0xf0374: 0xf0374, + 0xf0375: 0xf0375, + 0xf0376: 0xf0376, + 0xf0377: 0xf0377, + 0xf0378: 0xf0378, + 0xf0379: 0xf0379, + 0xf037a: 0xf037a, + 0xf037b: 0xf037b, + 0xf037c: 0xf037c, + 0xf037d: 0xf037d, + 0xf037e: 0xf037e, + 0xf037f: 0xf037f, + 0xf0380: 0xf0380, + 0xf0381: 0xf0381, + 0xf0382: 0xf0382, + 0xf0383: 0xf0383, + 0xf0384: 0xf0384, + 0xf0385: 0xf0385, + 0xf0386: 0xf0386, + 0xf0387: 0xf0387, + 0xf0388: 0xf0388, + 0xf0389: 0xf0389, + 0xf038a: 0xf038a, + 0xf038b: 0xf038b, + 0xf038c: 0xf038c, + 0xf038d: 0xf038d, + 0xf038e: 0xf038e, + 0xf038f: 0xf038f, + 0xf0390: 0xf0390, + 0xf0391: 0xf0391, + 0xf0392: 0xf0392, + 0xf0393: 0xf0393, + 0xf0394: 0xf0394, + 0xf0395: 0xf0395, + 0xf0396: 0xf0396, + 0xf0397: 0xf0397, + 0xf0398: 0xf0398, + 0xf0399: 0xf0399, + 0xf039a: 0xf039a, + 0xf039b: 0xf039b, + 0xf039c: 0xf039c, + 0xf039d: 0xf039d, + 0xf039e: 0xf039e, + 0xf039f: 0xf039f, + 0xf03a0: 0xf03a0, + 0xf03a1: 0xf03a1, + 0xf03a2: 0xf03a2, + 0xf03a3: 0xf03a3, + 0xf03a4: 0xf03a4, + 0xf03a5: 0xf03a5, + 0xf03a6: 0xf03a6, + 0xf03a7: 0xf03a7, + 0xf03a8: 0xf03a8, + 0xf03a9: 0xf03a9, + 0xf03aa: 0xf03aa, + 0xf03ab: 0xf03ab, + 0xf03ac: 0xf03ac, + 0xf03ad: 0xf03ad, + 0xf03ae: 0xf03ae, + 0xf03af: 0xf03af, + 0xf03b0: 0xf03b0, + 0xf03b1: 0xf03b1, + 0xf03b2: 0xf03b2, + 0xf03b3: 0xf03b3, + 0xf03b4: 0xf03b4, + 0xf03b5: 0xf03b5, + 0xf03b6: 0xf03b6, + 0xf03b7: 0xf03b7, + 0xf03b8: 0xf03b8, + 0xf03b9: 0xf03b9, + 0xf03ba: 0xf03ba, + 0xf03bb: 0xf03bb, + 0xf03bc: 0xf03bc, + 0xf03bd: 0xf03bd, + 0xf03be: 0xf03be, + 0xf03bf: 0xf03bf, + 0xf03c0: 0xf03c0, + 0xf03c1: 0xf03c1, + 0xf03c2: 0xf03c2, + 0xf03c3: 0xf03c3, + 0xf03c4: 0xf03c4, + 0xf03c5: 0xf03c5, + 0xf03c6: 0xf03c6, + 0xf03c7: 0xf03c7, + 0xf03c8: 0xf03c8, + 0xf03c9: 0xf03c9, + 0xf03ca: 0xf03ca, + 0xf03cb: 0xf03cb, + 0xf03cc: 0xf03cc, + 0xf03cd: 0xf03cd, + 0xf03ce: 0xf03ce, + 0xf03cf: 0xf03cf, + 0xf03d0: 0xf03d0, + 0xf03d1: 0xf03d1, + 0xf03d2: 0xf03d2, + 0xf03d3: 0xf03d3, + 0xf03d4: 0xf03d4, + 0xf03d5: 0xf03d5, + 0xf03d6: 0xf03d6, + 0xf03d7: 0xf03d7, + 0xf03d8: 0xf03d8, + 0xf03d9: 0xf03d9, + 0xf03da: 0xf03da, + 0xf03db: 0xf03db, + 0xf03dc: 0xf03dc, + 0xf03dd: 0xf03dd, + 0xf03de: 0xf03de, + 0xf03df: 0xf03df, + 0xf03e0: 0xf03e0, + 0xf03e1: 0xf03e1, + 0xf03e2: 0xf03e2, + 0xf03e3: 0xf03e3, + 0xf03e4: 0xf03e4, + 0xf03e5: 0xf03e5, + 0xf03e6: 0xf03e6, + 0xf03e7: 0xf03e7, + 0xf03e8: 0xf03e8, + 0xf03e9: 0xf03e9, + 0xf03ea: 0xf03ea, + 0xf03eb: 0xf03eb, + 0xf03ec: 0xf03ec, + 0xf03ed: 0xf03ed, + 0xf03ee: 0xf03ee, + 0xf03ef: 0xf03ef, + 0xf03f0: 0xf03f0, + 0xf03f1: 0xf03f1, + 0xf03f2: 0xf03f2, + 0xf03f3: 0xf03f3, + 0xf03f4: 0xf03f4, + 0xf03f5: 0xf03f5, + 0xf03f6: 0xf03f6, + 0xf03f7: 0xf03f7, + 0xf03f8: 0xf03f8, + 0xf03f9: 0xf03f9, + 0xf03fa: 0xf03fa, + 0xf03fb: 0xf03fb, + 0xf03fc: 0xf03fc, + 0xf03fd: 0xf03fd, + 0xf03fe: 0xf03fe, + 0xf03ff: 0xf03ff, + 0xf0400: 0xf0400, + 0xf0401: 0xf0401, + 0xf0402: 0xf0402, + 0xf0403: 0xf0403, + 0xf0404: 0xf0404, + 0xf0405: 0xf0405, + 0xf0406: 0xf0406, + 0xf0407: 0xf0407, + 0xf0408: 0xf0408, + 0xf0409: 0xf0409, + 0xf040a: 0xf040a, + 0xf040b: 0xf040b, + 0xf040c: 0xf040c, + 0xf040d: 0xf040d, + 0xf040e: 0xf040e, + 0xf040f: 0xf040f, + 0xf0410: 0xf0410, + 0xf0411: 0xf0411, + 0xf0412: 0xf0412, + 0xf0413: 0xf0413, + 0xf0414: 0xf0414, + 0xf0415: 0xf0415, + 0xf0416: 0xf0416, + 0xf0417: 0xf0417, + 0xf0418: 0xf0418, + 0xf0419: 0xf0419, + 0xf041a: 0xf041a, + 0xf041b: 0xf041b, + 0xf041c: 0xf041c, + 0xf041d: 0xf041d, + 0xf041e: 0xf041e, + 0xf041f: 0xf041f, + 0xf0420: 0xf0420, + 0xf0421: 0xf0421, + 0xf0422: 0xf0422, + 0xf0423: 0xf0423, + 0xf0424: 0xf0424, + 0xf0425: 0xf0425, + 0xf0426: 0xf0426, + 0xf0427: 0xf0427, + 0xf0428: 0xf0428, + 0xf0429: 0xf0429, + 0xf042a: 0xf042a, + 0xf042b: 0xf042b, + 0xf042c: 0xf042c, + 0xf042d: 0xf042d, + 0xf042e: 0xf042e, + 0xf042f: 0xf042f, + 0xf0430: 0xf0430, + 0xf0431: 0xf0431, + 0xf0432: 0xf0432, + 0xf0433: 0xf0433, + 0xf0434: 0xf0434, + 0xf0435: 0xf0435, + 0xf0436: 0xf0436, + 0xf0437: 0xf0437, + 0xf0438: 0xf0438, + 0xf0439: 0xf0439, + 0xf043a: 0xf043a, + 0xf043b: 0xf043b, + 0xf043c: 0xf043c, + 0xf043d: 0xf043d, + 0xf043e: 0xf043e, + 0xf043f: 0xf043f, + 0xf0440: 0xf0440, + 0xf0441: 0xf0441, + 0xf0442: 0xf0442, + 0xf0443: 0xf0443, + 0xf0444: 0xf0444, + 0xf0445: 0xf0445, + 0xf0446: 0xf0446, + 0xf0447: 0xf0447, + 0xf0448: 0xf0448, + 0xf0449: 0xf0449, + 0xf044a: 0xf044a, + 0xf044b: 0xf044b, + 0xf044c: 0xf044c, + 0xf044d: 0xf044d, + 0xf044e: 0xf044e, + 0xf044f: 0xf044f, + 0xf0450: 0xf0450, + 0xf0451: 0xf0451, + 0xf0452: 0xf0452, + 0xf0453: 0xf0453, + 0xf0454: 0xf0454, + 0xf0455: 0xf0455, + 0xf0456: 0xf0456, + 0xf0457: 0xf0457, + 0xf0458: 0xf0458, + 0xf0459: 0xf0459, + 0xf045a: 0xf045a, + 0xf045b: 0xf045b, + 0xf045c: 0xf045c, + 0xf045d: 0xf045d, + 0xf045e: 0xf045e, + 0xf045f: 0xf045f, + 0xf0460: 0xf0460, + 0xf0461: 0xf0461, + 0xf0462: 0xf0462, + 0xf0463: 0xf0463, + 0xf0464: 0xf0464, + 0xf0465: 0xf0465, + 0xf0466: 0xf0466, + 0xf0467: 0xf0467, + 0xf0468: 0xf0468, + 0xf0469: 0xf0469, + 0xf046a: 0xf046a, + 0xf046b: 0xf046b, + 0xf046c: 0xf046c, + 0xf046d: 0xf046d, + 0xf046e: 0xf046e, + 0xf046f: 0xf046f, + 0xf0470: 0xf0470, + 0xf0471: 0xf0471, + 0xf0472: 0xf0472, + 0xf0473: 0xf0473, + 0xf0474: 0xf0474, + 0xf0475: 0xf0475, + 0xf0476: 0xf0476, + 0xf0477: 0xf0477, + 0xf0478: 0xf0478, + 0xf0479: 0xf0479, + 0xf047a: 0xf047a, + 0xf047b: 0xf047b, + 0xf047c: 0xf047c, + 0xf047d: 0xf047d, + 0xf047e: 0xf047e, + 0xf047f: 0xf047f, + 0xf0480: 0xf0480, + 0xf0481: 0xf0481, + 0xf0482: 0xf0482, + 0xf0483: 0xf0483, + 0xf0484: 0xf0484, + 0xf0485: 0xf0485, + 0xf0486: 0xf0486, + 0xf0487: 0xf0487, + 0xf0488: 0xf0488, + 0xf0489: 0xf0489, + 0xf048a: 0xf048a, + 0xf048b: 0xf048b, + 0xf048c: 0xf048c, + 0xf048d: 0xf048d, + 0xf048e: 0xf048e, + 0xf048f: 0xf048f, + 0xf0490: 0xf0490, + 0xf0491: 0xf0491, + 0xf0492: 0xf0492, + 0xf0493: 0xf0493, + 0xf0494: 0xf0494, + 0xf0495: 0xf0495, + 0xf0496: 0xf0496, + 0xf0497: 0xf0497, + 0xf0498: 0xf0498, + 0xf0499: 0xf0499, + 0xf049a: 0xf049a, + 0xf049b: 0xf049b, + 0xf049c: 0xf049c, + 0xf049d: 0xf049d, + 0xf049e: 0xf049e, + 0xf049f: 0xf049f, + 0xf04a0: 0xf04a0, + 0xf04a1: 0xf04a1, + 0xf04a2: 0xf04a2, + 0xf04a3: 0xf04a3, + 0xf04a4: 0xf04a4, + 0xf04a5: 0xf04a5, + 0xf04a6: 0xf04a6, + 0xf04a7: 0xf04a7, + 0xf04a8: 0xf04a8, + 0xf04a9: 0xf04a9, + 0xf04aa: 0xf04aa, + 0xf04ab: 0xf04ab, + 0xf04ac: 0xf04ac, + 0xf04ad: 0xf04ad, + 0xf04ae: 0xf04ae, + 0xf04af: 0xf04af, + 0xf04b0: 0xf04b0, + 0xf04b1: 0xf04b1, + 0xf04b2: 0xf04b2, + 0xf04b3: 0xf04b3, + 0xf04b4: 0xf04b4, + 0xf04b5: 0xf04b5, + 0xf04b6: 0xf04b6, + 0xf04b7: 0xf04b7, + 0xf04b8: 0xf04b8, + 0xf04b9: 0xf04b9, + 0xf04ba: 0xf04ba, + 0xf04bb: 0xf04bb, + 0xf04bc: 0xf04bc, + 0xf04bd: 0xf04bd, + 0xf04be: 0xf04be, + 0xf04bf: 0xf04bf, + 0xf04c0: 0xf04c0, + 0xf04c1: 0xf04c1, + 0xf04c2: 0xf04c2, + 0xf04c3: 0xf04c3, + 0xf04c4: 0xf04c4, + 0xf04c5: 0xf04c5, + 0xf04c6: 0xf04c6, + 0xf04c7: 0xf04c7, + 0xf04c8: 0xf04c8, + 0xf04c9: 0xf04c9, + 0xf04ca: 0xf04ca, + 0xf04cb: 0xf04cb, + 0xf04cc: 0xf04cc, + 0xf04cd: 0xf04cd, + 0xf04ce: 0xf04ce, + 0xf04cf: 0xf04cf, + 0xf04d0: 0xf04d0, + 0xf04d1: 0xf04d1, + 0xf04d2: 0xf04d2, + 0xf04d3: 0xf04d3, + 0xf04d4: 0xf04d4, + 0xf04d5: 0xf04d5, + 0xf04d6: 0xf04d6, + 0xf04d7: 0xf04d7, + 0xf04d8: 0xf04d8, + 0xf04d9: 0xf04d9, + 0xf04da: 0xf04da, + 0xf04db: 0xf04db, + 0xf04dc: 0xf04dc, + 0xf04dd: 0xf04dd, + 0xf04de: 0xf04de, + 0xf04df: 0xf04df, + 0xf04e0: 0xf04e0, + 0xf04e1: 0xf04e1, + 0xf04e2: 0xf04e2, + 0xf04e3: 0xf04e3, + 0xf04e4: 0xf04e4, + 0xf04e5: 0xf04e5, + 0xf04e6: 0xf04e6, + 0xf04e7: 0xf04e7, + 0xf04e8: 0xf04e8, + 0xf04e9: 0xf04e9, + 0xf04ea: 0xf04ea, + 0xf04eb: 0xf04eb, + 0xf04ec: 0xf04ec, + 0xf04ed: 0xf04ed, + 0xf04ee: 0xf04ee, + 0xf04ef: 0xf04ef, + 0xf04f0: 0xf04f0, + 0xf04f1: 0xf04f1, + 0xf04f2: 0xf04f2, + 0xf04f3: 0xf04f3, + 0xf04f4: 0xf04f4, + 0xf04f5: 0xf04f5, + 0xf04f6: 0xf04f6, + 0xf04f7: 0xf04f7, + 0xf04f8: 0xf04f8, + 0xf04f9: 0xf04f9, + 0xf04fa: 0xf04fa, + 0xf04fb: 0xf04fb, + 0xf04fc: 0xf04fc, + 0xf04fd: 0xf04fd, + 0xf04fe: 0xf04fe, + 0xf04ff: 0xf04ff, + 0xf0500: 0xf0500, + 0xf0501: 0xf0501, + 0xf0502: 0xf0502, + 0xf0503: 0xf0503, + 0xf0504: 0xf0504, + 0xf0505: 0xf0505, + 0xf0506: 0xf0506, + 0xf0507: 0xf0507, + 0xf0508: 0xf0508, + 0xf0509: 0xf0509, + 0xf050a: 0xf050a, + 0xf050b: 0xf050b, + 0xf050c: 0xf050c, + 0xf050d: 0xf050d, + 0xf050e: 0xf050e, + 0xf050f: 0xf050f, + 0xf0510: 0xf0510, + 0xf0511: 0xf0511, + 0xf0512: 0xf0512, + 0xf0513: 0xf0513, + 0xf0514: 0xf0514, + 0xf0515: 0xf0515, + 0xf0516: 0xf0516, + 0xf0517: 0xf0517, + 0xf0518: 0xf0518, + 0xf0519: 0xf0519, + 0xf051a: 0xf051a, + 0xf051b: 0xf051b, + 0xf051c: 0xf051c, + 0xf051d: 0xf051d, + 0xf051e: 0xf051e, + 0xf051f: 0xf051f, + 0xf0520: 0xf0520, + 0xf0521: 0xf0521, + 0xf0522: 0xf0522, + 0xf0523: 0xf0523, + 0xf0524: 0xf0524, + 0xf0525: 0xf0525, + 0xf0526: 0xf0526, + 0xf0527: 0xf0527, + 0xf0528: 0xf0528, + 0xf0529: 0xf0529, + 0xf052a: 0xf052a, + 0xf052b: 0xf052b, + 0xf052c: 0xf052c, + 0xf052d: 0xf052d, + 0xf052e: 0xf052e, + 0xf052f: 0xf052f, + 0xf0530: 0xf0530, + 0xf0531: 0xf0531, + 0xf0532: 0xf0532, + 0xf0533: 0xf0533, + 0xf0534: 0xf0534, + 0xf0535: 0xf0535, + 0xf0536: 0xf0536, + 0xf0537: 0xf0537, + 0xf0538: 0xf0538, + 0xf0539: 0xf0539, + 0xf053a: 0xf053a, + 0xf053b: 0xf053b, + 0xf053c: 0xf053c, + 0xf053d: 0xf053d, + 0xf053e: 0xf053e, + 0xf053f: 0xf053f, + 0xf0540: 0xf0540, + 0xf0541: 0xf0541, + 0xf0542: 0xf0542, + 0xf0543: 0xf0543, + 0xf0544: 0xf0544, + 0xf0545: 0xf0545, + 0xf0546: 0xf0546, + 0xf0547: 0xf0547, + 0xf0548: 0xf0548, + 0xf0549: 0xf0549, + 0xf054a: 0xf054a, + 0xf054b: 0xf054b, + 0xf054c: 0xf054c, + 0xf054d: 0xf054d, + 0xf054e: 0xf054e, + 0xf054f: 0xf054f, + 0xf0550: 0xf0550, + 0xf0551: 0xf0551, + 0xf0552: 0xf0552, + 0xf0553: 0xf0553, + 0xf0554: 0xf0554, + 0xf0555: 0xf0555, + 0xf0556: 0xf0556, + 0xf0557: 0xf0557, + 0xf0558: 0xf0558, + 0xf0559: 0xf0559, + 0xf055a: 0xf055a, + 0xf055b: 0xf055b, + 0xf055c: 0xf055c, + 0xf055d: 0xf055d, + 0xf055e: 0xf055e, + 0xf055f: 0xf055f, + 0xf0560: 0xf0560, + 0xf0561: 0xf0561, + 0xf0562: 0xf0562, + 0xf0563: 0xf0563, + 0xf0564: 0xf0564, + 0xf0565: 0xf0565, + 0xf0566: 0xf0566, + 0xf0567: 0xf0567, + 0xf0568: 0xf0568, + 0xf0569: 0xf0569, + 0xf056a: 0xf056a, + 0xf056b: 0xf056b, + 0xf056c: 0xf056c, + 0xf056d: 0xf056d, + 0xf056e: 0xf056e, + 0xf056f: 0xf056f, + 0xf0570: 0xf0570, + 0xf0571: 0xf0571, + 0xf0572: 0xf0572, + 0xf0573: 0xf0573, + 0xf0574: 0xf0574, + 0xf0575: 0xf0575, + 0xf0576: 0xf0576, + 0xf0577: 0xf0577, + 0xf0578: 0xf0578, + 0xf0579: 0xf0579, + 0xf057a: 0xf057a, + 0xf057b: 0xf057b, + 0xf057c: 0xf057c, + 0xf057d: 0xf057d, + 0xf057e: 0xf057e, + 0xf057f: 0xf057f, + 0xf0580: 0xf0580, + 0xf0581: 0xf0581, + 0xf0582: 0xf0582, + 0xf0583: 0xf0583, + 0xf0584: 0xf0584, + 0xf0585: 0xf0585, + 0xf0586: 0xf0586, + 0xf0587: 0xf0587, + 0xf0588: 0xf0588, + 0xf0589: 0xf0589, + 0xf058a: 0xf058a, + 0xf058b: 0xf058b, + 0xf058c: 0xf058c, + 0xf058d: 0xf058d, + 0xf058e: 0xf058e, + 0xf058f: 0xf058f, + 0xf0590: 0xf0590, + 0xf0591: 0xf0591, + 0xf0592: 0xf0592, + 0xf0593: 0xf0593, + 0xf0594: 0xf0594, + 0xf0595: 0xf0595, + 0xf0596: 0xf0596, + 0xf0597: 0xf0597, + 0xf0598: 0xf0598, + 0xf0599: 0xf0599, + 0xf059a: 0xf059a, + 0xf059b: 0xf059b, + 0xf059c: 0xf059c, + 0xf059d: 0xf059d, + 0xf059e: 0xf059e, + 0xf059f: 0xf059f, + 0xf05a0: 0xf05a0, + 0xf05a1: 0xf05a1, + 0xf05a2: 0xf05a2, + 0xf05a3: 0xf05a3, + 0xf05a4: 0xf05a4, + 0xf05a5: 0xf05a5, + 0xf05a6: 0xf05a6, + 0xf05a7: 0xf05a7, + 0xf05a8: 0xf05a8, + 0xf05a9: 0xf05a9, + 0xf05aa: 0xf05aa, + 0xf05ab: 0xf05ab, + 0xf05ac: 0xf05ac, + 0xf05ad: 0xf05ad, + 0xf05ae: 0xf05ae, + 0xf05af: 0xf05af, + 0xf05b0: 0xf05b0, + 0xf05b1: 0xf05b1, + 0xf05b2: 0xf05b2, + 0xf05b3: 0xf05b3, + 0xf05b4: 0xf05b4, + 0xf05b5: 0xf05b5, + 0xf05b6: 0xf05b6, + 0xf05b7: 0xf05b7, + 0xf05b8: 0xf05b8, + 0xf05b9: 0xf05b9, + 0xf05ba: 0xf05ba, + 0xf05bb: 0xf05bb, + 0xf05bc: 0xf05bc, + 0xf05bd: 0xf05bd, + 0xf05be: 0xf05be, + 0xf05bf: 0xf05bf, + 0xf05c0: 0xf05c0, + 0xf05c1: 0xf05c1, + 0xf05c2: 0xf05c2, + 0xf05c3: 0xf05c3, + 0xf05c4: 0xf05c4, + 0xf05c5: 0xf05c5, + 0xf05c6: 0xf05c6, + 0xf05c7: 0xf05c7, + 0xf05c8: 0xf05c8, + 0xf05c9: 0xf05c9, + 0xf05ca: 0xf05ca, + 0xf05cb: 0xf05cb, + 0xf05cc: 0xf05cc, + 0xf05cd: 0xf05cd, + 0xf05ce: 0xf05ce, + 0xf05cf: 0xf05cf, + 0xf05d0: 0xf05d0, + 0xf05d1: 0xf05d1, + 0xf05d2: 0xf05d2, + 0xf05d3: 0xf05d3, + 0xf05d4: 0xf05d4, + 0xf05d5: 0xf05d5, + 0xf05d6: 0xf05d6, + 0xf05d7: 0xf05d7, + 0xf05d8: 0xf05d8, + 0xf05d9: 0xf05d9, + 0xf05da: 0xf05da, + 0xf05db: 0xf05db, + 0xf05dc: 0xf05dc, + 0xf05dd: 0xf05dd, + 0xf05de: 0xf05de, + 0xf05df: 0xf05df, + 0xf05e0: 0xf05e0, + 0xf05e1: 0xf05e1, + 0xf05e2: 0xf05e2, + 0xf05e3: 0xf05e3, + 0xf05e4: 0xf05e4, + 0xf05e5: 0xf05e5, + 0xf05e6: 0xf05e6, + 0xf05e7: 0xf05e7, + 0xf05e8: 0xf05e8, + 0xf05e9: 0xf05e9, + 0xf05ea: 0xf05ea, + 0xf05eb: 0xf05eb, + 0xf05ec: 0xf05ec, + 0xf05ed: 0xf05ed, + 0xf05ee: 0xf05ee, + 0xf05ef: 0xf05ef, + 0xf05f0: 0xf05f0, + 0xf05f1: 0xf05f1, + 0xf05f2: 0xf05f2, + 0xf05f3: 0xf05f3, + 0xf05f4: 0xf05f4, + 0xf05f5: 0xf05f5, + 0xf05f6: 0xf05f6, + 0xf05f7: 0xf05f7, + 0xf05f8: 0xf05f8, + 0xf05f9: 0xf05f9, + 0xf05fa: 0xf05fa, + 0xf05fb: 0xf05fb, + 0xf05fc: 0xf05fc, + 0xf05fd: 0xf05fd, + 0xf05fe: 0xf05fe, + 0xf05ff: 0xf05ff, + 0xf0600: 0xf0600, + 0xf0601: 0xf0601, + 0xf0602: 0xf0602, + 0xf0603: 0xf0603, + 0xf0604: 0xf0604, + 0xf0605: 0xf0605, + 0xf0606: 0xf0606, + 0xf0607: 0xf0607, + 0xf0608: 0xf0608, + 0xf0609: 0xf0609, + 0xf060a: 0xf060a, + 0xf060b: 0xf060b, + 0xf060c: 0xf060c, + 0xf060d: 0xf060d, + 0xf060e: 0xf060e, + 0xf060f: 0xf060f, + 0xf0610: 0xf0610, + 0xf0611: 0xf0611, + 0xf0612: 0xf0612, + 0xf0613: 0xf0613, + 0xf0614: 0xf0614, + 0xf0615: 0xf0615, + 0xf0616: 0xf0616, + 0xf0617: 0xf0617, + 0xf0618: 0xf0618, + 0xf0619: 0xf0619, + 0xf061a: 0xf061a, + 0xf061b: 0xf061b, + 0xf061c: 0xf061c, + 0xf061d: 0xf061d, + 0xf061e: 0xf061e, + 0xf061f: 0xf061f, + 0xf0620: 0xf0620, + 0xf0621: 0xf0621, + 0xf0622: 0xf0622, + 0xf0623: 0xf0623, + 0xf0624: 0xf0624, + 0xf0625: 0xf0625, + 0xf0626: 0xf0626, + 0xf0627: 0xf0627, + 0xf0628: 0xf0628, + 0xf0629: 0xf0629, + 0xf062a: 0xf062a, + 0xf062b: 0xf062b, + 0xf062c: 0xf062c, + 0xf062d: 0xf062d, + 0xf062e: 0xf062e, + 0xf062f: 0xf062f, + 0xf0630: 0xf0630, + 0xf0631: 0xf0631, + 0xf0632: 0xf0632, + 0xf0633: 0xf0633, + 0xf0634: 0xf0634, + 0xf0635: 0xf0635, + 0xf0636: 0xf0636, + 0xf0637: 0xf0637, + 0xf0638: 0xf0638, + 0xf0639: 0xf0639, + 0xf063a: 0xf063a, + 0xf063b: 0xf063b, + 0xf063c: 0xf063c, + 0xf063d: 0xf063d, + 0xf063e: 0xf063e, + 0xf063f: 0xf063f, + 0xf0640: 0xf0640, + 0xf0641: 0xf0641, + 0xf0642: 0xf0642, + 0xf0643: 0xf0643, + 0xf0644: 0xf0644, + 0xf0645: 0xf0645, + 0xf0646: 0xf0646, + 0xf0647: 0xf0647, + 0xf0648: 0xf0648, + 0xf0649: 0xf0649, + 0xf064a: 0xf064a, + 0xf064b: 0xf064b, + 0xf064c: 0xf064c, + 0xf064d: 0xf064d, + 0xf064e: 0xf064e, + 0xf064f: 0xf064f, + 0xf0650: 0xf0650, + 0xf0651: 0xf0651, + 0xf0652: 0xf0652, + 0xf0653: 0xf0653, + 0xf0654: 0xf0654, + 0xf0655: 0xf0655, + 0xf0656: 0xf0656, + 0xf0657: 0xf0657, + 0xf0658: 0xf0658, + 0xf0659: 0xf0659, + 0xf065a: 0xf065a, + 0xf065b: 0xf065b, + 0xf065c: 0xf065c, + 0xf065d: 0xf065d, + 0xf065e: 0xf065e, + 0xf065f: 0xf065f, + 0xf0660: 0xf0660, + 0xf0661: 0xf0661, + 0xf0662: 0xf0662, + 0xf0663: 0xf0663, + 0xf0664: 0xf0664, + 0xf0665: 0xf0665, + 0xf0666: 0xf0666, + 0xf0667: 0xf0667, + 0xf0668: 0xf0668, + 0xf0669: 0xf0669, + 0xf066a: 0xf066a, + 0xf066b: 0xf066b, + 0xf066c: 0xf066c, + 0xf066d: 0xf066d, + 0xf066e: 0xf066e, + 0xf066f: 0xf066f, + 0xf0670: 0xf0670, + 0xf0671: 0xf0671, + 0xf0672: 0xf0672, + 0xf0673: 0xf0673, + 0xf0674: 0xf0674, + 0xf0675: 0xf0675, + 0xf0676: 0xf0676, + 0xf0677: 0xf0677, + 0xf0678: 0xf0678, + 0xf0679: 0xf0679, + 0xf067a: 0xf067a, + 0xf067b: 0xf067b, + 0xf067c: 0xf067c, + 0xf067d: 0xf067d, + 0xf067e: 0xf067e, + 0xf067f: 0xf067f, + 0xf0680: 0xf0680, + 0xf0681: 0xf0681, + 0xf0682: 0xf0682, + 0xf0683: 0xf0683, + 0xf0684: 0xf0684, + 0xf0685: 0xf0685, + 0xf0686: 0xf0686, + 0xf0687: 0xf0687, + 0xf0688: 0xf0688, + 0xf0689: 0xf0689, + 0xf068a: 0xf068a, + 0xf068b: 0xf068b, + 0xf068c: 0xf068c, + 0xf068d: 0xf068d, + 0xf068e: 0xf068e, + 0xf068f: 0xf068f, + 0xf0690: 0xf0690, + 0xf0691: 0xf0691, + 0xf0692: 0xf0692, + 0xf0693: 0xf0693, + 0xf0694: 0xf0694, + 0xf0695: 0xf0695, + 0xf0696: 0xf0696, + 0xf0697: 0xf0697, + 0xf0698: 0xf0698, + 0xf0699: 0xf0699, + 0xf069a: 0xf069a, + 0xf069b: 0xf069b, + 0xf069c: 0xf069c, + 0xf069d: 0xf069d, + 0xf069e: 0xf069e, + 0xf069f: 0xf069f, + 0xf06a0: 0xf06a0, + 0xf06a1: 0xf06a1, + 0xf06a2: 0xf06a2, + 0xf06a3: 0xf06a3, + 0xf06a4: 0xf06a4, + 0xf06a5: 0xf06a5, + 0xf06a6: 0xf06a6, + 0xf06a7: 0xf06a7, + 0xf06a8: 0xf06a8, + 0xf06a9: 0xf06a9, + 0xf06aa: 0xf06aa, + 0xf06ab: 0xf06ab, + 0xf06ac: 0xf06ac, + 0xf06ad: 0xf06ad, + 0xf06ae: 0xf06ae, + 0xf06af: 0xf06af, + 0xf06b0: 0xf06b0, + 0xf06b1: 0xf06b1, + 0xf06b2: 0xf06b2, + 0xf06b3: 0xf06b3, + 0xf06b4: 0xf06b4, + 0xf06b5: 0xf06b5, + 0xf06b6: 0xf06b6, + 0xf06b7: 0xf06b7, + 0xf06b8: 0xf06b8, + 0xf06b9: 0xf06b9, + 0xf06ba: 0xf06ba, + 0xf06bb: 0xf06bb, + 0xf06bc: 0xf06bc, + 0xf06bd: 0xf06bd, + 0xf06be: 0xf06be, + 0xf06bf: 0xf06bf, + 0xf06c0: 0xf06c0, + 0xf06c1: 0xf06c1, + 0xf06c2: 0xf06c2, + 0xf06c3: 0xf06c3, + 0xf06c4: 0xf06c4, + 0xf06c5: 0xf06c5, + 0xf06c6: 0xf06c6, + 0xf06c7: 0xf06c7, + 0xf06c8: 0xf06c8, + 0xf06c9: 0xf06c9, + 0xf06ca: 0xf06ca, + 0xf06cb: 0xf06cb, + 0xf06cc: 0xf06cc, + 0xf06cd: 0xf06cd, + 0xf06ce: 0xf06ce, + 0xf06cf: 0xf06cf, + 0xf06d0: 0xf06d0, + 0xf06d1: 0xf06d1, + 0xf06d2: 0xf06d2, + 0xf06d3: 0xf06d3, + 0xf06d4: 0xf06d4, + 0xf06d5: 0xf06d5, + 0xf06d6: 0xf06d6, + 0xf06d7: 0xf06d7, + 0xf06d8: 0xf06d8, + 0xf06d9: 0xf06d9, + 0xf06da: 0xf06da, + 0xf06db: 0xf06db, + 0xf06dc: 0xf06dc, + 0xf06dd: 0xf06dd, + 0xf06de: 0xf06de, + 0xf06df: 0xf06df, + 0xf06e0: 0xf06e0, + 0xf06e1: 0xf06e1, + 0xf06e2: 0xf06e2, + 0xf06e3: 0xf06e3, + 0xf06e4: 0xf06e4, + 0xf06e5: 0xf06e5, + 0xf06e6: 0xf06e6, + 0xf06e7: 0xf06e7, + 0xf06e8: 0xf06e8, + 0xf06e9: 0xf06e9, + 0xf06ea: 0xf06ea, + 0xf06eb: 0xf06eb, + 0xf06ec: 0xf06ec, + 0xf06ed: 0xf06ed, + 0xf06ee: 0xf06ee, + 0xf06ef: 0xf06ef, + 0xf06f0: 0xf06f0, + 0xf06f1: 0xf06f1, + 0xf06f2: 0xf06f2, + 0xf06f3: 0xf06f3, + 0xf06f4: 0xf06f4, + 0xf06f5: 0xf06f5, + 0xf06f6: 0xf06f6, + 0xf06f7: 0xf06f7, + 0xf06f8: 0xf06f8, + 0xf06f9: 0xf06f9, + 0xf06fa: 0xf06fa, + 0xf06fb: 0xf06fb, + 0xf06fc: 0xf06fc, + 0xf06fd: 0xf06fd, + 0xf06fe: 0xf06fe, + 0xf06ff: 0xf06ff, + 0xf0700: 0xf0700, + 0xf0701: 0xf0701, + 0xf0702: 0xf0702, + 0xf0703: 0xf0703, + 0xf0704: 0xf0704, + 0xf0705: 0xf0705, + 0xf0706: 0xf0706, + 0xf0707: 0xf0707, + 0xf0708: 0xf0708, + 0xf0709: 0xf0709, + 0xf070a: 0xf070a, + 0xf070b: 0xf070b, + 0xf070c: 0xf070c, + 0xf070d: 0xf070d, + 0xf070e: 0xf070e, + 0xf070f: 0xf070f, + 0xf0710: 0xf0710, + 0xf0711: 0xf0711, + 0xf0712: 0xf0712, + 0xf0713: 0xf0713, + 0xf0714: 0xf0714, + 0xf0715: 0xf0715, + 0xf0716: 0xf0716, + 0xf0717: 0xf0717, + 0xf0718: 0xf0718, + 0xf0719: 0xf0719, + 0xf071a: 0xf071a, + 0xf071b: 0xf071b, + 0xf071c: 0xf071c, + 0xf071d: 0xf071d, + 0xf071e: 0xf071e, + 0xf071f: 0xf071f, + 0xf0720: 0xf0720, + 0xf0721: 0xf0721, + 0xf0722: 0xf0722, + 0xf0723: 0xf0723, + 0xf0724: 0xf0724, + 0xf0725: 0xf0725, + 0xf0726: 0xf0726, + 0xf0727: 0xf0727, + 0xf0728: 0xf0728, + 0xf0729: 0xf0729, + 0xf072a: 0xf072a, + 0xf072b: 0xf072b, + 0xf072c: 0xf072c, + 0xf072d: 0xf072d, + 0xf072e: 0xf072e, + 0xf072f: 0xf072f, + 0xf0730: 0xf0730, + 0xf0731: 0xf0731, + 0xf0732: 0xf0732, + 0xf0733: 0xf0733, + 0xf0734: 0xf0734, + 0xf0735: 0xf0735, + 0xf0736: 0xf0736, + 0xf0737: 0xf0737, + 0xf0738: 0xf0738, + 0xf0739: 0xf0739, + 0xf073a: 0xf073a, + 0xf073b: 0xf073b, + 0xf073c: 0xf073c, + 0xf073d: 0xf073d, + 0xf073e: 0xf073e, + 0xf073f: 0xf073f, + 0xf0740: 0xf0740, + 0xf0741: 0xf0741, + 0xf0742: 0xf0742, + 0xf0743: 0xf0743, + 0xf0744: 0xf0744, + 0xf0745: 0xf0745, + 0xf0746: 0xf0746, + 0xf0747: 0xf0747, + 0xf0748: 0xf0748, + 0xf0749: 0xf0749, + 0xf074a: 0xf074a, + 0xf074b: 0xf074b, + 0xf074c: 0xf074c, + 0xf074d: 0xf074d, + 0xf074e: 0xf074e, + 0xf074f: 0xf074f, + 0xf0750: 0xf0750, + 0xf0751: 0xf0751, + 0xf0752: 0xf0752, + 0xf0753: 0xf0753, + 0xf0754: 0xf0754, + 0xf0755: 0xf0755, + 0xf0756: 0xf0756, + 0xf0757: 0xf0757, + 0xf0758: 0xf0758, + 0xf0759: 0xf0759, + 0xf075a: 0xf075a, + 0xf075b: 0xf075b, + 0xf075c: 0xf075c, + 0xf075d: 0xf075d, + 0xf075e: 0xf075e, + 0xf075f: 0xf075f, + 0xf0760: 0xf0760, + 0xf0761: 0xf0761, + 0xf0762: 0xf0762, + 0xf0763: 0xf0763, + 0xf0764: 0xf0764, + 0xf0765: 0xf0765, + 0xf0766: 0xf0766, + 0xf0767: 0xf0767, + 0xf0768: 0xf0768, + 0xf0769: 0xf0769, + 0xf076a: 0xf076a, + 0xf076b: 0xf076b, + 0xf076c: 0xf076c, + 0xf076d: 0xf076d, + 0xf076e: 0xf076e, + 0xf076f: 0xf076f, + 0xf0770: 0xf0770, + 0xf0771: 0xf0771, + 0xf0772: 0xf0772, + 0xf0773: 0xf0773, + 0xf0774: 0xf0774, + 0xf0775: 0xf0775, + 0xf0776: 0xf0776, + 0xf0777: 0xf0777, + 0xf0778: 0xf0778, + 0xf0779: 0xf0779, + 0xf077a: 0xf077a, + 0xf077b: 0xf077b, + 0xf077c: 0xf077c, + 0xf077d: 0xf077d, + 0xf077e: 0xf077e, + 0xf077f: 0xf077f, + 0xf0780: 0xf0780, + 0xf0781: 0xf0781, + 0xf0782: 0xf0782, + 0xf0783: 0xf0783, + 0xf0784: 0xf0784, + 0xf0785: 0xf0785, + 0xf0786: 0xf0786, + 0xf0787: 0xf0787, + 0xf0788: 0xf0788, + 0xf0789: 0xf0789, + 0xf078a: 0xf078a, + 0xf078b: 0xf078b, + 0xf078c: 0xf078c, + 0xf078d: 0xf078d, + 0xf078e: 0xf078e, + 0xf078f: 0xf078f, + 0xf0790: 0xf0790, + 0xf0791: 0xf0791, + 0xf0792: 0xf0792, + 0xf0793: 0xf0793, + 0xf0794: 0xf0794, + 0xf0795: 0xf0795, + 0xf0796: 0xf0796, + 0xf0797: 0xf0797, + 0xf0798: 0xf0798, + 0xf0799: 0xf0799, + 0xf079a: 0xf079a, + 0xf079b: 0xf079b, + 0xf079c: 0xf079c, + 0xf079d: 0xf079d, + 0xf079e: 0xf079e, + 0xf079f: 0xf079f, + 0xf07a0: 0xf07a0, + 0xf07a1: 0xf07a1, + 0xf07a2: 0xf07a2, + 0xf07a3: 0xf07a3, + 0xf07a4: 0xf07a4, + 0xf07a5: 0xf07a5, + 0xf07a6: 0xf07a6, + 0xf07a7: 0xf07a7, + 0xf07a8: 0xf07a8, + 0xf07a9: 0xf07a9, + 0xf07aa: 0xf07aa, + 0xf07ab: 0xf07ab, + 0xf07ac: 0xf07ac, + 0xf07ad: 0xf07ad, + 0xf07ae: 0xf07ae, + 0xf07af: 0xf07af, + 0xf07b0: 0xf07b0, + 0xf07b1: 0xf07b1, + 0xf07b2: 0xf07b2, + 0xf07b3: 0xf07b3, + 0xf07b4: 0xf07b4, + 0xf07b5: 0xf07b5, + 0xf07b6: 0xf07b6, + 0xf07b7: 0xf07b7, + 0xf07b8: 0xf07b8, + 0xf07b9: 0xf07b9, + 0xf07ba: 0xf07ba, + 0xf07bb: 0xf07bb, + 0xf07bc: 0xf07bc, + 0xf07bd: 0xf07bd, + 0xf07be: 0xf07be, + 0xf07bf: 0xf07bf, + 0xf07c0: 0xf07c0, + 0xf07c1: 0xf07c1, + 0xf07c2: 0xf07c2, + 0xf07c3: 0xf07c3, + 0xf07c4: 0xf07c4, + 0xf07c5: 0xf07c5, + 0xf07c6: 0xf07c6, + 0xf07c7: 0xf07c7, + 0xf07c8: 0xf07c8, + 0xf07c9: 0xf07c9, + 0xf07ca: 0xf07ca, + 0xf07cb: 0xf07cb, + 0xf07cc: 0xf07cc, + 0xf07cd: 0xf07cd, + 0xf07ce: 0xf07ce, + 0xf07cf: 0xf07cf, + 0xf07d0: 0xf07d0, + 0xf07d1: 0xf07d1, + 0xf07d2: 0xf07d2, + 0xf07d3: 0xf07d3, + 0xf07d4: 0xf07d4, + 0xf07d5: 0xf07d5, + 0xf07d6: 0xf07d6, + 0xf07d7: 0xf07d7, + 0xf07d8: 0xf07d8, + 0xf07d9: 0xf07d9, + 0xf07da: 0xf07da, + 0xf07db: 0xf07db, + 0xf07dc: 0xf07dc, + 0xf07dd: 0xf07dd, + 0xf07de: 0xf07de, + 0xf07df: 0xf07df, + 0xf07e0: 0xf07e0, + 0xf07e1: 0xf07e1, + 0xf07e2: 0xf07e2, + 0xf07e3: 0xf07e3, + 0xf07e4: 0xf07e4, + 0xf07e5: 0xf07e5, + 0xf07e6: 0xf07e6, + 0xf07e7: 0xf07e7, + 0xf07e8: 0xf07e8, + 0xf07e9: 0xf07e9, + 0xf07ea: 0xf07ea, + 0xf07eb: 0xf07eb, + 0xf07ec: 0xf07ec, + 0xf07ed: 0xf07ed, + 0xf07ee: 0xf07ee, + 0xf07ef: 0xf07ef, + 0xf07f0: 0xf07f0, + 0xf07f1: 0xf07f1, + 0xf07f2: 0xf07f2, + 0xf07f3: 0xf07f3, + 0xf07f4: 0xf07f4, + 0xf07f5: 0xf07f5, + 0xf07f6: 0xf07f6, + 0xf07f7: 0xf07f7, + 0xf07f8: 0xf07f8, + 0xf07f9: 0xf07f9, + 0xf07fa: 0xf07fa, + 0xf07fb: 0xf07fb, + 0xf07fc: 0xf07fc, + 0xf07fd: 0xf07fd, + 0xf07fe: 0xf07fe, + 0xf07ff: 0xf07ff, + 0xf0800: 0xf0800, + 0xf0801: 0xf0801, + 0xf0802: 0xf0802, + 0xf0803: 0xf0803, + 0xf0804: 0xf0804, + 0xf0805: 0xf0805, + 0xf0806: 0xf0806, + 0xf0807: 0xf0807, + 0xf0808: 0xf0808, + 0xf0809: 0xf0809, + 0xf080a: 0xf080a, + 0xf080b: 0xf080b, + 0xf080c: 0xf080c, + 0xf080d: 0xf080d, + 0xf080e: 0xf080e, + 0xf080f: 0xf080f, + 0xf0810: 0xf0810, + 0xf0811: 0xf0811, + 0xf0812: 0xf0812, + 0xf0813: 0xf0813, + 0xf0814: 0xf0814, + 0xf0815: 0xf0815, + 0xf0816: 0xf0816, + 0xf0817: 0xf0817, + 0xf0818: 0xf0818, + 0xf0819: 0xf0819, + 0xf081a: 0xf081a, + 0xf081b: 0xf081b, + 0xf081c: 0xf081c, + 0xf081d: 0xf081d, + 0xf081e: 0xf081e, + 0xf081f: 0xf081f, + 0xf0820: 0xf0820, + 0xf0821: 0xf0821, + 0xf0822: 0xf0822, + 0xf0823: 0xf0823, + 0xf0824: 0xf0824, + 0xf0825: 0xf0825, + 0xf0826: 0xf0826, + 0xf0827: 0xf0827, + 0xf0828: 0xf0828, + 0xf0829: 0xf0829, + 0xf082a: 0xf082a, + 0xf082b: 0xf082b, + 0xf082c: 0xf082c, + 0xf082d: 0xf082d, + 0xf082e: 0xf082e, + 0xf082f: 0xf082f, + 0xf0830: 0xf0830, + 0xf0831: 0xf0831, + 0xf0832: 0xf0832, + 0xf0833: 0xf0833, + 0xf0834: 0xf0834, + 0xf0835: 0xf0835, + 0xf0836: 0xf0836, + 0xf0837: 0xf0837, + 0xf0838: 0xf0838, + 0xf0839: 0xf0839, + 0xf083a: 0xf083a, + 0xf083b: 0xf083b, + 0xf083c: 0xf083c, + 0xf083d: 0xf083d, + 0xf083e: 0xf083e, + 0xf083f: 0xf083f, + 0xf0840: 0xf0840, + 0xf0841: 0xf0841, + 0xf0842: 0xf0842, + 0xf0843: 0xf0843, + 0xf0844: 0xf0844, + 0xf0845: 0xf0845, + 0xf0846: 0xf0846, + 0xf0847: 0xf0847, + 0xf0848: 0xf0848, + 0xf0849: 0xf0849, + 0xf084a: 0xf084a, + 0xf084b: 0xf084b, + 0xf084c: 0xf084c, + 0xf084d: 0xf084d, + 0xf084e: 0xf084e, + 0xf084f: 0xf084f, + 0xf0850: 0xf0850, + 0xf0851: 0xf0851, + 0xf0852: 0xf0852, + 0xf0853: 0xf0853, + 0xf0854: 0xf0854, + 0xf0855: 0xf0855, + 0xf0856: 0xf0856, + 0xf0857: 0xf0857, + 0xf0858: 0xf0858, + 0xf0859: 0xf0859, + 0xf085a: 0xf085a, + 0xf085b: 0xf085b, + 0xf085c: 0xf085c, + 0xf085d: 0xf085d, + 0xf085e: 0xf085e, + 0xf085f: 0xf085f, + 0xf0860: 0xf0860, + 0xf0861: 0xf0861, + 0xf0862: 0xf0862, + 0xf0863: 0xf0863, + 0xf0864: 0xf0864, + 0xf0865: 0xf0865, + 0xf0866: 0xf0866, + 0xf0867: 0xf0867, + 0xf0868: 0xf0868, + 0xf0869: 0xf0869, + 0xf086a: 0xf086a, + 0xf086b: 0xf086b, + 0xf086c: 0xf086c, + 0xf086d: 0xf086d, + 0xf086e: 0xf086e, + 0xf086f: 0xf086f, + 0xf0870: 0xf0870, + 0xf0871: 0xf0871, + 0xf0872: 0xf0872, + 0xf0873: 0xf0873, + 0xf0874: 0xf0874, + 0xf0875: 0xf0875, + 0xf0876: 0xf0876, + 0xf0877: 0xf0877, + 0xf0878: 0xf0878, + 0xf0879: 0xf0879, + 0xf087a: 0xf087a, + 0xf087b: 0xf087b, + 0xf087c: 0xf087c, + 0xf087d: 0xf087d, + 0xf087e: 0xf087e, + 0xf087f: 0xf087f, + 0xf0880: 0xf0880, + 0xf0881: 0xf0881, + 0xf0882: 0xf0882, + 0xf0883: 0xf0883, + 0xf0884: 0xf0884, + 0xf0885: 0xf0885, + 0xf0886: 0xf0886, + 0xf0887: 0xf0887, + 0xf0888: 0xf0888, + 0xf0889: 0xf0889, + 0xf088a: 0xf088a, + 0xf088b: 0xf088b, + 0xf088c: 0xf088c, + 0xf088d: 0xf088d, + 0xf088e: 0xf088e, + 0xf088f: 0xf088f, + 0xf0890: 0xf0890, + 0xf0891: 0xf0891, + 0xf0892: 0xf0892, + 0xf0893: 0xf0893, + 0xf0894: 0xf0894, + 0xf0895: 0xf0895, + 0xf0896: 0xf0896, + 0xf0897: 0xf0897, + 0xf0898: 0xf0898, + 0xf0899: 0xf0899, + 0xf089a: 0xf089a, + 0xf089b: 0xf089b, + 0xf089c: 0xf089c, + 0xf089d: 0xf089d, + 0xf089e: 0xf089e, + 0xf089f: 0xf089f, + 0xf08a0: 0xf08a0, + 0xf08a1: 0xf08a1, + 0xf08a2: 0xf08a2, + 0xf08a3: 0xf08a3, + 0xf08a4: 0xf08a4, + 0xf08a5: 0xf08a5, + 0xf08a6: 0xf08a6, + 0xf08a7: 0xf08a7, + 0xf08a8: 0xf08a8, + 0xf08a9: 0xf08a9, + 0xf08aa: 0xf08aa, + 0xf08ab: 0xf08ab, + 0xf08ac: 0xf08ac, + 0xf08ad: 0xf08ad, + 0xf08ae: 0xf08ae, + 0xf08af: 0xf08af, + 0xf08b0: 0xf08b0, + 0xf08b1: 0xf08b1, + 0xf08b2: 0xf08b2, + 0xf08b3: 0xf08b3, + 0xf08b4: 0xf08b4, + 0xf08b5: 0xf08b5, + 0xf08b6: 0xf08b6, + 0xf08b7: 0xf08b7, + 0xf08b8: 0xf08b8, + 0xf08b9: 0xf08b9, + 0xf08ba: 0xf08ba, + 0xf08bb: 0xf08bb, + 0xf08bc: 0xf08bc, + 0xf08bd: 0xf08bd, + 0xf08be: 0xf08be, + 0xf08bf: 0xf08bf, + 0xf08c0: 0xf08c0, + 0xf08c1: 0xf08c1, + 0xf08c2: 0xf08c2, + 0xf08c3: 0xf08c3, + 0xf08c4: 0xf08c4, + 0xf08c5: 0xf08c5, + 0xf08c6: 0xf08c6, + 0xf08c7: 0xf08c7, + 0xf08c8: 0xf08c8, + 0xf08c9: 0xf08c9, + 0xf08ca: 0xf08ca, + 0xf08cb: 0xf08cb, + 0xf08cc: 0xf08cc, + 0xf08cd: 0xf08cd, + 0xf08ce: 0xf08ce, + 0xf08cf: 0xf08cf, + 0xf08d0: 0xf08d0, + 0xf08d1: 0xf08d1, + 0xf08d2: 0xf08d2, + 0xf08d3: 0xf08d3, + 0xf08d4: 0xf08d4, + 0xf08d5: 0xf08d5, + 0xf08d6: 0xf08d6, + 0xf08d7: 0xf08d7, + 0xf08d8: 0xf08d8, + 0xf08d9: 0xf08d9, + 0xf08da: 0xf08da, + 0xf08db: 0xf08db, + 0xf08dc: 0xf08dc, + 0xf08dd: 0xf08dd, + 0xf08de: 0xf08de, + 0xf08df: 0xf08df, + 0xf08e0: 0xf08e0, + 0xf08e1: 0xf08e1, + 0xf08e2: 0xf08e2, + 0xf08e3: 0xf08e3, + 0xf08e4: 0xf08e4, + 0xf08e5: 0xf08e5, + 0xf08e6: 0xf08e6, + 0xf08e7: 0xf08e7, + 0xf08e8: 0xf08e8, + 0xf08e9: 0xf08e9, + 0xf08ea: 0xf08ea, + 0xf08eb: 0xf08eb, + 0xf08ec: 0xf08ec, + 0xf08ed: 0xf08ed, + 0xf08ee: 0xf08ee, + 0xf08ef: 0xf08ef, + 0xf08f0: 0xf08f0, + 0xf08f1: 0xf08f1, + 0xf08f2: 0xf08f2, + 0xf08f3: 0xf08f3, + 0xf08f4: 0xf08f4, + 0xf08f5: 0xf08f5, + 0xf08f6: 0xf08f6, + 0xf08f7: 0xf08f7, + 0xf08f8: 0xf08f8, + 0xf08f9: 0xf08f9, + 0xf08fa: 0xf08fa, + 0xf08fb: 0xf08fb, + 0xf08fc: 0xf08fc, + 0xf08fd: 0xf08fd, + 0xf08fe: 0xf08fe, + 0xf08ff: 0xf08ff, + 0xf0900: 0xf0900, + 0xf0901: 0xf0901, + 0xf0902: 0xf0902, + 0xf0903: 0xf0903, + 0xf0904: 0xf0904, + 0xf0905: 0xf0905, + 0xf0906: 0xf0906, + 0xf0907: 0xf0907, + 0xf0908: 0xf0908, + 0xf0909: 0xf0909, + 0xf090a: 0xf090a, + 0xf090b: 0xf090b, + 0xf090c: 0xf090c, + 0xf090d: 0xf090d, + 0xf090e: 0xf090e, + 0xf090f: 0xf090f, + 0xf0910: 0xf0910, + 0xf0911: 0xf0911, + 0xf0912: 0xf0912, + 0xf0913: 0xf0913, + 0xf0914: 0xf0914, + 0xf0915: 0xf0915, + 0xf0916: 0xf0916, + 0xf0917: 0xf0917, + 0xf0918: 0xf0918, + 0xf0919: 0xf0919, + 0xf091a: 0xf091a, + 0xf091b: 0xf091b, + 0xf091c: 0xf091c, + 0xf091d: 0xf091d, + 0xf091e: 0xf091e, + 0xf091f: 0xf091f, + 0xf0920: 0xf0920, + 0xf0921: 0xf0921, + 0xf0922: 0xf0922, + 0xf0923: 0xf0923, + 0xf0924: 0xf0924, + 0xf0925: 0xf0925, + 0xf0926: 0xf0926, + 0xf0927: 0xf0927, + 0xf0928: 0xf0928, + 0xf0929: 0xf0929, + 0xf092a: 0xf092a, + 0xf092b: 0xf092b, + 0xf092c: 0xf092c, + 0xf092d: 0xf092d, + 0xf092e: 0xf092e, + 0xf092f: 0xf092f, + 0xf0930: 0xf0930, + 0xf0931: 0xf0931, + 0xf0932: 0xf0932, + 0xf0933: 0xf0933, + 0xf0934: 0xf0934, + 0xf0935: 0xf0935, + 0xf0936: 0xf0936, + 0xf0937: 0xf0937, + 0xf0938: 0xf0938, + 0xf0939: 0xf0939, + 0xf093a: 0xf093a, + 0xf093b: 0xf093b, + 0xf093c: 0xf093c, + 0xf093d: 0xf093d, + 0xf093e: 0xf093e, + 0xf093f: 0xf093f, + 0xf0940: 0xf0940, + 0xf0941: 0xf0941, + 0xf0942: 0xf0942, + 0xf0943: 0xf0943, + 0xf0944: 0xf0944, + 0xf0945: 0xf0945, + 0xf0946: 0xf0946, + 0xf0947: 0xf0947, + 0xf0948: 0xf0948, + 0xf0949: 0xf0949, + 0xf094a: 0xf094a, + 0xf094b: 0xf094b, + 0xf094c: 0xf094c, + 0xf094d: 0xf094d, + 0xf094e: 0xf094e, + 0xf094f: 0xf094f, + 0xf0950: 0xf0950, + 0xf0951: 0xf0951, + 0xf0952: 0xf0952, + 0xf0953: 0xf0953, + 0xf0954: 0xf0954, + 0xf0955: 0xf0955, + 0xf0956: 0xf0956, + 0xf0957: 0xf0957, + 0xf0958: 0xf0958, + 0xf0959: 0xf0959, + 0xf095a: 0xf095a, + 0xf095b: 0xf095b, + 0xf095c: 0xf095c, + 0xf095d: 0xf095d, + 0xf095e: 0xf095e, + 0xf095f: 0xf095f, + 0xf0960: 0xf0960, + 0xf0961: 0xf0961, + 0xf0962: 0xf0962, + 0xf0963: 0xf0963, + 0xf0964: 0xf0964, + 0xf0965: 0xf0965, + 0xf0966: 0xf0966, + 0xf0967: 0xf0967, + 0xf0968: 0xf0968, + 0xf0969: 0xf0969, + 0xf096a: 0xf096a, + 0xf096b: 0xf096b, + 0xf096c: 0xf096c, + 0xf096d: 0xf096d, + 0xf096e: 0xf096e, + 0xf096f: 0xf096f, + 0xf0970: 0xf0970, + 0xf0971: 0xf0971, + 0xf0972: 0xf0972, + 0xf0973: 0xf0973, + 0xf0974: 0xf0974, + 0xf0975: 0xf0975, + 0xf0976: 0xf0976, + 0xf0977: 0xf0977, + 0xf0978: 0xf0978, + 0xf0979: 0xf0979, + 0xf097a: 0xf097a, + 0xf097b: 0xf097b, + 0xf097c: 0xf097c, + 0xf097d: 0xf097d, + 0xf097e: 0xf097e, + 0xf097f: 0xf097f, + 0xf0980: 0xf0980, + 0xf0981: 0xf0981, + 0xf0982: 0xf0982, + 0xf0983: 0xf0983, + 0xf0984: 0xf0984, + 0xf0985: 0xf0985, + 0xf0986: 0xf0986, + 0xf0987: 0xf0987, + 0xf0988: 0xf0988, + 0xf0989: 0xf0989, + 0xf098a: 0xf098a, + 0xf098b: 0xf098b, + 0xf098c: 0xf098c, + 0xf098d: 0xf098d, + 0xf098e: 0xf098e, + 0xf098f: 0xf098f, + 0xf0990: 0xf0990, + 0xf0991: 0xf0991, + 0xf0992: 0xf0992, + 0xf0993: 0xf0993, + 0xf0994: 0xf0994, + 0xf0995: 0xf0995, + 0xf0996: 0xf0996, + 0xf0997: 0xf0997, + 0xf0998: 0xf0998, + 0xf0999: 0xf0999, + 0xf099a: 0xf099a, + 0xf099b: 0xf099b, + 0xf099c: 0xf099c, + 0xf099d: 0xf099d, + 0xf099e: 0xf099e, + 0xf099f: 0xf099f, + 0xf09a0: 0xf09a0, + 0xf09a1: 0xf09a1, + 0xf09a2: 0xf09a2, + 0xf09a3: 0xf09a3, + 0xf09a4: 0xf09a4, + 0xf09a5: 0xf09a5, + 0xf09a6: 0xf09a6, + 0xf09a7: 0xf09a7, + 0xf09a8: 0xf09a8, + 0xf09a9: 0xf09a9, + 0xf09aa: 0xf09aa, + 0xf09ab: 0xf09ab, + 0xf09ac: 0xf09ac, + 0xf09ad: 0xf09ad, + 0xf09ae: 0xf09ae, + 0xf09af: 0xf09af, + 0xf09b0: 0xf09b0, + 0xf09b1: 0xf09b1, + 0xf09b2: 0xf09b2, + 0xf09b3: 0xf09b3, + 0xf09b4: 0xf09b4, + 0xf09b5: 0xf09b5, + 0xf09b6: 0xf09b6, + 0xf09b7: 0xf09b7, + 0xf09b8: 0xf09b8, + 0xf09b9: 0xf09b9, + 0xf09ba: 0xf09ba, + 0xf09bb: 0xf09bb, + 0xf09bc: 0xf09bc, + 0xf09bd: 0xf09bd, + 0xf09be: 0xf09be, + 0xf09bf: 0xf09bf, + 0xf09c0: 0xf09c0, + 0xf09c1: 0xf09c1, + 0xf09c2: 0xf09c2, + 0xf09c3: 0xf09c3, + 0xf09c4: 0xf09c4, + 0xf09c5: 0xf09c5, + 0xf09c6: 0xf09c6, + 0xf09c7: 0xf09c7, + 0xf09c8: 0xf09c8, + 0xf09c9: 0xf09c9, + 0xf09ca: 0xf09ca, + 0xf09cb: 0xf09cb, + 0xf09cc: 0xf09cc, + 0xf09cd: 0xf09cd, + 0xf09ce: 0xf09ce, + 0xf09cf: 0xf09cf, + 0xf09d0: 0xf09d0, + 0xf09d1: 0xf09d1, + 0xf09d2: 0xf09d2, + 0xf09d3: 0xf09d3, + 0xf09d4: 0xf09d4, + 0xf09d5: 0xf09d5, + 0xf09d6: 0xf09d6, + 0xf09d7: 0xf09d7, + 0xf09d8: 0xf09d8, + 0xf09d9: 0xf09d9, + 0xf09da: 0xf09da, + 0xf09db: 0xf09db, + 0xf09dc: 0xf09dc, + 0xf09dd: 0xf09dd, + 0xf09de: 0xf09de, + 0xf09df: 0xf09df, + 0xf09e0: 0xf09e0, + 0xf09e1: 0xf09e1, + 0xf09e2: 0xf09e2, + 0xf09e3: 0xf09e3, + 0xf09e4: 0xf09e4, + 0xf09e5: 0xf09e5, + 0xf09e6: 0xf09e6, + 0xf09e7: 0xf09e7, + 0xf09e8: 0xf09e8, + 0xf09e9: 0xf09e9, + 0xf09ea: 0xf09ea, + 0xf09eb: 0xf09eb, + 0xf09ec: 0xf09ec, + 0xf09ed: 0xf09ed, + 0xf09ee: 0xf09ee, + 0xf09ef: 0xf09ef, + 0xf09f0: 0xf09f0, + 0xf09f1: 0xf09f1, + 0xf09f2: 0xf09f2, + 0xf09f3: 0xf09f3, + 0xf09f4: 0xf09f4, + 0xf09f5: 0xf09f5, + 0xf09f6: 0xf09f6, + 0xf09f7: 0xf09f7, + 0xf09f8: 0xf09f8, + 0xf09f9: 0xf09f9, + 0xf09fa: 0xf09fa, + 0xf09fb: 0xf09fb, + 0xf09fc: 0xf09fc, + 0xf09fd: 0xf09fd, + 0xf09fe: 0xf09fe, + 0xf09ff: 0xf09ff, + 0xf0a00: 0xf0a00, + 0xf0a01: 0xf0a01, + 0xf0a02: 0xf0a02, + 0xf0a03: 0xf0a03, + 0xf0a04: 0xf0a04, + 0xf0a05: 0xf0a05, + 0xf0a06: 0xf0a06, + 0xf0a07: 0xf0a07, + 0xf0a08: 0xf0a08, + 0xf0a09: 0xf0a09, + 0xf0a0a: 0xf0a0a, + 0xf0a0b: 0xf0a0b, + 0xf0a0c: 0xf0a0c, + 0xf0a0d: 0xf0a0d, + 0xf0a0e: 0xf0a0e, + 0xf0a0f: 0xf0a0f, + 0xf0a10: 0xf0a10, + 0xf0a11: 0xf0a11, + 0xf0a12: 0xf0a12, + 0xf0a13: 0xf0a13, + 0xf0a14: 0xf0a14, + 0xf0a15: 0xf0a15, + 0xf0a16: 0xf0a16, + 0xf0a17: 0xf0a17, + 0xf0a18: 0xf0a18, + 0xf0a19: 0xf0a19, + 0xf0a1a: 0xf0a1a, + 0xf0a1b: 0xf0a1b, + 0xf0a1c: 0xf0a1c, + 0xf0a1d: 0xf0a1d, + 0xf0a1e: 0xf0a1e, + 0xf0a1f: 0xf0a1f, + 0xf0a20: 0xf0a20, + 0xf0a21: 0xf0a21, + 0xf0a22: 0xf0a22, + 0xf0a23: 0xf0a23, + 0xf0a24: 0xf0a24, + 0xf0a25: 0xf0a25, + 0xf0a26: 0xf0a26, + 0xf0a27: 0xf0a27, + 0xf0a28: 0xf0a28, + 0xf0a29: 0xf0a29, + 0xf0a2a: 0xf0a2a, + 0xf0a2b: 0xf0a2b, + 0xf0a2c: 0xf0a2c, + 0xf0a2d: 0xf0a2d, + 0xf0a2e: 0xf0a2e, + 0xf0a2f: 0xf0a2f, + 0xf0a30: 0xf0a30, + 0xf0a31: 0xf0a31, + 0xf0a32: 0xf0a32, + 0xf0a33: 0xf0a33, + 0xf0a34: 0xf0a34, + 0xf0a35: 0xf0a35, + 0xf0a36: 0xf0a36, + 0xf0a37: 0xf0a37, + 0xf0a38: 0xf0a38, + 0xf0a39: 0xf0a39, + 0xf0a3a: 0xf0a3a, + 0xf0a3b: 0xf0a3b, + 0xf0a3c: 0xf0a3c, + 0xf0a3d: 0xf0a3d, + 0xf0a3e: 0xf0a3e, + 0xf0a3f: 0xf0a3f, + 0xf0a40: 0xf0a40, + 0xf0a41: 0xf0a41, + 0xf0a42: 0xf0a42, + 0xf0a43: 0xf0a43, + 0xf0a44: 0xf0a44, + 0xf0a45: 0xf0a45, + 0xf0a46: 0xf0a46, + 0xf0a47: 0xf0a47, + 0xf0a48: 0xf0a48, + 0xf0a49: 0xf0a49, + 0xf0a4a: 0xf0a4a, + 0xf0a4b: 0xf0a4b, + 0xf0a4c: 0xf0a4c, + 0xf0a4d: 0xf0a4d, + 0xf0a4e: 0xf0a4e, + 0xf0a4f: 0xf0a4f, + 0xf0a50: 0xf0a50, + 0xf0a51: 0xf0a51, + 0xf0a52: 0xf0a52, + 0xf0a53: 0xf0a53, + 0xf0a54: 0xf0a54, + 0xf0a55: 0xf0a55, + 0xf0a56: 0xf0a56, + 0xf0a57: 0xf0a57, + 0xf0a58: 0xf0a58, + 0xf0a59: 0xf0a59, + 0xf0a5a: 0xf0a5a, + 0xf0a5b: 0xf0a5b, + 0xf0a5c: 0xf0a5c, + 0xf0a5d: 0xf0a5d, + 0xf0a5e: 0xf0a5e, + 0xf0a5f: 0xf0a5f, + 0xf0a60: 0xf0a60, + 0xf0a61: 0xf0a61, + 0xf0a62: 0xf0a62, + 0xf0a63: 0xf0a63, + 0xf0a64: 0xf0a64, + 0xf0a65: 0xf0a65, + 0xf0a66: 0xf0a66, + 0xf0a67: 0xf0a67, + 0xf0a68: 0xf0a68, + 0xf0a69: 0xf0a69, + 0xf0a6a: 0xf0a6a, + 0xf0a6b: 0xf0a6b, + 0xf0a6c: 0xf0a6c, + 0xf0a6d: 0xf0a6d, + 0xf0a6e: 0xf0a6e, + 0xf0a6f: 0xf0a6f, + 0xf0a70: 0xf0a70, + 0xf0a71: 0xf0a71, + 0xf0a72: 0xf0a72, + 0xf0a73: 0xf0a73, + 0xf0a74: 0xf0a74, + 0xf0a75: 0xf0a75, + 0xf0a76: 0xf0a76, + 0xf0a77: 0xf0a77, + 0xf0a78: 0xf0a78, + 0xf0a79: 0xf0a79, + 0xf0a7a: 0xf0a7a, + 0xf0a7b: 0xf0a7b, + 0xf0a7c: 0xf0a7c, + 0xf0a7d: 0xf0a7d, + 0xf0a7e: 0xf0a7e, + 0xf0a7f: 0xf0a7f, + 0xf0a80: 0xf0a80, + 0xf0a81: 0xf0a81, + 0xf0a82: 0xf0a82, + 0xf0a83: 0xf0a83, + 0xf0a84: 0xf0a84, + 0xf0a85: 0xf0a85, + 0xf0a86: 0xf0a86, + 0xf0a87: 0xf0a87, + 0xf0a88: 0xf0a88, + 0xf0a89: 0xf0a89, + 0xf0a8a: 0xf0a8a, + 0xf0a8b: 0xf0a8b, + 0xf0a8c: 0xf0a8c, + 0xf0a8d: 0xf0a8d, + 0xf0a8e: 0xf0a8e, + 0xf0a8f: 0xf0a8f, + 0xf0a90: 0xf0a90, + 0xf0a91: 0xf0a91, + 0xf0a92: 0xf0a92, + 0xf0a93: 0xf0a93, + 0xf0a94: 0xf0a94, + 0xf0a95: 0xf0a95, + 0xf0a96: 0xf0a96, + 0xf0a97: 0xf0a97, + 0xf0a98: 0xf0a98, + 0xf0a99: 0xf0a99, + 0xf0a9a: 0xf0a9a, + 0xf0a9b: 0xf0a9b, + 0xf0a9c: 0xf0a9c, + 0xf0a9d: 0xf0a9d, + 0xf0a9e: 0xf0a9e, + 0xf0a9f: 0xf0a9f, + 0xf0aa0: 0xf0aa0, + 0xf0aa1: 0xf0aa1, + 0xf0aa2: 0xf0aa2, + 0xf0aa3: 0xf0aa3, + 0xf0aa4: 0xf0aa4, + 0xf0aa5: 0xf0aa5, + 0xf0aa6: 0xf0aa6, + 0xf0aa7: 0xf0aa7, + 0xf0aa8: 0xf0aa8, + 0xf0aa9: 0xf0aa9, + 0xf0aaa: 0xf0aaa, + 0xf0aab: 0xf0aab, + 0xf0aac: 0xf0aac, + 0xf0aad: 0xf0aad, + 0xf0aae: 0xf0aae, + 0xf0aaf: 0xf0aaf, + 0xf0ab0: 0xf0ab0, + 0xf0ab1: 0xf0ab1, + 0xf0ab2: 0xf0ab2, + 0xf0ab3: 0xf0ab3, + 0xf0ab4: 0xf0ab4, + 0xf0ab5: 0xf0ab5, + 0xf0ab6: 0xf0ab6, + 0xf0ab7: 0xf0ab7, + 0xf0ab8: 0xf0ab8, + 0xf0ab9: 0xf0ab9, + 0xf0aba: 0xf0aba, + 0xf0abb: 0xf0abb, + 0xf0abc: 0xf0abc, + 0xf0abd: 0xf0abd, + 0xf0abe: 0xf0abe, + 0xf0abf: 0xf0abf, + 0xf0ac0: 0xf0ac0, + 0xf0ac1: 0xf0ac1, + 0xf0ac2: 0xf0ac2, + 0xf0ac3: 0xf0ac3, + 0xf0ac4: 0xf0ac4, + 0xf0ac5: 0xf0ac5, + 0xf0ac6: 0xf0ac6, + 0xf0ac7: 0xf0ac7, + 0xf0ac8: 0xf0ac8, + 0xf0ac9: 0xf0ac9, + 0xf0aca: 0xf0aca, + 0xf0acb: 0xf0acb, + 0xf0acc: 0xf0acc, + 0xf0acd: 0xf0acd, + 0xf0ace: 0xf0ace, + 0xf0acf: 0xf0acf, + 0xf0ad0: 0xf0ad0, + 0xf0ad1: 0xf0ad1, + 0xf0ad2: 0xf0ad2, + 0xf0ad3: 0xf0ad3, + 0xf0ad4: 0xf0ad4, + 0xf0ad5: 0xf0ad5, + 0xf0ad6: 0xf0ad6, + 0xf0ad7: 0xf0ad7, + 0xf0ad8: 0xf0ad8, + 0xf0ad9: 0xf0ad9, + 0xf0ada: 0xf0ada, + 0xf0adb: 0xf0adb, + 0xf0adc: 0xf0adc, + 0xf0add: 0xf0add, + 0xf0ade: 0xf0ade, + 0xf0adf: 0xf0adf, + 0xf0ae0: 0xf0ae0, + 0xf0ae1: 0xf0ae1, + 0xf0ae2: 0xf0ae2, + 0xf0ae3: 0xf0ae3, + 0xf0ae4: 0xf0ae4, + 0xf0ae5: 0xf0ae5, + 0xf0ae6: 0xf0ae6, + 0xf0ae7: 0xf0ae7, + 0xf0ae8: 0xf0ae8, + 0xf0ae9: 0xf0ae9, + 0xf0aea: 0xf0aea, + 0xf0aeb: 0xf0aeb, + 0xf0aec: 0xf0aec, + 0xf0aed: 0xf0aed, + 0xf0aee: 0xf0aee, + 0xf0aef: 0xf0aef, + 0xf0af0: 0xf0af0, + 0xf0af1: 0xf0af1, + 0xf0af2: 0xf0af2, + 0xf0af3: 0xf0af3, + 0xf0af4: 0xf0af4, + 0xf0af5: 0xf0af5, + 0xf0af6: 0xf0af6, + 0xf0af7: 0xf0af7, + 0xf0af8: 0xf0af8, + 0xf0af9: 0xf0af9, + 0xf0afa: 0xf0afa, + 0xf0afb: 0xf0afb, + 0xf0afc: 0xf0afc, + 0xf0afd: 0xf0afd, + 0xf0afe: 0xf0afe, + 0xf0aff: 0xf0aff, + 0xf0b00: 0xf0b00, + 0xf0b01: 0xf0b01, + 0xf0b02: 0xf0b02, + 0xf0b03: 0xf0b03, + 0xf0b04: 0xf0b04, + 0xf0b05: 0xf0b05, + 0xf0b06: 0xf0b06, + 0xf0b07: 0xf0b07, + 0xf0b08: 0xf0b08, + 0xf0b09: 0xf0b09, + 0xf0b0a: 0xf0b0a, + 0xf0b0b: 0xf0b0b, + 0xf0b0c: 0xf0b0c, + 0xf0b0d: 0xf0b0d, + 0xf0b0e: 0xf0b0e, + 0xf0b0f: 0xf0b0f, + 0xf0b10: 0xf0b10, + 0xf0b11: 0xf0b11, + 0xf0b12: 0xf0b12, + 0xf0b13: 0xf0b13, + 0xf0b14: 0xf0b14, + 0xf0b15: 0xf0b15, + 0xf0b16: 0xf0b16, + 0xf0b17: 0xf0b17, + 0xf0b18: 0xf0b18, + 0xf0b19: 0xf0b19, + 0xf0b1a: 0xf0b1a, + 0xf0b1b: 0xf0b1b, + 0xf0b1c: 0xf0b1c, + 0xf0b1d: 0xf0b1d, + 0xf0b1e: 0xf0b1e, + 0xf0b1f: 0xf0b1f, + 0xf0b20: 0xf0b20, + 0xf0b21: 0xf0b21, + 0xf0b22: 0xf0b22, + 0xf0b23: 0xf0b23, + 0xf0b24: 0xf0b24, + 0xf0b25: 0xf0b25, + 0xf0b26: 0xf0b26, + 0xf0b27: 0xf0b27, + 0xf0b28: 0xf0b28, + 0xf0b29: 0xf0b29, + 0xf0b2a: 0xf0b2a, + 0xf0b2b: 0xf0b2b, + 0xf0b2c: 0xf0b2c, + 0xf0b2d: 0xf0b2d, + 0xf0b2e: 0xf0b2e, + 0xf0b2f: 0xf0b2f, + 0xf0b30: 0xf0b30, + 0xf0b31: 0xf0b31, + 0xf0b32: 0xf0b32, + 0xf0b33: 0xf0b33, + 0xf0b34: 0xf0b34, + 0xf0b35: 0xf0b35, + 0xf0b36: 0xf0b36, + 0xf0b37: 0xf0b37, + 0xf0b38: 0xf0b38, + 0xf0b39: 0xf0b39, + 0xf0b3a: 0xf0b3a, + 0xf0b3b: 0xf0b3b, + 0xf0b3c: 0xf0b3c, + 0xf0b3d: 0xf0b3d, + 0xf0b3e: 0xf0b3e, + 0xf0b3f: 0xf0b3f, + 0xf0b40: 0xf0b40, + 0xf0b41: 0xf0b41, + 0xf0b42: 0xf0b42, + 0xf0b43: 0xf0b43, + 0xf0b44: 0xf0b44, + 0xf0b45: 0xf0b45, + 0xf0b46: 0xf0b46, + 0xf0b47: 0xf0b47, + 0xf0b48: 0xf0b48, + 0xf0b49: 0xf0b49, + 0xf0b4a: 0xf0b4a, + 0xf0b4b: 0xf0b4b, + 0xf0b4c: 0xf0b4c, + 0xf0b4d: 0xf0b4d, + 0xf0b4e: 0xf0b4e, + 0xf0b4f: 0xf0b4f, + 0xf0b50: 0xf0b50, + 0xf0b51: 0xf0b51, + 0xf0b52: 0xf0b52, + 0xf0b53: 0xf0b53, + 0xf0b54: 0xf0b54, + 0xf0b55: 0xf0b55, + 0xf0b56: 0xf0b56, + 0xf0b57: 0xf0b57, + 0xf0b58: 0xf0b58, + 0xf0b59: 0xf0b59, + 0xf0b5a: 0xf0b5a, + 0xf0b5b: 0xf0b5b, + 0xf0b5c: 0xf0b5c, + 0xf0b5d: 0xf0b5d, + 0xf0b5e: 0xf0b5e, + 0xf0b5f: 0xf0b5f, + 0xf0b60: 0xf0b60, + 0xf0b61: 0xf0b61, + 0xf0b62: 0xf0b62, + 0xf0b63: 0xf0b63, + 0xf0b64: 0xf0b64, + 0xf0b65: 0xf0b65, + 0xf0b66: 0xf0b66, + 0xf0b67: 0xf0b67, + 0xf0b68: 0xf0b68, + 0xf0b69: 0xf0b69, + 0xf0b6a: 0xf0b6a, + 0xf0b6b: 0xf0b6b, + 0xf0b6c: 0xf0b6c, + 0xf0b6d: 0xf0b6d, + 0xf0b6e: 0xf0b6e, + 0xf0b6f: 0xf0b6f, + 0xf0b70: 0xf0b70, + 0xf0b71: 0xf0b71, + 0xf0b72: 0xf0b72, + 0xf0b73: 0xf0b73, + 0xf0b74: 0xf0b74, + 0xf0b75: 0xf0b75, + 0xf0b76: 0xf0b76, + 0xf0b77: 0xf0b77, + 0xf0b78: 0xf0b78, + 0xf0b79: 0xf0b79, + 0xf0b7a: 0xf0b7a, + 0xf0b7b: 0xf0b7b, + 0xf0b7c: 0xf0b7c, + 0xf0b7d: 0xf0b7d, + 0xf0b7e: 0xf0b7e, + 0xf0b7f: 0xf0b7f, + 0xf0b80: 0xf0b80, + 0xf0b81: 0xf0b81, + 0xf0b82: 0xf0b82, + 0xf0b83: 0xf0b83, + 0xf0b84: 0xf0b84, + 0xf0b85: 0xf0b85, + 0xf0b86: 0xf0b86, + 0xf0b87: 0xf0b87, + 0xf0b88: 0xf0b88, + 0xf0b89: 0xf0b89, + 0xf0b8a: 0xf0b8a, + 0xf0b8b: 0xf0b8b, + 0xf0b8c: 0xf0b8c, + 0xf0b8d: 0xf0b8d, + 0xf0b8e: 0xf0b8e, + 0xf0b8f: 0xf0b8f, + 0xf0b90: 0xf0b90, + 0xf0b91: 0xf0b91, + 0xf0b92: 0xf0b92, + 0xf0b93: 0xf0b93, + 0xf0b94: 0xf0b94, + 0xf0b95: 0xf0b95, + 0xf0b96: 0xf0b96, + 0xf0b97: 0xf0b97, + 0xf0b98: 0xf0b98, + 0xf0b99: 0xf0b99, + 0xf0b9a: 0xf0b9a, + 0xf0b9b: 0xf0b9b, + 0xf0b9c: 0xf0b9c, + 0xf0b9d: 0xf0b9d, + 0xf0b9e: 0xf0b9e, + 0xf0b9f: 0xf0b9f, + 0xf0ba0: 0xf0ba0, + 0xf0ba1: 0xf0ba1, + 0xf0ba2: 0xf0ba2, + 0xf0ba3: 0xf0ba3, + 0xf0ba4: 0xf0ba4, + 0xf0ba5: 0xf0ba5, + 0xf0ba6: 0xf0ba6, + 0xf0ba7: 0xf0ba7, + 0xf0ba8: 0xf0ba8, + 0xf0ba9: 0xf0ba9, + 0xf0baa: 0xf0baa, + 0xf0bab: 0xf0bab, + 0xf0bac: 0xf0bac, + 0xf0bad: 0xf0bad, + 0xf0bae: 0xf0bae, + 0xf0baf: 0xf0baf, + 0xf0bb0: 0xf0bb0, + 0xf0bb1: 0xf0bb1, + 0xf0bb2: 0xf0bb2, + 0xf0bb3: 0xf0bb3, + 0xf0bb4: 0xf0bb4, + 0xf0bb5: 0xf0bb5, + 0xf0bb6: 0xf0bb6, + 0xf0bb7: 0xf0bb7, + 0xf0bb8: 0xf0bb8, + 0xf0bb9: 0xf0bb9, + 0xf0bba: 0xf0bba, + 0xf0bbb: 0xf0bbb, + 0xf0bbc: 0xf0bbc, + 0xf0bbd: 0xf0bbd, + 0xf0bbe: 0xf0bbe, + 0xf0bbf: 0xf0bbf, + 0xf0bc0: 0xf0bc0, + 0xf0bc1: 0xf0bc1, + 0xf0bc2: 0xf0bc2, + 0xf0bc3: 0xf0bc3, + 0xf0bc4: 0xf0bc4, + 0xf0bc5: 0xf0bc5, + 0xf0bc6: 0xf0bc6, + 0xf0bc7: 0xf0bc7, + 0xf0bc8: 0xf0bc8, + 0xf0bc9: 0xf0bc9, + 0xf0bca: 0xf0bca, + 0xf0bcb: 0xf0bcb, + 0xf0bcc: 0xf0bcc, + 0xf0bcd: 0xf0bcd, + 0xf0bce: 0xf0bce, + 0xf0bcf: 0xf0bcf, + 0xf0bd0: 0xf0bd0, + 0xf0bd1: 0xf0bd1, + 0xf0bd2: 0xf0bd2, + 0xf0bd3: 0xf0bd3, + 0xf0bd4: 0xf0bd4, + 0xf0bd5: 0xf0bd5, + 0xf0bd6: 0xf0bd6, + 0xf0bd7: 0xf0bd7, + 0xf0bd8: 0xf0bd8, + 0xf0bd9: 0xf0bd9, + 0xf0bda: 0xf0bda, + 0xf0bdb: 0xf0bdb, + 0xf0bdc: 0xf0bdc, + 0xf0bdd: 0xf0bdd, + 0xf0bde: 0xf0bde, + 0xf0bdf: 0xf0bdf, + 0xf0be0: 0xf0be0, + 0xf0be1: 0xf0be1, + 0xf0be2: 0xf0be2, + 0xf0be3: 0xf0be3, + 0xf0be4: 0xf0be4, + 0xf0be5: 0xf0be5, + 0xf0be6: 0xf0be6, + 0xf0be7: 0xf0be7, + 0xf0be8: 0xf0be8, + 0xf0be9: 0xf0be9, + 0xf0bea: 0xf0bea, + 0xf0beb: 0xf0beb, + 0xf0bec: 0xf0bec, + 0xf0bed: 0xf0bed, + 0xf0bee: 0xf0bee, + 0xf0bef: 0xf0bef, + 0xf0bf0: 0xf0bf0, + 0xf0bf1: 0xf0bf1, + 0xf0bf2: 0xf0bf2, + 0xf0bf3: 0xf0bf3, + 0xf0bf4: 0xf0bf4, + 0xf0bf5: 0xf0bf5, + 0xf0bf6: 0xf0bf6, + 0xf0bf7: 0xf0bf7, + 0xf0bf8: 0xf0bf8, + 0xf0bf9: 0xf0bf9, + 0xf0bfa: 0xf0bfa, + 0xf0bfb: 0xf0bfb, + 0xf0bfc: 0xf0bfc, + 0xf0bfd: 0xf0bfd, + 0xf0bfe: 0xf0bfe, + 0xf0bff: 0xf0bff, + 0xf0c00: 0xf0c00, + 0xf0c01: 0xf0c01, + 0xf0c02: 0xf0c02, + 0xf0c03: 0xf0c03, + 0xf0c04: 0xf0c04, + 0xf0c05: 0xf0c05, + 0xf0c06: 0xf0c06, + 0xf0c07: 0xf0c07, + 0xf0c08: 0xf0c08, + 0xf0c09: 0xf0c09, + 0xf0c0a: 0xf0c0a, + 0xf0c0b: 0xf0c0b, + 0xf0c0c: 0xf0c0c, + 0xf0c0d: 0xf0c0d, + 0xf0c0e: 0xf0c0e, + 0xf0c0f: 0xf0c0f, + 0xf0c10: 0xf0c10, + 0xf0c11: 0xf0c11, + 0xf0c12: 0xf0c12, + 0xf0c13: 0xf0c13, + 0xf0c14: 0xf0c14, + 0xf0c15: 0xf0c15, + 0xf0c16: 0xf0c16, + 0xf0c17: 0xf0c17, + 0xf0c18: 0xf0c18, + 0xf0c19: 0xf0c19, + 0xf0c1a: 0xf0c1a, + 0xf0c1b: 0xf0c1b, + 0xf0c1c: 0xf0c1c, + 0xf0c1d: 0xf0c1d, + 0xf0c1e: 0xf0c1e, + 0xf0c1f: 0xf0c1f, + 0xf0c20: 0xf0c20, + 0xf0c21: 0xf0c21, + 0xf0c22: 0xf0c22, + 0xf0c23: 0xf0c23, + 0xf0c24: 0xf0c24, + 0xf0c25: 0xf0c25, + 0xf0c26: 0xf0c26, + 0xf0c27: 0xf0c27, + 0xf0c28: 0xf0c28, + 0xf0c29: 0xf0c29, + 0xf0c2a: 0xf0c2a, + 0xf0c2b: 0xf0c2b, + 0xf0c2c: 0xf0c2c, + 0xf0c2d: 0xf0c2d, + 0xf0c2e: 0xf0c2e, + 0xf0c2f: 0xf0c2f, + 0xf0c30: 0xf0c30, + 0xf0c31: 0xf0c31, + 0xf0c32: 0xf0c32, + 0xf0c33: 0xf0c33, + 0xf0c34: 0xf0c34, + 0xf0c35: 0xf0c35, + 0xf0c36: 0xf0c36, + 0xf0c37: 0xf0c37, + 0xf0c38: 0xf0c38, + 0xf0c39: 0xf0c39, + 0xf0c3a: 0xf0c3a, + 0xf0c3b: 0xf0c3b, + 0xf0c3c: 0xf0c3c, + 0xf0c3d: 0xf0c3d, + 0xf0c3e: 0xf0c3e, + 0xf0c3f: 0xf0c3f, + 0xf0c40: 0xf0c40, + 0xf0c41: 0xf0c41, + 0xf0c42: 0xf0c42, + 0xf0c43: 0xf0c43, + 0xf0c44: 0xf0c44, + 0xf0c45: 0xf0c45, + 0xf0c46: 0xf0c46, + 0xf0c47: 0xf0c47, + 0xf0c48: 0xf0c48, + 0xf0c49: 0xf0c49, + 0xf0c4a: 0xf0c4a, + 0xf0c4b: 0xf0c4b, + 0xf0c4c: 0xf0c4c, + 0xf0c4d: 0xf0c4d, + 0xf0c4e: 0xf0c4e, + 0xf0c4f: 0xf0c4f, + 0xf0c50: 0xf0c50, + 0xf0c51: 0xf0c51, + 0xf0c52: 0xf0c52, + 0xf0c53: 0xf0c53, + 0xf0c54: 0xf0c54, + 0xf0c55: 0xf0c55, + 0xf0c56: 0xf0c56, + 0xf0c57: 0xf0c57, + 0xf0c58: 0xf0c58, + 0xf0c59: 0xf0c59, + 0xf0c5a: 0xf0c5a, + 0xf0c5b: 0xf0c5b, + 0xf0c5c: 0xf0c5c, + 0xf0c5d: 0xf0c5d, + 0xf0c5e: 0xf0c5e, + 0xf0c5f: 0xf0c5f, + 0xf0c60: 0xf0c60, + 0xf0c61: 0xf0c61, + 0xf0c62: 0xf0c62, + 0xf0c63: 0xf0c63, + 0xf0c64: 0xf0c64, + 0xf0c65: 0xf0c65, + 0xf0c66: 0xf0c66, + 0xf0c67: 0xf0c67, + 0xf0c68: 0xf0c68, + 0xf0c69: 0xf0c69, + 0xf0c6a: 0xf0c6a, + 0xf0c6b: 0xf0c6b, + 0xf0c6c: 0xf0c6c, + 0xf0c6d: 0xf0c6d, + 0xf0c6e: 0xf0c6e, + 0xf0c6f: 0xf0c6f, + 0xf0c70: 0xf0c70, + 0xf0c71: 0xf0c71, + 0xf0c72: 0xf0c72, + 0xf0c73: 0xf0c73, + 0xf0c74: 0xf0c74, + 0xf0c75: 0xf0c75, + 0xf0c76: 0xf0c76, + 0xf0c77: 0xf0c77, + 0xf0c78: 0xf0c78, + 0xf0c79: 0xf0c79, + 0xf0c7a: 0xf0c7a, + 0xf0c7b: 0xf0c7b, + 0xf0c7c: 0xf0c7c, + 0xf0c7d: 0xf0c7d, + 0xf0c7e: 0xf0c7e, + 0xf0c7f: 0xf0c7f, + 0xf0c80: 0xf0c80, + 0xf0c81: 0xf0c81, + 0xf0c82: 0xf0c82, + 0xf0c83: 0xf0c83, + 0xf0c84: 0xf0c84, + 0xf0c85: 0xf0c85, + 0xf0c86: 0xf0c86, + 0xf0c87: 0xf0c87, + 0xf0c88: 0xf0c88, + 0xf0c89: 0xf0c89, + 0xf0c8a: 0xf0c8a, + 0xf0c8b: 0xf0c8b, + 0xf0c8c: 0xf0c8c, + 0xf0c8d: 0xf0c8d, + 0xf0c8e: 0xf0c8e, + 0xf0c8f: 0xf0c8f, + 0xf0c90: 0xf0c90, + 0xf0c91: 0xf0c91, + 0xf0c92: 0xf0c92, + 0xf0c93: 0xf0c93, + 0xf0c94: 0xf0c94, + 0xf0c95: 0xf0c95, + 0xf0c96: 0xf0c96, + 0xf0c97: 0xf0c97, + 0xf0c98: 0xf0c98, + 0xf0c99: 0xf0c99, + 0xf0c9a: 0xf0c9a, + 0xf0c9b: 0xf0c9b, + 0xf0c9c: 0xf0c9c, + 0xf0c9d: 0xf0c9d, + 0xf0c9e: 0xf0c9e, + 0xf0c9f: 0xf0c9f, + 0xf0ca0: 0xf0ca0, + 0xf0ca1: 0xf0ca1, + 0xf0ca2: 0xf0ca2, + 0xf0ca3: 0xf0ca3, + 0xf0ca4: 0xf0ca4, + 0xf0ca5: 0xf0ca5, + 0xf0ca6: 0xf0ca6, + 0xf0ca7: 0xf0ca7, + 0xf0ca8: 0xf0ca8, + 0xf0ca9: 0xf0ca9, + 0xf0caa: 0xf0caa, + 0xf0cab: 0xf0cab, + 0xf0cac: 0xf0cac, + 0xf0cad: 0xf0cad, + 0xf0cae: 0xf0cae, + 0xf0caf: 0xf0caf, + 0xf0cb0: 0xf0cb0, + 0xf0cb1: 0xf0cb1, + 0xf0cb2: 0xf0cb2, + 0xf0cb3: 0xf0cb3, + 0xf0cb4: 0xf0cb4, + 0xf0cb5: 0xf0cb5, + 0xf0cb6: 0xf0cb6, + 0xf0cb7: 0xf0cb7, + 0xf0cb8: 0xf0cb8, + 0xf0cb9: 0xf0cb9, + 0xf0cba: 0xf0cba, + 0xf0cbb: 0xf0cbb, + 0xf0cbc: 0xf0cbc, + 0xf0cbd: 0xf0cbd, + 0xf0cbe: 0xf0cbe, + 0xf0cbf: 0xf0cbf, + 0xf0cc0: 0xf0cc0, + 0xf0cc1: 0xf0cc1, + 0xf0cc2: 0xf0cc2, + 0xf0cc3: 0xf0cc3, + 0xf0cc4: 0xf0cc4, + 0xf0cc5: 0xf0cc5, + 0xf0cc6: 0xf0cc6, + 0xf0cc7: 0xf0cc7, + 0xf0cc8: 0xf0cc8, + 0xf0cc9: 0xf0cc9, + 0xf0cca: 0xf0cca, + 0xf0ccb: 0xf0ccb, + 0xf0ccc: 0xf0ccc, + 0xf0ccd: 0xf0ccd, + 0xf0cce: 0xf0cce, + 0xf0ccf: 0xf0ccf, + 0xf0cd0: 0xf0cd0, + 0xf0cd1: 0xf0cd1, + 0xf0cd2: 0xf0cd2, + 0xf0cd3: 0xf0cd3, + 0xf0cd4: 0xf0cd4, + 0xf0cd5: 0xf0cd5, + 0xf0cd6: 0xf0cd6, + 0xf0cd7: 0xf0cd7, + 0xf0cd8: 0xf0cd8, + 0xf0cd9: 0xf0cd9, + 0xf0cda: 0xf0cda, + 0xf0cdb: 0xf0cdb, + 0xf0cdc: 0xf0cdc, + 0xf0cdd: 0xf0cdd, + 0xf0cde: 0xf0cde, + 0xf0cdf: 0xf0cdf, + 0xf0ce0: 0xf0ce0, + 0xf0ce1: 0xf0ce1, + 0xf0ce2: 0xf0ce2, + 0xf0ce3: 0xf0ce3, + 0xf0ce4: 0xf0ce4, + 0xf0ce5: 0xf0ce5, + 0xf0ce6: 0xf0ce6, + 0xf0ce7: 0xf0ce7, + 0xf0ce8: 0xf0ce8, + 0xf0ce9: 0xf0ce9, + 0xf0cea: 0xf0cea, + 0xf0ceb: 0xf0ceb, + 0xf0cec: 0xf0cec, + 0xf0ced: 0xf0ced, + 0xf0cee: 0xf0cee, + 0xf0cef: 0xf0cef, + 0xf0cf0: 0xf0cf0, + 0xf0cf1: 0xf0cf1, + 0xf0cf2: 0xf0cf2, + 0xf0cf3: 0xf0cf3, + 0xf0cf4: 0xf0cf4, + 0xf0cf5: 0xf0cf5, + 0xf0cf6: 0xf0cf6, + 0xf0cf7: 0xf0cf7, + 0xf0cf8: 0xf0cf8, + 0xf0cf9: 0xf0cf9, + 0xf0cfa: 0xf0cfa, + 0xf0cfb: 0xf0cfb, + 0xf0cfc: 0xf0cfc, + 0xf0cfd: 0xf0cfd, + 0xf0cfe: 0xf0cfe, + 0xf0cff: 0xf0cff, + 0xf0d00: 0xf0d00, + 0xf0d01: 0xf0d01, + 0xf0d02: 0xf0d02, + 0xf0d03: 0xf0d03, + 0xf0d04: 0xf0d04, + 0xf0d05: 0xf0d05, + 0xf0d06: 0xf0d06, + 0xf0d07: 0xf0d07, + 0xf0d08: 0xf0d08, + 0xf0d09: 0xf0d09, + 0xf0d0a: 0xf0d0a, + 0xf0d0b: 0xf0d0b, + 0xf0d0c: 0xf0d0c, + 0xf0d0d: 0xf0d0d, + 0xf0d0e: 0xf0d0e, + 0xf0d0f: 0xf0d0f, + 0xf0d10: 0xf0d10, + 0xf0d11: 0xf0d11, + 0xf0d12: 0xf0d12, + 0xf0d13: 0xf0d13, + 0xf0d14: 0xf0d14, + 0xf0d15: 0xf0d15, + 0xf0d16: 0xf0d16, + 0xf0d17: 0xf0d17, + 0xf0d18: 0xf0d18, + 0xf0d19: 0xf0d19, + 0xf0d1a: 0xf0d1a, + 0xf0d1b: 0xf0d1b, + 0xf0d1c: 0xf0d1c, + 0xf0d1d: 0xf0d1d, + 0xf0d1e: 0xf0d1e, + 0xf0d1f: 0xf0d1f, + 0xf0d20: 0xf0d20, + 0xf0d21: 0xf0d21, + 0xf0d22: 0xf0d22, + 0xf0d23: 0xf0d23, + 0xf0d24: 0xf0d24, + 0xf0d25: 0xf0d25, + 0xf0d26: 0xf0d26, + 0xf0d27: 0xf0d27, + 0xf0d28: 0xf0d28, + 0xf0d29: 0xf0d29, + 0xf0d2a: 0xf0d2a, + 0xf0d2b: 0xf0d2b, + 0xf0d2c: 0xf0d2c, + 0xf0d2d: 0xf0d2d, + 0xf0d2e: 0xf0d2e, + 0xf0d2f: 0xf0d2f, + 0xf0d30: 0xf0d30, + 0xf0d31: 0xf0d31, + 0xf0d32: 0xf0d32, + 0xf0d33: 0xf0d33, + 0xf0d34: 0xf0d34, + 0xf0d35: 0xf0d35, + 0xf0d36: 0xf0d36, + 0xf0d37: 0xf0d37, + 0xf0d38: 0xf0d38, + 0xf0d39: 0xf0d39, + 0xf0d3a: 0xf0d3a, + 0xf0d3b: 0xf0d3b, + 0xf0d3c: 0xf0d3c, + 0xf0d3d: 0xf0d3d, + 0xf0d3e: 0xf0d3e, + 0xf0d3f: 0xf0d3f, + 0xf0d40: 0xf0d40, + 0xf0d41: 0xf0d41, + 0xf0d42: 0xf0d42, + 0xf0d43: 0xf0d43, + 0xf0d44: 0xf0d44, + 0xf0d45: 0xf0d45, + 0xf0d46: 0xf0d46, + 0xf0d47: 0xf0d47, + 0xf0d48: 0xf0d48, + 0xf0d49: 0xf0d49, + 0xf0d4a: 0xf0d4a, + 0xf0d4b: 0xf0d4b, + 0xf0d4c: 0xf0d4c, + 0xf0d4d: 0xf0d4d, + 0xf0d4e: 0xf0d4e, + 0xf0d4f: 0xf0d4f, + 0xf0d50: 0xf0d50, + 0xf0d51: 0xf0d51, + 0xf0d52: 0xf0d52, + 0xf0d53: 0xf0d53, + 0xf0d54: 0xf0d54, + 0xf0d55: 0xf0d55, + 0xf0d56: 0xf0d56, + 0xf0d57: 0xf0d57, + 0xf0d58: 0xf0d58, + 0xf0d59: 0xf0d59, + 0xf0d5a: 0xf0d5a, + 0xf0d5b: 0xf0d5b, + 0xf0d5c: 0xf0d5c, + 0xf0d5d: 0xf0d5d, + 0xf0d5e: 0xf0d5e, + 0xf0d5f: 0xf0d5f, + 0xf0d60: 0xf0d60, + 0xf0d61: 0xf0d61, + 0xf0d62: 0xf0d62, + 0xf0d63: 0xf0d63, + 0xf0d64: 0xf0d64, + 0xf0d65: 0xf0d65, + 0xf0d66: 0xf0d66, + 0xf0d67: 0xf0d67, + 0xf0d68: 0xf0d68, + 0xf0d69: 0xf0d69, + 0xf0d6a: 0xf0d6a, + 0xf0d6b: 0xf0d6b, + 0xf0d6c: 0xf0d6c, + 0xf0d6d: 0xf0d6d, + 0xf0d6e: 0xf0d6e, + 0xf0d6f: 0xf0d6f, + 0xf0d70: 0xf0d70, + 0xf0d71: 0xf0d71, + 0xf0d72: 0xf0d72, + 0xf0d73: 0xf0d73, + 0xf0d74: 0xf0d74, + 0xf0d75: 0xf0d75, + 0xf0d76: 0xf0d76, + 0xf0d77: 0xf0d77, + 0xf0d78: 0xf0d78, + 0xf0d79: 0xf0d79, + 0xf0d7a: 0xf0d7a, + 0xf0d7b: 0xf0d7b, + 0xf0d7c: 0xf0d7c, + 0xf0d7d: 0xf0d7d, + 0xf0d7e: 0xf0d7e, + 0xf0d7f: 0xf0d7f, + 0xf0d80: 0xf0d80, + 0xf0d81: 0xf0d81, + 0xf0d82: 0xf0d82, + 0xf0d83: 0xf0d83, + 0xf0d84: 0xf0d84, + 0xf0d85: 0xf0d85, + 0xf0d86: 0xf0d86, + 0xf0d87: 0xf0d87, + 0xf0d88: 0xf0d88, + 0xf0d89: 0xf0d89, + 0xf0d8a: 0xf0d8a, + 0xf0d8b: 0xf0d8b, + 0xf0d8c: 0xf0d8c, + 0xf0d8d: 0xf0d8d, + 0xf0d8e: 0xf0d8e, + 0xf0d8f: 0xf0d8f, + 0xf0d90: 0xf0d90, + 0xf0d91: 0xf0d91, + 0xf0d92: 0xf0d92, + 0xf0d93: 0xf0d93, + 0xf0d94: 0xf0d94, + 0xf0d95: 0xf0d95, + 0xf0d96: 0xf0d96, + 0xf0d97: 0xf0d97, + 0xf0d98: 0xf0d98, + 0xf0d99: 0xf0d99, + 0xf0d9a: 0xf0d9a, + 0xf0d9b: 0xf0d9b, + 0xf0d9c: 0xf0d9c, + 0xf0d9d: 0xf0d9d, + 0xf0d9e: 0xf0d9e, + 0xf0d9f: 0xf0d9f, + 0xf0da0: 0xf0da0, + 0xf0da1: 0xf0da1, + 0xf0da2: 0xf0da2, + 0xf0da3: 0xf0da3, + 0xf0da4: 0xf0da4, + 0xf0da5: 0xf0da5, + 0xf0da6: 0xf0da6, + 0xf0da7: 0xf0da7, + 0xf0da8: 0xf0da8, + 0xf0da9: 0xf0da9, + 0xf0daa: 0xf0daa, + 0xf0dab: 0xf0dab, + 0xf0dac: 0xf0dac, + 0xf0dad: 0xf0dad, + 0xf0dae: 0xf0dae, + 0xf0daf: 0xf0daf, + 0xf0db0: 0xf0db0, + 0xf0db1: 0xf0db1, + 0xf0db2: 0xf0db2, + 0xf0db3: 0xf0db3, + 0xf0db4: 0xf0db4, + 0xf0db5: 0xf0db5, + 0xf0db6: 0xf0db6, + 0xf0db7: 0xf0db7, + 0xf0db8: 0xf0db8, + 0xf0db9: 0xf0db9, + 0xf0dba: 0xf0dba, + 0xf0dbb: 0xf0dbb, + 0xf0dbc: 0xf0dbc, + 0xf0dbd: 0xf0dbd, + 0xf0dbe: 0xf0dbe, + 0xf0dbf: 0xf0dbf, + 0xf0dc0: 0xf0dc0, + 0xf0dc1: 0xf0dc1, + 0xf0dc2: 0xf0dc2, + 0xf0dc3: 0xf0dc3, + 0xf0dc4: 0xf0dc4, + 0xf0dc5: 0xf0dc5, + 0xf0dc6: 0xf0dc6, + 0xf0dc7: 0xf0dc7, + 0xf0dc8: 0xf0dc8, + 0xf0dc9: 0xf0dc9, + 0xf0dca: 0xf0dca, + 0xf0dcb: 0xf0dcb, + 0xf0dcc: 0xf0dcc, + 0xf0dcd: 0xf0dcd, + 0xf0dce: 0xf0dce, + 0xf0dcf: 0xf0dcf, + 0xf0dd0: 0xf0dd0, + 0xf0dd1: 0xf0dd1, + 0xf0dd2: 0xf0dd2, + 0xf0dd3: 0xf0dd3, + 0xf0dd4: 0xf0dd4, + 0xf0dd5: 0xf0dd5, + 0xf0dd6: 0xf0dd6, + 0xf0dd7: 0xf0dd7, + 0xf0dd8: 0xf0dd8, + 0xf0dd9: 0xf0dd9, + 0xf0dda: 0xf0dda, + 0xf0ddb: 0xf0ddb, + 0xf0ddc: 0xf0ddc, + 0xf0ddd: 0xf0ddd, + 0xf0dde: 0xf0dde, + 0xf0ddf: 0xf0ddf, + 0xf0de0: 0xf0de0, + 0xf0de1: 0xf0de1, + 0xf0de2: 0xf0de2, + 0xf0de3: 0xf0de3, + 0xf0de4: 0xf0de4, + 0xf0de5: 0xf0de5, + 0xf0de6: 0xf0de6, + 0xf0de7: 0xf0de7, + 0xf0de8: 0xf0de8, + 0xf0de9: 0xf0de9, + 0xf0dea: 0xf0dea, + 0xf0deb: 0xf0deb, + 0xf0dec: 0xf0dec, + 0xf0ded: 0xf0ded, + 0xf0dee: 0xf0dee, + 0xf0def: 0xf0def, + 0xf0df0: 0xf0df0, + 0xf0df1: 0xf0df1, + 0xf0df2: 0xf0df2, + 0xf0df3: 0xf0df3, + 0xf0df4: 0xf0df4, + 0xf0df5: 0xf0df5, + 0xf0df6: 0xf0df6, + 0xf0df7: 0xf0df7, + 0xf0df8: 0xf0df8, + 0xf0df9: 0xf0df9, + 0xf0dfa: 0xf0dfa, + 0xf0dfb: 0xf0dfb, + 0xf0dfc: 0xf0dfc, + 0xf0dfd: 0xf0dfd, + 0xf0dfe: 0xf0dfe, + 0xf0dff: 0xf0dff, + 0xf0e00: 0xf0e00, + 0xf0e01: 0xf0e01, + 0xf0e02: 0xf0e02, + 0xf0e03: 0xf0e03, + 0xf0e04: 0xf0e04, + 0xf0e05: 0xf0e05, + 0xf0e06: 0xf0e06, + 0xf0e07: 0xf0e07, + 0xf0e08: 0xf0e08, + 0xf0e09: 0xf0e09, + 0xf0e0a: 0xf0e0a, + 0xf0e0b: 0xf0e0b, + 0xf0e0c: 0xf0e0c, + 0xf0e0d: 0xf0e0d, + 0xf0e0e: 0xf0e0e, + 0xf0e0f: 0xf0e0f, + 0xf0e10: 0xf0e10, + 0xf0e11: 0xf0e11, + 0xf0e12: 0xf0e12, + 0xf0e13: 0xf0e13, + 0xf0e14: 0xf0e14, + 0xf0e15: 0xf0e15, + 0xf0e16: 0xf0e16, + 0xf0e17: 0xf0e17, + 0xf0e18: 0xf0e18, + 0xf0e19: 0xf0e19, + 0xf0e1a: 0xf0e1a, + 0xf0e1b: 0xf0e1b, + 0xf0e1c: 0xf0e1c, + 0xf0e1d: 0xf0e1d, + 0xf0e1e: 0xf0e1e, + 0xf0e1f: 0xf0e1f, + 0xf0e20: 0xf0e20, + 0xf0e21: 0xf0e21, + 0xf0e22: 0xf0e22, + 0xf0e23: 0xf0e23, + 0xf0e24: 0xf0e24, + 0xf0e25: 0xf0e25, + 0xf0e26: 0xf0e26, + 0xf0e27: 0xf0e27, + 0xf0e28: 0xf0e28, + 0xf0e29: 0xf0e29, + 0xf0e2a: 0xf0e2a, + 0xf0e2b: 0xf0e2b, + 0xf0e2c: 0xf0e2c, + 0xf0e2d: 0xf0e2d, + 0xf0e2e: 0xf0e2e, + 0xf0e2f: 0xf0e2f, + 0xf0e30: 0xf0e30, + 0xf0e31: 0xf0e31, + 0xf0e32: 0xf0e32, + 0xf0e33: 0xf0e33, + 0xf0e34: 0xf0e34, + 0xf0e35: 0xf0e35, + 0xf0e36: 0xf0e36, + 0xf0e37: 0xf0e37, + 0xf0e38: 0xf0e38, + 0xf0e39: 0xf0e39, + 0xf0e3a: 0xf0e3a, + 0xf0e3b: 0xf0e3b, + 0xf0e3c: 0xf0e3c, + 0xf0e3d: 0xf0e3d, + 0xf0e3e: 0xf0e3e, + 0xf0e3f: 0xf0e3f, + 0xf0e40: 0xf0e40, + 0xf0e41: 0xf0e41, + 0xf0e42: 0xf0e42, + 0xf0e43: 0xf0e43, + 0xf0e44: 0xf0e44, + 0xf0e45: 0xf0e45, + 0xf0e46: 0xf0e46, + 0xf0e47: 0xf0e47, + 0xf0e48: 0xf0e48, + 0xf0e49: 0xf0e49, + 0xf0e4a: 0xf0e4a, + 0xf0e4b: 0xf0e4b, + 0xf0e4c: 0xf0e4c, + 0xf0e4d: 0xf0e4d, + 0xf0e4e: 0xf0e4e, + 0xf0e4f: 0xf0e4f, + 0xf0e50: 0xf0e50, + 0xf0e51: 0xf0e51, + 0xf0e52: 0xf0e52, + 0xf0e53: 0xf0e53, + 0xf0e54: 0xf0e54, + 0xf0e55: 0xf0e55, + 0xf0e56: 0xf0e56, + 0xf0e57: 0xf0e57, + 0xf0e58: 0xf0e58, + 0xf0e59: 0xf0e59, + 0xf0e5a: 0xf0e5a, + 0xf0e5b: 0xf0e5b, + 0xf0e5c: 0xf0e5c, + 0xf0e5d: 0xf0e5d, + 0xf0e5e: 0xf0e5e, + 0xf0e5f: 0xf0e5f, + 0xf0e60: 0xf0e60, + 0xf0e61: 0xf0e61, + 0xf0e62: 0xf0e62, + 0xf0e63: 0xf0e63, + 0xf0e64: 0xf0e64, + 0xf0e65: 0xf0e65, + 0xf0e66: 0xf0e66, + 0xf0e67: 0xf0e67, + 0xf0e68: 0xf0e68, + 0xf0e69: 0xf0e69, + 0xf0e6a: 0xf0e6a, + 0xf0e6b: 0xf0e6b, + 0xf0e6c: 0xf0e6c, + 0xf0e6d: 0xf0e6d, + 0xf0e6e: 0xf0e6e, + 0xf0e6f: 0xf0e6f, + 0xf0e70: 0xf0e70, + 0xf0e71: 0xf0e71, + 0xf0e72: 0xf0e72, + 0xf0e73: 0xf0e73, + 0xf0e74: 0xf0e74, + 0xf0e75: 0xf0e75, + 0xf0e76: 0xf0e76, + 0xf0e77: 0xf0e77, + 0xf0e78: 0xf0e78, + 0xf0e79: 0xf0e79, + 0xf0e7a: 0xf0e7a, + 0xf0e7b: 0xf0e7b, + 0xf0e7c: 0xf0e7c, + 0xf0e7d: 0xf0e7d, + 0xf0e7e: 0xf0e7e, + 0xf0e7f: 0xf0e7f, + 0xf0e80: 0xf0e80, + 0xf0e81: 0xf0e81, + 0xf0e82: 0xf0e82, + 0xf0e83: 0xf0e83, + 0xf0e84: 0xf0e84, + 0xf0e85: 0xf0e85, + 0xf0e86: 0xf0e86, + 0xf0e87: 0xf0e87, + 0xf0e88: 0xf0e88, + 0xf0e89: 0xf0e89, + 0xf0e8a: 0xf0e8a, + 0xf0e8b: 0xf0e8b, + 0xf0e8c: 0xf0e8c, + 0xf0e8d: 0xf0e8d, + 0xf0e8e: 0xf0e8e, + 0xf0e8f: 0xf0e8f, + 0xf0e90: 0xf0e90, + 0xf0e91: 0xf0e91, + 0xf0e92: 0xf0e92, + 0xf0e93: 0xf0e93, + 0xf0e94: 0xf0e94, + 0xf0e95: 0xf0e95, + 0xf0e96: 0xf0e96, + 0xf0e97: 0xf0e97, + 0xf0e98: 0xf0e98, + 0xf0e99: 0xf0e99, + 0xf0e9a: 0xf0e9a, + 0xf0e9b: 0xf0e9b, + 0xf0e9c: 0xf0e9c, + 0xf0e9d: 0xf0e9d, + 0xf0e9e: 0xf0e9e, + 0xf0e9f: 0xf0e9f, + 0xf0ea0: 0xf0ea0, + 0xf0ea1: 0xf0ea1, + 0xf0ea2: 0xf0ea2, + 0xf0ea3: 0xf0ea3, + 0xf0ea4: 0xf0ea4, + 0xf0ea5: 0xf0ea5, + 0xf0ea6: 0xf0ea6, + 0xf0ea7: 0xf0ea7, + 0xf0ea8: 0xf0ea8, + 0xf0ea9: 0xf0ea9, + 0xf0eaa: 0xf0eaa, + 0xf0eab: 0xf0eab, + 0xf0eac: 0xf0eac, + 0xf0ead: 0xf0ead, + 0xf0eae: 0xf0eae, + 0xf0eaf: 0xf0eaf, + 0xf0eb0: 0xf0eb0, + 0xf0eb1: 0xf0eb1, + 0xf0eb2: 0xf0eb2, + 0xf0eb3: 0xf0eb3, + 0xf0eb4: 0xf0eb4, + 0xf0eb5: 0xf0eb5, + 0xf0eb6: 0xf0eb6, + 0xf0eb7: 0xf0eb7, + 0xf0eb8: 0xf0eb8, + 0xf0eb9: 0xf0eb9, + 0xf0eba: 0xf0eba, + 0xf0ebb: 0xf0ebb, + 0xf0ebc: 0xf0ebc, + 0xf0ebd: 0xf0ebd, + 0xf0ebe: 0xf0ebe, + 0xf0ebf: 0xf0ebf, + 0xf0ec0: 0xf0ec0, + 0xf0ec1: 0xf0ec1, + 0xf0ec2: 0xf0ec2, + 0xf0ec3: 0xf0ec3, + 0xf0ec4: 0xf0ec4, + 0xf0ec5: 0xf0ec5, + 0xf0ec6: 0xf0ec6, + 0xf0ec7: 0xf0ec7, + 0xf0ec8: 0xf0ec8, + 0xf0ec9: 0xf0ec9, + 0xf0eca: 0xf0eca, + 0xf0ecb: 0xf0ecb, + 0xf0ecc: 0xf0ecc, + 0xf0ecd: 0xf0ecd, + 0xf0ece: 0xf0ece, + 0xf0ecf: 0xf0ecf, + 0xf0ed0: 0xf0ed0, + 0xf0ed1: 0xf0ed1, + 0xf0ed2: 0xf0ed2, + 0xf0ed3: 0xf0ed3, + 0xf0ed4: 0xf0ed4, + 0xf0ed5: 0xf0ed5, + 0xf0ed6: 0xf0ed6, + 0xf0ed7: 0xf0ed7, + 0xf0ed8: 0xf0ed8, + 0xf0ed9: 0xf0ed9, + 0xf0eda: 0xf0eda, + 0xf0edb: 0xf0edb, + 0xf0edc: 0xf0edc, + 0xf0edd: 0xf0edd, + 0xf0ede: 0xf0ede, + 0xf0edf: 0xf0edf, + 0xf0ee0: 0xf0ee0, + 0xf0ee1: 0xf0ee1, + 0xf0ee2: 0xf0ee2, + 0xf0ee3: 0xf0ee3, + 0xf0ee4: 0xf0ee4, + 0xf0ee5: 0xf0ee5, + 0xf0ee6: 0xf0ee6, + 0xf0ee7: 0xf0ee7, + 0xf0ee8: 0xf0ee8, + 0xf0ee9: 0xf0ee9, + 0xf0eea: 0xf0eea, + 0xf0eeb: 0xf0eeb, + 0xf0eec: 0xf0eec, + 0xf0eed: 0xf0eed, + 0xf0eee: 0xf0eee, + 0xf0eef: 0xf0eef, + 0xf0ef0: 0xf0ef0, + 0xf0ef1: 0xf0ef1, + 0xf0ef2: 0xf0ef2, + 0xf0ef3: 0xf0ef3, + 0xf0ef4: 0xf0ef4, + 0xf0ef5: 0xf0ef5, + 0xf0ef6: 0xf0ef6, + 0xf0ef7: 0xf0ef7, + 0xf0ef8: 0xf0ef8, + 0xf0ef9: 0xf0ef9, + 0xf0efa: 0xf0efa, + 0xf0efb: 0xf0efb, + 0xf0efc: 0xf0efc, + 0xf0efd: 0xf0efd, + 0xf0efe: 0xf0efe, + 0xf0eff: 0xf0eff, + 0xf0f00: 0xf0f00, + 0xf0f01: 0xf0f01, + 0xf0f02: 0xf0f02, + 0xf0f03: 0xf0f03, + 0xf0f04: 0xf0f04, + 0xf0f05: 0xf0f05, + 0xf0f06: 0xf0f06, + 0xf0f07: 0xf0f07, + 0xf0f08: 0xf0f08, + 0xf0f09: 0xf0f09, + 0xf0f0a: 0xf0f0a, + 0xf0f0b: 0xf0f0b, + 0xf0f0c: 0xf0f0c, + 0xf0f0d: 0xf0f0d, + 0xf0f0e: 0xf0f0e, + 0xf0f0f: 0xf0f0f, + 0xf0f10: 0xf0f10, + 0xf0f11: 0xf0f11, + 0xf0f12: 0xf0f12, + 0xf0f13: 0xf0f13, + 0xf0f14: 0xf0f14, + 0xf0f15: 0xf0f15, + 0xf0f16: 0xf0f16, + 0xf0f17: 0xf0f17, + 0xf0f18: 0xf0f18, + 0xf0f19: 0xf0f19, + 0xf0f1a: 0xf0f1a, + 0xf0f1b: 0xf0f1b, + 0xf0f1c: 0xf0f1c, + 0xf0f1d: 0xf0f1d, + 0xf0f1e: 0xf0f1e, + 0xf0f1f: 0xf0f1f, + 0xf0f20: 0xf0f20, + 0xf0f21: 0xf0f21, + 0xf0f22: 0xf0f22, + 0xf0f23: 0xf0f23, + 0xf0f24: 0xf0f24, + 0xf0f25: 0xf0f25, + 0xf0f26: 0xf0f26, + 0xf0f27: 0xf0f27, + 0xf0f28: 0xf0f28, + 0xf0f29: 0xf0f29, + 0xf0f2a: 0xf0f2a, + 0xf0f2b: 0xf0f2b, + 0xf0f2c: 0xf0f2c, + 0xf0f2d: 0xf0f2d, + 0xf0f2e: 0xf0f2e, + 0xf0f2f: 0xf0f2f, + 0xf0f30: 0xf0f30, + 0xf0f31: 0xf0f31, + 0xf0f32: 0xf0f32, + 0xf0f33: 0xf0f33, + 0xf0f34: 0xf0f34, + 0xf0f35: 0xf0f35, + 0xf0f36: 0xf0f36, + 0xf0f37: 0xf0f37, + 0xf0f38: 0xf0f38, + 0xf0f39: 0xf0f39, + 0xf0f3a: 0xf0f3a, + 0xf0f3b: 0xf0f3b, + 0xf0f3c: 0xf0f3c, + 0xf0f3d: 0xf0f3d, + 0xf0f3e: 0xf0f3e, + 0xf0f3f: 0xf0f3f, + 0xf0f40: 0xf0f40, + 0xf0f41: 0xf0f41, + 0xf0f42: 0xf0f42, + 0xf0f43: 0xf0f43, + 0xf0f44: 0xf0f44, + 0xf0f45: 0xf0f45, + 0xf0f46: 0xf0f46, + 0xf0f47: 0xf0f47, + 0xf0f48: 0xf0f48, + 0xf0f49: 0xf0f49, + 0xf0f4a: 0xf0f4a, + 0xf0f4b: 0xf0f4b, + 0xf0f4c: 0xf0f4c, + 0xf0f4d: 0xf0f4d, + 0xf0f4e: 0xf0f4e, + 0xf0f4f: 0xf0f4f, + 0xf0f50: 0xf0f50, + 0xf0f51: 0xf0f51, + 0xf0f52: 0xf0f52, + 0xf0f53: 0xf0f53, + 0xf0f54: 0xf0f54, + 0xf0f55: 0xf0f55, + 0xf0f56: 0xf0f56, + 0xf0f57: 0xf0f57, + 0xf0f58: 0xf0f58, + 0xf0f59: 0xf0f59, + 0xf0f5a: 0xf0f5a, + 0xf0f5b: 0xf0f5b, + 0xf0f5c: 0xf0f5c, + 0xf0f5d: 0xf0f5d, + 0xf0f5e: 0xf0f5e, + 0xf0f5f: 0xf0f5f, + 0xf0f60: 0xf0f60, + 0xf0f61: 0xf0f61, + 0xf0f62: 0xf0f62, + 0xf0f63: 0xf0f63, + 0xf0f64: 0xf0f64, + 0xf0f65: 0xf0f65, + 0xf0f66: 0xf0f66, + 0xf0f67: 0xf0f67, + 0xf0f68: 0xf0f68, + 0xf0f69: 0xf0f69, + 0xf0f6a: 0xf0f6a, + 0xf0f6b: 0xf0f6b, + 0xf0f6c: 0xf0f6c, + 0xf0f6d: 0xf0f6d, + 0xf0f6e: 0xf0f6e, + 0xf0f6f: 0xf0f6f, + 0xf0f70: 0xf0f70, + 0xf0f71: 0xf0f71, + 0xf0f72: 0xf0f72, + 0xf0f73: 0xf0f73, + 0xf0f74: 0xf0f74, + 0xf0f75: 0xf0f75, + 0xf0f76: 0xf0f76, + 0xf0f77: 0xf0f77, + 0xf0f78: 0xf0f78, + 0xf0f79: 0xf0f79, + 0xf0f7a: 0xf0f7a, + 0xf0f7b: 0xf0f7b, + 0xf0f7c: 0xf0f7c, + 0xf0f7d: 0xf0f7d, + 0xf0f7e: 0xf0f7e, + 0xf0f7f: 0xf0f7f, + 0xf0f80: 0xf0f80, + 0xf0f81: 0xf0f81, + 0xf0f82: 0xf0f82, + 0xf0f83: 0xf0f83, + 0xf0f84: 0xf0f84, + 0xf0f85: 0xf0f85, + 0xf0f86: 0xf0f86, + 0xf0f87: 0xf0f87, + 0xf0f88: 0xf0f88, + 0xf0f89: 0xf0f89, + 0xf0f8a: 0xf0f8a, + 0xf0f8b: 0xf0f8b, + 0xf0f8c: 0xf0f8c, + 0xf0f8d: 0xf0f8d, + 0xf0f8e: 0xf0f8e, + 0xf0f8f: 0xf0f8f, + 0xf0f90: 0xf0f90, + 0xf0f91: 0xf0f91, + 0xf0f92: 0xf0f92, + 0xf0f93: 0xf0f93, + 0xf0f94: 0xf0f94, + 0xf0f95: 0xf0f95, + 0xf0f96: 0xf0f96, + 0xf0f97: 0xf0f97, + 0xf0f98: 0xf0f98, + 0xf0f99: 0xf0f99, + 0xf0f9a: 0xf0f9a, + 0xf0f9b: 0xf0f9b, + 0xf0f9c: 0xf0f9c, + 0xf0f9d: 0xf0f9d, + 0xf0f9e: 0xf0f9e, + 0xf0f9f: 0xf0f9f, + 0xf0fa0: 0xf0fa0, + 0xf0fa1: 0xf0fa1, + 0xf0fa2: 0xf0fa2, + 0xf0fa3: 0xf0fa3, + 0xf0fa4: 0xf0fa4, + 0xf0fa5: 0xf0fa5, + 0xf0fa6: 0xf0fa6, + 0xf0fa7: 0xf0fa7, + 0xf0fa8: 0xf0fa8, + 0xf0fa9: 0xf0fa9, + 0xf0faa: 0xf0faa, + 0xf0fab: 0xf0fab, + 0xf0fac: 0xf0fac, + 0xf0fad: 0xf0fad, + 0xf0fae: 0xf0fae, + 0xf0faf: 0xf0faf, + 0xf0fb0: 0xf0fb0, + 0xf0fb1: 0xf0fb1, + 0xf0fb2: 0xf0fb2, + 0xf0fb3: 0xf0fb3, + 0xf0fb4: 0xf0fb4, + 0xf0fb5: 0xf0fb5, + 0xf0fb6: 0xf0fb6, + 0xf0fb7: 0xf0fb7, + 0xf0fb8: 0xf0fb8, + 0xf0fb9: 0xf0fb9, + 0xf0fba: 0xf0fba, + 0xf0fbb: 0xf0fbb, + 0xf0fbc: 0xf0fbc, + 0xf0fbd: 0xf0fbd, + 0xf0fbe: 0xf0fbe, + 0xf0fbf: 0xf0fbf, + 0xf0fc0: 0xf0fc0, + 0xf0fc1: 0xf0fc1, + 0xf0fc2: 0xf0fc2, + 0xf0fc3: 0xf0fc3, + 0xf0fc4: 0xf0fc4, + 0xf0fc5: 0xf0fc5, + 0xf0fc6: 0xf0fc6, + 0xf0fc7: 0xf0fc7, + 0xf0fc8: 0xf0fc8, + 0xf0fc9: 0xf0fc9, + 0xf0fca: 0xf0fca, + 0xf0fcb: 0xf0fcb, + 0xf0fcc: 0xf0fcc, + 0xf0fcd: 0xf0fcd, + 0xf0fce: 0xf0fce, + 0xf0fcf: 0xf0fcf, + 0xf0fd0: 0xf0fd0, + 0xf0fd1: 0xf0fd1, + 0xf0fd2: 0xf0fd2, + 0xf0fd3: 0xf0fd3, + 0xf0fd4: 0xf0fd4, + 0xf0fd5: 0xf0fd5, + 0xf0fd6: 0xf0fd6, + 0xf0fd7: 0xf0fd7, + 0xf0fd8: 0xf0fd8, + 0xf0fd9: 0xf0fd9, + 0xf0fda: 0xf0fda, + 0xf0fdb: 0xf0fdb, + 0xf0fdc: 0xf0fdc, + 0xf0fdd: 0xf0fdd, + 0xf0fde: 0xf0fde, + 0xf0fdf: 0xf0fdf, + 0xf0fe0: 0xf0fe0, + 0xf0fe1: 0xf0fe1, + 0xf0fe2: 0xf0fe2, + 0xf0fe3: 0xf0fe3, + 0xf0fe4: 0xf0fe4, + 0xf0fe5: 0xf0fe5, + 0xf0fe6: 0xf0fe6, + 0xf0fe7: 0xf0fe7, + 0xf0fe8: 0xf0fe8, + 0xf0fe9: 0xf0fe9, + 0xf0fea: 0xf0fea, + 0xf0feb: 0xf0feb, + 0xf0fec: 0xf0fec, + 0xf0fed: 0xf0fed, + 0xf0fee: 0xf0fee, + 0xf0fef: 0xf0fef, + 0xf0ff0: 0xf0ff0, + 0xf0ff1: 0xf0ff1, + 0xf0ff2: 0xf0ff2, + 0xf0ff3: 0xf0ff3, + 0xf0ff4: 0xf0ff4, + 0xf0ff5: 0xf0ff5, + 0xf0ff6: 0xf0ff6, + 0xf0ff7: 0xf0ff7, + 0xf0ff8: 0xf0ff8, + 0xf0ff9: 0xf0ff9, + 0xf0ffa: 0xf0ffa, + 0xf0ffb: 0xf0ffb, + 0xf0ffc: 0xf0ffc, + 0xf0ffd: 0xf0ffd, + 0xf0ffe: 0xf0ffe, + 0xf0fff: 0xf0fff, + 0xf1000: 0xf1000, + 0xf1001: 0xf1001, + 0xf1002: 0xf1002, + 0xf1003: 0xf1003, + 0xf1004: 0xf1004, + 0xf1005: 0xf1005, + 0xf1006: 0xf1006, + 0xf1007: 0xf1007, + 0xf1008: 0xf1008, + 0xf1009: 0xf1009, + 0xf100a: 0xf100a, + 0xf100b: 0xf100b, + 0xf100c: 0xf100c, + 0xf100d: 0xf100d, + 0xf100e: 0xf100e, + 0xf100f: 0xf100f, + 0xf1010: 0xf1010, + 0xf1011: 0xf1011, + 0xf1012: 0xf1012, + 0xf1013: 0xf1013, + 0xf1014: 0xf1014, + 0xf1015: 0xf1015, + 0xf1016: 0xf1016, + 0xf1017: 0xf1017, + 0xf1018: 0xf1018, + 0xf1019: 0xf1019, + 0xf101a: 0xf101a, + 0xf101b: 0xf101b, + 0xf101c: 0xf101c, + 0xf101d: 0xf101d, + 0xf101e: 0xf101e, + 0xf101f: 0xf101f, + 0xf1020: 0xf1020, + 0xf1021: 0xf1021, + 0xf1022: 0xf1022, + 0xf1023: 0xf1023, + 0xf1024: 0xf1024, + 0xf1025: 0xf1025, + 0xf1026: 0xf1026, + 0xf1027: 0xf1027, + 0xf1028: 0xf1028, + 0xf1029: 0xf1029, + 0xf102a: 0xf102a, + 0xf102b: 0xf102b, + 0xf102c: 0xf102c, + 0xf102d: 0xf102d, + 0xf102e: 0xf102e, + 0xf102f: 0xf102f, + 0xf1030: 0xf1030, + 0xf1031: 0xf1031, + 0xf1032: 0xf1032, + 0xf1033: 0xf1033, + 0xf1034: 0xf1034, + 0xf1035: 0xf1035, + 0xf1036: 0xf1036, + 0xf1037: 0xf1037, + 0xf1038: 0xf1038, + 0xf1039: 0xf1039, + 0xf103a: 0xf103a, + 0xf103b: 0xf103b, + 0xf103c: 0xf103c, + 0xf103d: 0xf103d, + 0xf103e: 0xf103e, + 0xf103f: 0xf103f, + 0xf1040: 0xf1040, + 0xf1041: 0xf1041, + 0xf1042: 0xf1042, + 0xf1043: 0xf1043, + 0xf1044: 0xf1044, + 0xf1045: 0xf1045, + 0xf1046: 0xf1046, + 0xf1047: 0xf1047, + 0xf1048: 0xf1048, + 0xf1049: 0xf1049, + 0xf104a: 0xf104a, + 0xf104b: 0xf104b, + 0xf104c: 0xf104c, + 0xf104d: 0xf104d, + 0xf104e: 0xf104e, + 0xf104f: 0xf104f, + 0xf1050: 0xf1050, + 0xf1051: 0xf1051, + 0xf1052: 0xf1052, + 0xf1053: 0xf1053, + 0xf1054: 0xf1054, + 0xf1055: 0xf1055, + 0xf1056: 0xf1056, + 0xf1057: 0xf1057, + 0xf1058: 0xf1058, + 0xf1059: 0xf1059, + 0xf105a: 0xf105a, + 0xf105b: 0xf105b, + 0xf105c: 0xf105c, + 0xf105d: 0xf105d, + 0xf105e: 0xf105e, + 0xf105f: 0xf105f, + 0xf1060: 0xf1060, + 0xf1061: 0xf1061, + 0xf1062: 0xf1062, + 0xf1063: 0xf1063, + 0xf1064: 0xf1064, + 0xf1065: 0xf1065, + 0xf1066: 0xf1066, + 0xf1067: 0xf1067, + 0xf1068: 0xf1068, + 0xf1069: 0xf1069, + 0xf106a: 0xf106a, + 0xf106b: 0xf106b, + 0xf106c: 0xf106c, + 0xf106d: 0xf106d, + 0xf106e: 0xf106e, + 0xf106f: 0xf106f, + 0xf1070: 0xf1070, + 0xf1071: 0xf1071, + 0xf1072: 0xf1072, + 0xf1073: 0xf1073, + 0xf1074: 0xf1074, + 0xf1075: 0xf1075, + 0xf1076: 0xf1076, + 0xf1077: 0xf1077, + 0xf1078: 0xf1078, + 0xf1079: 0xf1079, + 0xf107a: 0xf107a, + 0xf107b: 0xf107b, + 0xf107c: 0xf107c, + 0xf107d: 0xf107d, + 0xf107e: 0xf107e, + 0xf107f: 0xf107f, + 0xf1080: 0xf1080, + 0xf1081: 0xf1081, + 0xf1082: 0xf1082, + 0xf1083: 0xf1083, + 0xf1084: 0xf1084, + 0xf1085: 0xf1085, + 0xf1086: 0xf1086, + 0xf1087: 0xf1087, + 0xf1088: 0xf1088, + 0xf1089: 0xf1089, + 0xf108a: 0xf108a, + 0xf108b: 0xf108b, + 0xf108c: 0xf108c, + 0xf108d: 0xf108d, + 0xf108e: 0xf108e, + 0xf108f: 0xf108f, + 0xf1090: 0xf1090, + 0xf1091: 0xf1091, + 0xf1092: 0xf1092, + 0xf1093: 0xf1093, + 0xf1094: 0xf1094, + 0xf1095: 0xf1095, + 0xf1096: 0xf1096, + 0xf1097: 0xf1097, + 0xf1098: 0xf1098, + 0xf1099: 0xf1099, + 0xf109a: 0xf109a, + 0xf109b: 0xf109b, + 0xf109c: 0xf109c, + 0xf109d: 0xf109d, + 0xf109e: 0xf109e, + 0xf109f: 0xf109f, + 0xf10a0: 0xf10a0, + 0xf10a1: 0xf10a1, + 0xf10a2: 0xf10a2, + 0xf10a3: 0xf10a3, + 0xf10a4: 0xf10a4, + 0xf10a5: 0xf10a5, + 0xf10a6: 0xf10a6, + 0xf10a7: 0xf10a7, + 0xf10a8: 0xf10a8, + 0xf10a9: 0xf10a9, + 0xf10aa: 0xf10aa, + 0xf10ab: 0xf10ab, + 0xf10ac: 0xf10ac, + 0xf10ad: 0xf10ad, + 0xf10ae: 0xf10ae, + 0xf10af: 0xf10af, + 0xf10b0: 0xf10b0, + 0xf10b1: 0xf10b1, + 0xf10b2: 0xf10b2, + 0xf10b3: 0xf10b3, + 0xf10b4: 0xf10b4, + 0xf10b5: 0xf10b5, + 0xf10b6: 0xf10b6, + 0xf10b7: 0xf10b7, + 0xf10b8: 0xf10b8, + 0xf10b9: 0xf10b9, + 0xf10ba: 0xf10ba, + 0xf10bb: 0xf10bb, + 0xf10bc: 0xf10bc, + 0xf10bd: 0xf10bd, + 0xf10be: 0xf10be, + 0xf10bf: 0xf10bf, + 0xf10c0: 0xf10c0, + 0xf10c1: 0xf10c1, + 0xf10c2: 0xf10c2, + 0xf10c3: 0xf10c3, + 0xf10c4: 0xf10c4, + 0xf10c5: 0xf10c5, + 0xf10c6: 0xf10c6, + 0xf10c7: 0xf10c7, + 0xf10c8: 0xf10c8, + 0xf10c9: 0xf10c9, + 0xf10ca: 0xf10ca, + 0xf10cb: 0xf10cb, + 0xf10cc: 0xf10cc, + 0xf10cd: 0xf10cd, + 0xf10ce: 0xf10ce, + 0xf10cf: 0xf10cf, + 0xf10d0: 0xf10d0, + 0xf10d1: 0xf10d1, + 0xf10d2: 0xf10d2, + 0xf10d3: 0xf10d3, + 0xf10d4: 0xf10d4, + 0xf10d5: 0xf10d5, + 0xf10d6: 0xf10d6, + 0xf10d7: 0xf10d7, + 0xf10d8: 0xf10d8, + 0xf10d9: 0xf10d9, + 0xf10da: 0xf10da, + 0xf10db: 0xf10db, + 0xf10dc: 0xf10dc, + 0xf10dd: 0xf10dd, + 0xf10de: 0xf10de, + 0xf10df: 0xf10df, + 0xf10e0: 0xf10e0, + 0xf10e1: 0xf10e1, + 0xf10e2: 0xf10e2, + 0xf10e3: 0xf10e3, + 0xf10e4: 0xf10e4, + 0xf10e5: 0xf10e5, + 0xf10e6: 0xf10e6, + 0xf10e7: 0xf10e7, + 0xf10e8: 0xf10e8, + 0xf10e9: 0xf10e9, + 0xf10ea: 0xf10ea, + 0xf10eb: 0xf10eb, + 0xf10ec: 0xf10ec, + 0xf10ed: 0xf10ed, + 0xf10ee: 0xf10ee, + 0xf10ef: 0xf10ef, + 0xf10f0: 0xf10f0, + 0xf10f1: 0xf10f1, + 0xf10f2: 0xf10f2, + 0xf10f3: 0xf10f3, + 0xf10f4: 0xf10f4, + 0xf10f5: 0xf10f5, + 0xf10f6: 0xf10f6, + 0xf10f7: 0xf10f7, + 0xf10f8: 0xf10f8, + 0xf10f9: 0xf10f9, + 0xf10fa: 0xf10fa, + 0xf10fb: 0xf10fb, + 0xf10fc: 0xf10fc, + 0xf10fd: 0xf10fd, + 0xf10fe: 0xf10fe, + 0xf10ff: 0xf10ff, + 0xf1100: 0xf1100, + 0xf1101: 0xf1101, + 0xf1102: 0xf1102, + 0xf1103: 0xf1103, + 0xf1104: 0xf1104, + 0xf1105: 0xf1105, + 0xf1106: 0xf1106, + 0xf1107: 0xf1107, + 0xf1108: 0xf1108, + 0xf1109: 0xf1109, + 0xf110a: 0xf110a, + 0xf110b: 0xf110b, + 0xf110c: 0xf110c, + 0xf110d: 0xf110d, + 0xf110e: 0xf110e, + 0xf110f: 0xf110f, + 0xf1110: 0xf1110, + 0xf1111: 0xf1111, + 0xf1112: 0xf1112, + 0xf1113: 0xf1113, + 0xf1114: 0xf1114, + 0xf1115: 0xf1115, + 0xf1116: 0xf1116, + 0xf1117: 0xf1117, + 0xf1118: 0xf1118, + 0xf1119: 0xf1119, + 0xf111a: 0xf111a, + 0xf111b: 0xf111b, + 0xf111c: 0xf111c, + 0xf111d: 0xf111d, + 0xf111e: 0xf111e, + 0xf111f: 0xf111f, + 0xf1120: 0xf1120, + 0xf1121: 0xf1121, + 0xf1122: 0xf1122, + 0xf1123: 0xf1123, + 0xf1124: 0xf1124, + 0xf1125: 0xf1125, + 0xf1126: 0xf1126, + 0xf1127: 0xf1127, + 0xf1128: 0xf1128, + 0xf1129: 0xf1129, + 0xf112a: 0xf112a, + 0xf112b: 0xf112b, + 0xf112c: 0xf112c, + 0xf112d: 0xf112d, + 0xf112e: 0xf112e, + 0xf112f: 0xf112f, + 0xf1130: 0xf1130, + 0xf1131: 0xf1131, + 0xf1132: 0xf1132, + 0xf1133: 0xf1133, + 0xf1134: 0xf1134, + 0xf1135: 0xf1135, + 0xf1136: 0xf1136, + 0xf1137: 0xf1137, + 0xf1138: 0xf1138, + 0xf1139: 0xf1139, + 0xf113a: 0xf113a, + 0xf113b: 0xf113b, + 0xf113c: 0xf113c, + 0xf113d: 0xf113d, + 0xf113e: 0xf113e, + 0xf113f: 0xf113f, + 0xf1140: 0xf1140, + 0xf1141: 0xf1141, + 0xf1142: 0xf1142, + 0xf1143: 0xf1143, + 0xf1144: 0xf1144, + 0xf1145: 0xf1145, + 0xf1146: 0xf1146, + 0xf1147: 0xf1147, + 0xf1148: 0xf1148, + 0xf1149: 0xf1149, + 0xf114a: 0xf114a, + 0xf114b: 0xf114b, + 0xf114c: 0xf114c, + 0xf114d: 0xf114d, + 0xf114e: 0xf114e, + 0xf114f: 0xf114f, + 0xf1150: 0xf1150, + 0xf1151: 0xf1151, + 0xf1152: 0xf1152, + 0xf1153: 0xf1153, + 0xf1154: 0xf1154, + 0xf1155: 0xf1155, + 0xf1156: 0xf1156, + 0xf1157: 0xf1157, + 0xf1158: 0xf1158, + 0xf1159: 0xf1159, + 0xf115a: 0xf115a, + 0xf115b: 0xf115b, + 0xf115c: 0xf115c, + 0xf115d: 0xf115d, + 0xf115e: 0xf115e, + 0xf115f: 0xf115f, + 0xf1160: 0xf1160, + 0xf1161: 0xf1161, + 0xf1162: 0xf1162, + 0xf1163: 0xf1163, + 0xf1164: 0xf1164, + 0xf1165: 0xf1165, + 0xf1166: 0xf1166, + 0xf1167: 0xf1167, + 0xf1168: 0xf1168, + 0xf1169: 0xf1169, + 0xf116a: 0xf116a, + 0xf116b: 0xf116b, + 0xf116c: 0xf116c, + 0xf116d: 0xf116d, + 0xf116e: 0xf116e, + 0xf116f: 0xf116f, + 0xf1170: 0xf1170, + 0xf1171: 0xf1171, + 0xf1172: 0xf1172, + 0xf1173: 0xf1173, + 0xf1174: 0xf1174, + 0xf1175: 0xf1175, + 0xf1176: 0xf1176, + 0xf1177: 0xf1177, + 0xf1178: 0xf1178, + 0xf1179: 0xf1179, + 0xf117a: 0xf117a, + 0xf117b: 0xf117b, + 0xf117c: 0xf117c, + 0xf117d: 0xf117d, + 0xf117e: 0xf117e, + 0xf117f: 0xf117f, + 0xf1180: 0xf1180, + 0xf1181: 0xf1181, + 0xf1182: 0xf1182, + 0xf1183: 0xf1183, + 0xf1184: 0xf1184, + 0xf1185: 0xf1185, + 0xf1186: 0xf1186, + 0xf1187: 0xf1187, + 0xf1188: 0xf1188, + 0xf1189: 0xf1189, + 0xf118a: 0xf118a, + 0xf118b: 0xf118b, + 0xf118c: 0xf118c, + 0xf118d: 0xf118d, + 0xf118e: 0xf118e, + 0xf118f: 0xf118f, + 0xf1190: 0xf1190, + 0xf1191: 0xf1191, + 0xf1192: 0xf1192, + 0xf1193: 0xf1193, + 0xf1194: 0xf1194, + 0xf1195: 0xf1195, + 0xf1196: 0xf1196, + 0xf1197: 0xf1197, + 0xf1198: 0xf1198, + 0xf1199: 0xf1199, + 0xf119a: 0xf119a, + 0xf119b: 0xf119b, + 0xf119c: 0xf119c, + 0xf119d: 0xf119d, + 0xf119e: 0xf119e, + 0xf119f: 0xf119f, + 0xf11a0: 0xf11a0, + 0xf11a1: 0xf11a1, + 0xf11a2: 0xf11a2, + 0xf11a3: 0xf11a3, + 0xf11a4: 0xf11a4, + 0xf11a5: 0xf11a5, + 0xf11a6: 0xf11a6, + 0xf11a7: 0xf11a7, + 0xf11a8: 0xf11a8, + 0xf11a9: 0xf11a9, + 0xf11aa: 0xf11aa, + 0xf11ab: 0xf11ab, + 0xf11ac: 0xf11ac, + 0xf11ad: 0xf11ad, + 0xf11ae: 0xf11ae, + 0xf11af: 0xf11af, + 0xf11b0: 0xf11b0, + 0xf11b1: 0xf11b1, + 0xf11b2: 0xf11b2, + 0xf11b3: 0xf11b3, + 0xf11b4: 0xf11b4, + 0xf11b5: 0xf11b5, + 0xf11b6: 0xf11b6, + 0xf11b7: 0xf11b7, + 0xf11b8: 0xf11b8, + 0xf11b9: 0xf11b9, + 0xf11ba: 0xf11ba, + 0xf11bb: 0xf11bb, + 0xf11bc: 0xf11bc, + 0xf11bd: 0xf11bd, + 0xf11be: 0xf11be, + 0xf11bf: 0xf11bf, + 0xf11c0: 0xf11c0, + 0xf11c1: 0xf11c1, + 0xf11c2: 0xf11c2, + 0xf11c3: 0xf11c3, + 0xf11c4: 0xf11c4, + 0xf11c5: 0xf11c5, + 0xf11c6: 0xf11c6, + 0xf11c7: 0xf11c7, + 0xf11c8: 0xf11c8, + 0xf11c9: 0xf11c9, + 0xf11ca: 0xf11ca, + 0xf11cb: 0xf11cb, + 0xf11cc: 0xf11cc, + 0xf11cd: 0xf11cd, + 0xf11ce: 0xf11ce, + 0xf11cf: 0xf11cf, + 0xf11d0: 0xf11d0, + 0xf11d1: 0xf11d1, + 0xf11d2: 0xf11d2, + 0xf11d3: 0xf11d3, + 0xf11d4: 0xf11d4, + 0xf11d5: 0xf11d5, + 0xf11d6: 0xf11d6, + 0xf11d7: 0xf11d7, + 0xf11d8: 0xf11d8, + 0xf11d9: 0xf11d9, + 0xf11da: 0xf11da, + 0xf11db: 0xf11db, + 0xf11dc: 0xf11dc, + 0xf11dd: 0xf11dd, + 0xf11de: 0xf11de, + 0xf11df: 0xf11df, + 0xf11e0: 0xf11e0, + 0xf11e1: 0xf11e1, + 0xf11e2: 0xf11e2, + 0xf11e3: 0xf11e3, + 0xf11e4: 0xf11e4, + 0xf11e5: 0xf11e5, + 0xf11e6: 0xf11e6, + 0xf11e7: 0xf11e7, + 0xf11e8: 0xf11e8, + 0xf11e9: 0xf11e9, + 0xf11ea: 0xf11ea, + 0xf11eb: 0xf11eb, + 0xf11ec: 0xf11ec, + 0xf11ed: 0xf11ed, + 0xf11ee: 0xf11ee, + 0xf11ef: 0xf11ef, + 0xf11f0: 0xf11f0, + 0xf11f1: 0xf11f1, + 0xf11f2: 0xf11f2, + 0xf11f3: 0xf11f3, + 0xf11f4: 0xf11f4, + 0xf11f5: 0xf11f5, + 0xf11f6: 0xf11f6, + 0xf11f7: 0xf11f7, + 0xf11f8: 0xf11f8, + 0xf11f9: 0xf11f9, + 0xf11fa: 0xf11fa, + 0xf11fb: 0xf11fb, + 0xf11fc: 0xf11fc, + 0xf11fd: 0xf11fd, + 0xf11fe: 0xf11fe, + 0xf11ff: 0xf11ff, + 0xf1200: 0xf1200, + 0xf1201: 0xf1201, + 0xf1202: 0xf1202, + 0xf1203: 0xf1203, + 0xf1204: 0xf1204, + 0xf1205: 0xf1205, + 0xf1206: 0xf1206, + 0xf1207: 0xf1207, + 0xf1208: 0xf1208, + 0xf1209: 0xf1209, + 0xf120a: 0xf120a, + 0xf120b: 0xf120b, + 0xf120c: 0xf120c, + 0xf120d: 0xf120d, + 0xf120e: 0xf120e, + 0xf120f: 0xf120f, + 0xf1210: 0xf1210, + 0xf1211: 0xf1211, + 0xf1212: 0xf1212, + 0xf1213: 0xf1213, + 0xf1214: 0xf1214, + 0xf1215: 0xf1215, + 0xf1216: 0xf1216, + 0xf1217: 0xf1217, + 0xf1218: 0xf1218, + 0xf1219: 0xf1219, + 0xf121a: 0xf121a, + 0xf121b: 0xf121b, + 0xf121c: 0xf121c, + 0xf121d: 0xf121d, + 0xf121e: 0xf121e, + 0xf121f: 0xf121f, + 0xf1220: 0xf1220, + 0xf1221: 0xf1221, + 0xf1222: 0xf1222, + 0xf1223: 0xf1223, + 0xf1224: 0xf1224, + 0xf1225: 0xf1225, + 0xf1226: 0xf1226, + 0xf1227: 0xf1227, + 0xf1228: 0xf1228, + 0xf1229: 0xf1229, + 0xf122a: 0xf122a, + 0xf122b: 0xf122b, + 0xf122c: 0xf122c, + 0xf122d: 0xf122d, + 0xf122e: 0xf122e, + 0xf122f: 0xf122f, + 0xf1230: 0xf1230, + 0xf1231: 0xf1231, + 0xf1232: 0xf1232, + 0xf1233: 0xf1233, + 0xf1234: 0xf1234, + 0xf1235: 0xf1235, + 0xf1236: 0xf1236, + 0xf1237: 0xf1237, + 0xf1238: 0xf1238, + 0xf1239: 0xf1239, + 0xf123a: 0xf123a, + 0xf123b: 0xf123b, + 0xf123c: 0xf123c, + 0xf123d: 0xf123d, + 0xf123e: 0xf123e, + 0xf123f: 0xf123f, + 0xf1240: 0xf1240, + 0xf1241: 0xf1241, + 0xf1242: 0xf1242, + 0xf1243: 0xf1243, + 0xf1244: 0xf1244, + 0xf1245: 0xf1245, + 0xf1246: 0xf1246, + 0xf1247: 0xf1247, + 0xf1248: 0xf1248, + 0xf1249: 0xf1249, + 0xf124a: 0xf124a, + 0xf124b: 0xf124b, + 0xf124c: 0xf124c, + 0xf124d: 0xf124d, + 0xf124e: 0xf124e, + 0xf124f: 0xf124f, + 0xf1250: 0xf1250, + 0xf1251: 0xf1251, + 0xf1252: 0xf1252, + 0xf1253: 0xf1253, + 0xf1254: 0xf1254, + 0xf1255: 0xf1255, + 0xf1256: 0xf1256, + 0xf1257: 0xf1257, + 0xf1258: 0xf1258, + 0xf1259: 0xf1259, + 0xf125a: 0xf125a, + 0xf125b: 0xf125b, + 0xf125c: 0xf125c, + 0xf125d: 0xf125d, + 0xf125e: 0xf125e, + 0xf125f: 0xf125f, + 0xf1260: 0xf1260, + 0xf1261: 0xf1261, + 0xf1262: 0xf1262, + 0xf1263: 0xf1263, + 0xf1264: 0xf1264, + 0xf1265: 0xf1265, + 0xf1266: 0xf1266, + 0xf1267: 0xf1267, + 0xf1268: 0xf1268, + 0xf1269: 0xf1269, + 0xf126a: 0xf126a, + 0xf126b: 0xf126b, + 0xf126c: 0xf126c, + 0xf126d: 0xf126d, + 0xf126e: 0xf126e, + 0xf126f: 0xf126f, + 0xf1270: 0xf1270, + 0xf1271: 0xf1271, + 0xf1272: 0xf1272, + 0xf1273: 0xf1273, + 0xf1274: 0xf1274, + 0xf1275: 0xf1275, + 0xf1276: 0xf1276, + 0xf1277: 0xf1277, + 0xf1278: 0xf1278, + 0xf1279: 0xf1279, + 0xf127a: 0xf127a, + 0xf127b: 0xf127b, + 0xf127c: 0xf127c, + 0xf127d: 0xf127d, + 0xf127e: 0xf127e, + 0xf127f: 0xf127f, + 0xf1280: 0xf1280, + 0xf1281: 0xf1281, + 0xf1282: 0xf1282, + 0xf1283: 0xf1283, + 0xf1284: 0xf1284, + 0xf1285: 0xf1285, + 0xf1286: 0xf1286, + 0xf1287: 0xf1287, + 0xf1288: 0xf1288, + 0xf1289: 0xf1289, + 0xf128a: 0xf128a, + 0xf128b: 0xf128b, + 0xf128c: 0xf128c, + 0xf128d: 0xf128d, + 0xf128e: 0xf128e, + 0xf128f: 0xf128f, + 0xf1290: 0xf1290, + 0xf1291: 0xf1291, + 0xf1292: 0xf1292, + 0xf1293: 0xf1293, + 0xf1294: 0xf1294, + 0xf1295: 0xf1295, + 0xf1296: 0xf1296, + 0xf1297: 0xf1297, + 0xf1298: 0xf1298, + 0xf1299: 0xf1299, + 0xf129a: 0xf129a, + 0xf129b: 0xf129b, + 0xf129c: 0xf129c, + 0xf129d: 0xf129d, + 0xf129e: 0xf129e, + 0xf129f: 0xf129f, + 0xf12a0: 0xf12a0, + 0xf12a1: 0xf12a1, + 0xf12a2: 0xf12a2, + 0xf12a3: 0xf12a3, + 0xf12a4: 0xf12a4, + 0xf12a5: 0xf12a5, + 0xf12a6: 0xf12a6, + 0xf12a7: 0xf12a7, + 0xf12a8: 0xf12a8, + 0xf12a9: 0xf12a9, + 0xf12aa: 0xf12aa, + 0xf12ab: 0xf12ab, + 0xf12ac: 0xf12ac, + 0xf12ad: 0xf12ad, + 0xf12ae: 0xf12ae, + 0xf12af: 0xf12af, + 0xf12b0: 0xf12b0, + 0xf12b1: 0xf12b1, + 0xf12b2: 0xf12b2, + 0xf12b3: 0xf12b3, + 0xf12b4: 0xf12b4, + 0xf12b5: 0xf12b5, + 0xf12b6: 0xf12b6, + 0xf12b7: 0xf12b7, + 0xf12b8: 0xf12b8, + 0xf12b9: 0xf12b9, + 0xf12ba: 0xf12ba, + 0xf12bb: 0xf12bb, + 0xf12bc: 0xf12bc, + 0xf12bd: 0xf12bd, + 0xf12be: 0xf12be, + 0xf12bf: 0xf12bf, + 0xf12c0: 0xf12c0, + 0xf12c1: 0xf12c1, + 0xf12c2: 0xf12c2, + 0xf12c3: 0xf12c3, + 0xf12c4: 0xf12c4, + 0xf12c5: 0xf12c5, + 0xf12c6: 0xf12c6, + 0xf12c7: 0xf12c7, + 0xf12c8: 0xf12c8, + 0xf12c9: 0xf12c9, + 0xf12ca: 0xf12ca, + 0xf12cb: 0xf12cb, + 0xf12cc: 0xf12cc, + 0xf12cd: 0xf12cd, + 0xf12ce: 0xf12ce, + 0xf12cf: 0xf12cf, + 0xf12d0: 0xf12d0, + 0xf12d1: 0xf12d1, + 0xf12d2: 0xf12d2, + 0xf12d3: 0xf12d3, + 0xf12d4: 0xf12d4, + 0xf12d5: 0xf12d5, + 0xf12d6: 0xf12d6, + 0xf12d7: 0xf12d7, + 0xf12d8: 0xf12d8, + 0xf12d9: 0xf12d9, + 0xf12da: 0xf12da, + 0xf12db: 0xf12db, + 0xf12dc: 0xf12dc, + 0xf12dd: 0xf12dd, + 0xf12de: 0xf12de, + 0xf12df: 0xf12df, + 0xf12e0: 0xf12e0, + 0xf12e1: 0xf12e1, + 0xf12e2: 0xf12e2, + 0xf12e3: 0xf12e3, + 0xf12e4: 0xf12e4, + 0xf12e5: 0xf12e5, + 0xf12e6: 0xf12e6, + 0xf12e7: 0xf12e7, + 0xf12e8: 0xf12e8, + 0xf12e9: 0xf12e9, + 0xf12ea: 0xf12ea, + 0xf12eb: 0xf12eb, + 0xf12ec: 0xf12ec, + 0xf12ed: 0xf12ed, + 0xf12ee: 0xf12ee, + 0xf12ef: 0xf12ef, + 0xf12f0: 0xf12f0, + 0xf12f1: 0xf12f1, + 0xf12f2: 0xf12f2, + 0xf12f3: 0xf12f3, + 0xf12f4: 0xf12f4, + 0xf12f5: 0xf12f5, + 0xf12f6: 0xf12f6, + 0xf12f7: 0xf12f7, + 0xf12f8: 0xf12f8, + 0xf12f9: 0xf12f9, + 0xf12fa: 0xf12fa, + 0xf12fb: 0xf12fb, + 0xf12fc: 0xf12fc, + 0xf12fd: 0xf12fd, + 0xf12fe: 0xf12fe, + 0xf12ff: 0xf12ff, + 0xf1300: 0xf1300, + 0xf1301: 0xf1301, + 0xf1302: 0xf1302, + 0xf1303: 0xf1303, + 0xf1304: 0xf1304, + 0xf1305: 0xf1305, + 0xf1306: 0xf1306, + 0xf1307: 0xf1307, + 0xf1308: 0xf1308, + 0xf1309: 0xf1309, + 0xf130a: 0xf130a, + 0xf130b: 0xf130b, + 0xf130c: 0xf130c, + 0xf130d: 0xf130d, + 0xf130e: 0xf130e, + 0xf130f: 0xf130f, + 0xf1310: 0xf1310, + 0xf1311: 0xf1311, + 0xf1312: 0xf1312, + 0xf1313: 0xf1313, + 0xf1314: 0xf1314, + 0xf1315: 0xf1315, + 0xf1316: 0xf1316, + 0xf1317: 0xf1317, + 0xf1318: 0xf1318, + 0xf1319: 0xf1319, + 0xf131a: 0xf131a, + 0xf131b: 0xf131b, + 0xf131c: 0xf131c, + 0xf131d: 0xf131d, + 0xf131e: 0xf131e, + 0xf131f: 0xf131f, + 0xf1320: 0xf1320, + 0xf1321: 0xf1321, + 0xf1322: 0xf1322, + 0xf1323: 0xf1323, + 0xf1324: 0xf1324, + 0xf1325: 0xf1325, + 0xf1326: 0xf1326, + 0xf1327: 0xf1327, + 0xf1328: 0xf1328, + 0xf1329: 0xf1329, + 0xf132a: 0xf132a, + 0xf132b: 0xf132b, + 0xf132c: 0xf132c, + 0xf132d: 0xf132d, + 0xf132e: 0xf132e, + 0xf132f: 0xf132f, + 0xf1330: 0xf1330, + 0xf1331: 0xf1331, + 0xf1332: 0xf1332, + 0xf1333: 0xf1333, + 0xf1334: 0xf1334, + 0xf1335: 0xf1335, + 0xf1336: 0xf1336, + 0xf1337: 0xf1337, + 0xf1338: 0xf1338, + 0xf1339: 0xf1339, + 0xf133a: 0xf133a, + 0xf133b: 0xf133b, + 0xf133c: 0xf133c, + 0xf133d: 0xf133d, + 0xf133e: 0xf133e, + 0xf133f: 0xf133f, + 0xf1340: 0xf1340, + 0xf1341: 0xf1341, + 0xf1342: 0xf1342, + 0xf1343: 0xf1343, + 0xf1344: 0xf1344, + 0xf1345: 0xf1345, + 0xf1346: 0xf1346, + 0xf1347: 0xf1347, + 0xf1348: 0xf1348, + 0xf1349: 0xf1349, + 0xf134a: 0xf134a, + 0xf134b: 0xf134b, + 0xf134c: 0xf134c, + 0xf134d: 0xf134d, + 0xf134e: 0xf134e, + 0xf134f: 0xf134f, + 0xf1350: 0xf1350, + 0xf1351: 0xf1351, + 0xf1352: 0xf1352, + 0xf1353: 0xf1353, + 0xf1354: 0xf1354, + 0xf1355: 0xf1355, + 0xf1356: 0xf1356, + 0xf1357: 0xf1357, + 0xf1358: 0xf1358, + 0xf1359: 0xf1359, + 0xf135a: 0xf135a, + 0xf135b: 0xf135b, + 0xf135c: 0xf135c, + 0xf135d: 0xf135d, + 0xf135e: 0xf135e, + 0xf135f: 0xf135f, + 0xf1360: 0xf1360, + 0xf1361: 0xf1361, + 0xf1362: 0xf1362, + 0xf1363: 0xf1363, + 0xf1364: 0xf1364, + 0xf1365: 0xf1365, + 0xf1366: 0xf1366, + 0xf1367: 0xf1367, + 0xf1368: 0xf1368, + 0xf1369: 0xf1369, + 0xf136a: 0xf136a, + 0xf136b: 0xf136b, + 0xf136c: 0xf136c, + 0xf136d: 0xf136d, + 0xf136e: 0xf136e, + 0xf136f: 0xf136f, + 0xf1370: 0xf1370, + 0xf1371: 0xf1371, + 0xf1372: 0xf1372, + 0xf1373: 0xf1373, + 0xf1374: 0xf1374, + 0xf1375: 0xf1375, + 0xf1376: 0xf1376, + 0xf1377: 0xf1377, + 0xf1378: 0xf1378, + 0xf1379: 0xf1379, + 0xf137a: 0xf137a, + 0xf137b: 0xf137b, + 0xf137c: 0xf137c, + 0xf137d: 0xf137d, + 0xf137e: 0xf137e, + 0xf137f: 0xf137f, + 0xf1380: 0xf1380, + 0xf1381: 0xf1381, + 0xf1382: 0xf1382, + 0xf1383: 0xf1383, + 0xf1384: 0xf1384, + 0xf1385: 0xf1385, + 0xf1386: 0xf1386, + 0xf1387: 0xf1387, + 0xf1388: 0xf1388, + 0xf1389: 0xf1389, + 0xf138a: 0xf138a, + 0xf138b: 0xf138b, + 0xf138c: 0xf138c, + 0xf138d: 0xf138d, + 0xf138e: 0xf138e, + 0xf138f: 0xf138f, + 0xf1390: 0xf1390, + 0xf1391: 0xf1391, + 0xf1392: 0xf1392, + 0xf1393: 0xf1393, + 0xf1394: 0xf1394, + 0xf1395: 0xf1395, + 0xf1396: 0xf1396, + 0xf1397: 0xf1397, + 0xf1398: 0xf1398, + 0xf1399: 0xf1399, + 0xf139a: 0xf139a, + 0xf139b: 0xf139b, + 0xf139c: 0xf139c, + 0xf139d: 0xf139d, + 0xf139e: 0xf139e, + 0xf139f: 0xf139f, + 0xf13a0: 0xf13a0, + 0xf13a1: 0xf13a1, + 0xf13a2: 0xf13a2, + 0xf13a3: 0xf13a3, + 0xf13a4: 0xf13a4, + 0xf13a5: 0xf13a5, + 0xf13a6: 0xf13a6, + 0xf13a7: 0xf13a7, + 0xf13a8: 0xf13a8, + 0xf13a9: 0xf13a9, + 0xf13aa: 0xf13aa, + 0xf13ab: 0xf13ab, + 0xf13ac: 0xf13ac, + 0xf13ad: 0xf13ad, + 0xf13ae: 0xf13ae, + 0xf13af: 0xf13af, + 0xf13b0: 0xf13b0, + 0xf13b1: 0xf13b1, + 0xf13b2: 0xf13b2, + 0xf13b3: 0xf13b3, + 0xf13b4: 0xf13b4, + 0xf13b5: 0xf13b5, + 0xf13b6: 0xf13b6, + 0xf13b7: 0xf13b7, + 0xf13b8: 0xf13b8, + 0xf13b9: 0xf13b9, + 0xf13ba: 0xf13ba, + 0xf13bb: 0xf13bb, + 0xf13bc: 0xf13bc, + 0xf13bd: 0xf13bd, + 0xf13be: 0xf13be, + 0xf13bf: 0xf13bf, + 0xf13c0: 0xf13c0, + 0xf13c1: 0xf13c1, + 0xf13c2: 0xf13c2, + 0xf13c3: 0xf13c3, + 0xf13c4: 0xf13c4, + 0xf13c5: 0xf13c5, + 0xf13c6: 0xf13c6, + 0xf13c7: 0xf13c7, + 0xf13c8: 0xf13c8, + 0xf13c9: 0xf13c9, + 0xf13ca: 0xf13ca, + 0xf13cb: 0xf13cb, + 0xf13cc: 0xf13cc, + 0xf13cd: 0xf13cd, + 0xf13ce: 0xf13ce, + 0xf13cf: 0xf13cf, + 0xf13d0: 0xf13d0, + 0xf13d1: 0xf13d1, + 0xf13d2: 0xf13d2, + 0xf13d3: 0xf13d3, + 0xf13d4: 0xf13d4, + 0xf13d5: 0xf13d5, + 0xf13d6: 0xf13d6, + 0xf13d7: 0xf13d7, + 0xf13d8: 0xf13d8, + 0xf13d9: 0xf13d9, + 0xf13da: 0xf13da, + 0xf13db: 0xf13db, + 0xf13dc: 0xf13dc, + 0xf13dd: 0xf13dd, + 0xf13de: 0xf13de, + 0xf13df: 0xf13df, + 0xf13e0: 0xf13e0, + 0xf13e1: 0xf13e1, + 0xf13e2: 0xf13e2, + 0xf13e3: 0xf13e3, + 0xf13e4: 0xf13e4, + 0xf13e5: 0xf13e5, + 0xf13e6: 0xf13e6, + 0xf13e7: 0xf13e7, + 0xf13e8: 0xf13e8, + 0xf13e9: 0xf13e9, + 0xf13ea: 0xf13ea, + 0xf13eb: 0xf13eb, + 0xf13ec: 0xf13ec, + 0xf13ed: 0xf13ed, + 0xf13ee: 0xf13ee, + 0xf13ef: 0xf13ef, + 0xf13f0: 0xf13f0, + 0xf13f1: 0xf13f1, + 0xf13f2: 0xf13f2, + 0xf13f3: 0xf13f3, + 0xf13f4: 0xf13f4, + 0xf13f5: 0xf13f5, + 0xf13f6: 0xf13f6, + 0xf13f7: 0xf13f7, + 0xf13f8: 0xf13f8, + 0xf13f9: 0xf13f9, + 0xf13fa: 0xf13fa, + 0xf13fb: 0xf13fb, + 0xf13fc: 0xf13fc, + 0xf13fd: 0xf13fd, + 0xf13fe: 0xf13fe, + 0xf13ff: 0xf13ff, + 0xf1400: 0xf1400, + 0xf1401: 0xf1401, + 0xf1402: 0xf1402, + 0xf1403: 0xf1403, + 0xf1404: 0xf1404, + 0xf1405: 0xf1405, + 0xf1406: 0xf1406, + 0xf1407: 0xf1407, + 0xf1408: 0xf1408, + 0xf1409: 0xf1409, + 0xf140a: 0xf140a, + 0xf140b: 0xf140b, + 0xf140c: 0xf140c, + 0xf140d: 0xf140d, + 0xf140e: 0xf140e, + 0xf140f: 0xf140f, + 0xf1410: 0xf1410, + 0xf1411: 0xf1411, + 0xf1412: 0xf1412, + 0xf1413: 0xf1413, + 0xf1414: 0xf1414, + 0xf1415: 0xf1415, + 0xf1416: 0xf1416, + 0xf1417: 0xf1417, + 0xf1418: 0xf1418, + 0xf1419: 0xf1419, + 0xf141a: 0xf141a, + 0xf141b: 0xf141b, + 0xf141c: 0xf141c, + 0xf141d: 0xf141d, + 0xf141e: 0xf141e, + 0xf141f: 0xf141f, + 0xf1420: 0xf1420, + 0xf1421: 0xf1421, + 0xf1422: 0xf1422, + 0xf1423: 0xf1423, + 0xf1424: 0xf1424, + 0xf1425: 0xf1425, + 0xf1426: 0xf1426, + 0xf1427: 0xf1427, + 0xf1428: 0xf1428, + 0xf1429: 0xf1429, + 0xf142a: 0xf142a, + 0xf142b: 0xf142b, + 0xf142c: 0xf142c, + 0xf142d: 0xf142d, + 0xf142e: 0xf142e, + 0xf142f: 0xf142f, + 0xf1430: 0xf1430, + 0xf1431: 0xf1431, + 0xf1432: 0xf1432, + 0xf1433: 0xf1433, + 0xf1434: 0xf1434, + 0xf1435: 0xf1435, + 0xf1436: 0xf1436, + 0xf1437: 0xf1437, + 0xf1438: 0xf1438, + 0xf1439: 0xf1439, + 0xf143a: 0xf143a, + 0xf143b: 0xf143b, + 0xf143c: 0xf143c, + 0xf143d: 0xf143d, + 0xf143e: 0xf143e, + 0xf143f: 0xf143f, + 0xf1440: 0xf1440, + 0xf1441: 0xf1441, + 0xf1442: 0xf1442, + 0xf1443: 0xf1443, + 0xf1444: 0xf1444, + 0xf1445: 0xf1445, + 0xf1446: 0xf1446, + 0xf1447: 0xf1447, + 0xf1448: 0xf1448, + 0xf1449: 0xf1449, + 0xf144a: 0xf144a, + 0xf144b: 0xf144b, + 0xf144c: 0xf144c, + 0xf144d: 0xf144d, + 0xf144e: 0xf144e, + 0xf144f: 0xf144f, + 0xf1450: 0xf1450, + 0xf1451: 0xf1451, + 0xf1452: 0xf1452, + 0xf1453: 0xf1453, + 0xf1454: 0xf1454, + 0xf1455: 0xf1455, + 0xf1456: 0xf1456, + 0xf1457: 0xf1457, + 0xf1458: 0xf1458, + 0xf1459: 0xf1459, + 0xf145a: 0xf145a, + 0xf145b: 0xf145b, + 0xf145c: 0xf145c, + 0xf145d: 0xf145d, + 0xf145e: 0xf145e, + 0xf145f: 0xf145f, + 0xf1460: 0xf1460, + 0xf1461: 0xf1461, + 0xf1462: 0xf1462, + 0xf1463: 0xf1463, + 0xf1464: 0xf1464, + 0xf1465: 0xf1465, + 0xf1466: 0xf1466, + 0xf1467: 0xf1467, + 0xf1468: 0xf1468, + 0xf1469: 0xf1469, + 0xf146a: 0xf146a, + 0xf146b: 0xf146b, + 0xf146c: 0xf146c, + 0xf146d: 0xf146d, + 0xf146e: 0xf146e, + 0xf146f: 0xf146f, + 0xf1470: 0xf1470, + 0xf1471: 0xf1471, + 0xf1472: 0xf1472, + 0xf1473: 0xf1473, + 0xf1474: 0xf1474, + 0xf1475: 0xf1475, + 0xf1476: 0xf1476, + 0xf1477: 0xf1477, + 0xf1478: 0xf1478, + 0xf1479: 0xf1479, + 0xf147a: 0xf147a, + 0xf147b: 0xf147b, + 0xf147c: 0xf147c, + 0xf147d: 0xf147d, + 0xf147e: 0xf147e, + 0xf147f: 0xf147f, + 0xf1480: 0xf1480, + 0xf1481: 0xf1481, + 0xf1482: 0xf1482, + 0xf1483: 0xf1483, + 0xf1484: 0xf1484, + 0xf1485: 0xf1485, + 0xf1486: 0xf1486, + 0xf1487: 0xf1487, + 0xf1488: 0xf1488, + 0xf1489: 0xf1489, + 0xf148a: 0xf148a, + 0xf148b: 0xf148b, + 0xf148c: 0xf148c, + 0xf148d: 0xf148d, + 0xf148e: 0xf148e, + 0xf148f: 0xf148f, + 0xf1490: 0xf1490, + 0xf1491: 0xf1491, + 0xf1492: 0xf1492, + 0xf1493: 0xf1493, + 0xf1494: 0xf1494, + 0xf1495: 0xf1495, + 0xf1496: 0xf1496, + 0xf1497: 0xf1497, + 0xf1498: 0xf1498, + 0xf1499: 0xf1499, + 0xf149a: 0xf149a, + 0xf149b: 0xf149b, + 0xf149c: 0xf149c, + 0xf149d: 0xf149d, + 0xf149e: 0xf149e, + 0xf149f: 0xf149f, + 0xf14a0: 0xf14a0, + 0xf14a1: 0xf14a1, + 0xf14a2: 0xf14a2, + 0xf14a3: 0xf14a3, + 0xf14a4: 0xf14a4, + 0xf14a5: 0xf14a5, + 0xf14a6: 0xf14a6, + 0xf14a7: 0xf14a7, + 0xf14a8: 0xf14a8, + 0xf14a9: 0xf14a9, + 0xf14aa: 0xf14aa, + 0xf14ab: 0xf14ab, + 0xf14ac: 0xf14ac, + 0xf14ad: 0xf14ad, + 0xf14ae: 0xf14ae, + 0xf14af: 0xf14af, + 0xf14b0: 0xf14b0, + 0xf14b1: 0xf14b1, + 0xf14b2: 0xf14b2, + 0xf14b3: 0xf14b3, + 0xf14b4: 0xf14b4, + 0xf14b5: 0xf14b5, + 0xf14b6: 0xf14b6, + 0xf14b7: 0xf14b7, + 0xf14b8: 0xf14b8, + 0xf14b9: 0xf14b9, + 0xf14ba: 0xf14ba, + 0xf14bb: 0xf14bb, + 0xf14bc: 0xf14bc, + 0xf14bd: 0xf14bd, + 0xf14be: 0xf14be, + 0xf14bf: 0xf14bf, + 0xf14c0: 0xf14c0, + 0xf14c1: 0xf14c1, + 0xf14c2: 0xf14c2, + 0xf14c3: 0xf14c3, + 0xf14c4: 0xf14c4, + 0xf14c5: 0xf14c5, + 0xf14c6: 0xf14c6, + 0xf14c7: 0xf14c7, + 0xf14c8: 0xf14c8, + 0xf14c9: 0xf14c9, + 0xf14ca: 0xf14ca, + 0xf14cb: 0xf14cb, + 0xf14cc: 0xf14cc, + 0xf14cd: 0xf14cd, + 0xf14ce: 0xf14ce, + 0xf14cf: 0xf14cf, + 0xf14d0: 0xf14d0, + 0xf14d1: 0xf14d1, + 0xf14d2: 0xf14d2, + 0xf14d3: 0xf14d3, + 0xf14d4: 0xf14d4, + 0xf14d5: 0xf14d5, + 0xf14d6: 0xf14d6, + 0xf14d7: 0xf14d7, + 0xf14d8: 0xf14d8, + 0xf14d9: 0xf14d9, + 0xf14da: 0xf14da, + 0xf14db: 0xf14db, + 0xf14dc: 0xf14dc, + 0xf14dd: 0xf14dd, + 0xf14de: 0xf14de, + 0xf14df: 0xf14df, + 0xf14e0: 0xf14e0, + 0xf14e1: 0xf14e1, + 0xf14e2: 0xf14e2, + 0xf14e3: 0xf14e3, + 0xf14e4: 0xf14e4, + 0xf14e5: 0xf14e5, + 0xf14e6: 0xf14e6, + 0xf14e7: 0xf14e7, + 0xf14e8: 0xf14e8, + 0xf14e9: 0xf14e9, + 0xf14ea: 0xf14ea, + 0xf14eb: 0xf14eb, + 0xf14ec: 0xf14ec, + 0xf14ed: 0xf14ed, + 0xf14ee: 0xf14ee, + 0xf14ef: 0xf14ef, + 0xf14f0: 0xf14f0, + 0xf14f1: 0xf14f1, + 0xf14f2: 0xf14f2, + 0xf14f3: 0xf14f3, + 0xf14f4: 0xf14f4, + 0xf14f5: 0xf14f5, + 0xf14f6: 0xf14f6, + 0xf14f7: 0xf14f7, + 0xf14f8: 0xf14f8, + 0xf14f9: 0xf14f9, + 0xf14fa: 0xf14fa, + 0xf14fb: 0xf14fb, + 0xf14fc: 0xf14fc, + 0xf14fd: 0xf14fd, + 0xf14fe: 0xf14fe, + 0xf14ff: 0xf14ff, + 0xf1500: 0xf1500, + 0xf1501: 0xf1501, + 0xf1502: 0xf1502, + 0xf1503: 0xf1503, + 0xf1504: 0xf1504, + 0xf1505: 0xf1505, + 0xf1506: 0xf1506, + 0xf1507: 0xf1507, + 0xf1508: 0xf1508, + 0xf1509: 0xf1509, + 0xf150a: 0xf150a, + 0xf150b: 0xf150b, + 0xf150c: 0xf150c, + 0xf150d: 0xf150d, + 0xf150e: 0xf150e, + 0xf150f: 0xf150f, + 0xf1510: 0xf1510, + 0xf1511: 0xf1511, + 0xf1512: 0xf1512, + 0xf1513: 0xf1513, + 0xf1514: 0xf1514, + 0xf1515: 0xf1515, + 0xf1516: 0xf1516, + 0xf1517: 0xf1517, + 0xf1518: 0xf1518, + 0xf1519: 0xf1519, + 0xf151a: 0xf151a, + 0xf151b: 0xf151b, + 0xf151c: 0xf151c, + 0xf151d: 0xf151d, + 0xf151e: 0xf151e, + 0xf151f: 0xf151f, + 0xf1520: 0xf1520, + 0xf1521: 0xf1521, + 0xf1522: 0xf1522, + 0xf1523: 0xf1523, + 0xf1524: 0xf1524, + 0xf1525: 0xf1525, + 0xf1526: 0xf1526, + 0xf1527: 0xf1527, + 0xf1528: 0xf1528, + 0xf1529: 0xf1529, + 0xf152a: 0xf152a, + 0xf152b: 0xf152b, + 0xf152c: 0xf152c, + 0xf152d: 0xf152d, + 0xf152e: 0xf152e, + 0xf152f: 0xf152f, + 0xf1530: 0xf1530, + 0xf1531: 0xf1531, + 0xf1532: 0xf1532, + 0xf1533: 0xf1533, + 0xf1534: 0xf1534, + 0xf1535: 0xf1535, + 0xf1536: 0xf1536, + 0xf1537: 0xf1537, + 0xf1538: 0xf1538, + 0xf1539: 0xf1539, + 0xf153a: 0xf153a, + 0xf153b: 0xf153b, + 0xf153c: 0xf153c, + 0xf153d: 0xf153d, + 0xf153e: 0xf153e, + 0xf153f: 0xf153f, + 0xf1540: 0xf1540, + 0xf1541: 0xf1541, + 0xf1542: 0xf1542, + 0xf1543: 0xf1543, + 0xf1544: 0xf1544, + 0xf1545: 0xf1545, + 0xf1546: 0xf1546, + 0xf1547: 0xf1547, + 0xf1548: 0xf1548, + 0xf1549: 0xf1549, + 0xf154a: 0xf154a, + 0xf154b: 0xf154b, + 0xf154c: 0xf154c, + 0xf154d: 0xf154d, + 0xf154e: 0xf154e, + 0xf154f: 0xf154f, + 0xf1550: 0xf1550, + 0xf1551: 0xf1551, + 0xf1552: 0xf1552, + 0xf1553: 0xf1553, + 0xf1554: 0xf1554, + 0xf1555: 0xf1555, + 0xf1556: 0xf1556, + 0xf1557: 0xf1557, + 0xf1558: 0xf1558, + 0xf1559: 0xf1559, + 0xf155a: 0xf155a, + 0xf155b: 0xf155b, + 0xf155c: 0xf155c, + 0xf155d: 0xf155d, + 0xf155e: 0xf155e, + 0xf155f: 0xf155f, + 0xf1560: 0xf1560, + 0xf1561: 0xf1561, + 0xf1562: 0xf1562, + 0xf1563: 0xf1563, + 0xf1564: 0xf1564, + 0xf1565: 0xf1565, + 0xf1566: 0xf1566, + 0xf1567: 0xf1567, + 0xf1568: 0xf1568, + 0xf1569: 0xf1569, + 0xf156a: 0xf156a, + 0xf156b: 0xf156b, + 0xf156c: 0xf156c, + 0xf156d: 0xf156d, + 0xf156e: 0xf156e, + 0xf156f: 0xf156f, + 0xf1570: 0xf1570, + 0xf1571: 0xf1571, + 0xf1572: 0xf1572, + 0xf1573: 0xf1573, + 0xf1574: 0xf1574, + 0xf1575: 0xf1575, + 0xf1576: 0xf1576, + 0xf1577: 0xf1577, + 0xf1578: 0xf1578, + 0xf1579: 0xf1579, + 0xf157a: 0xf157a, + 0xf157b: 0xf157b, + 0xf157c: 0xf157c, + 0xf157d: 0xf157d, + 0xf157e: 0xf157e, + 0xf157f: 0xf157f, + 0xf1580: 0xf1580, + 0xf1581: 0xf1581, + 0xf1582: 0xf1582, + 0xf1583: 0xf1583, + 0xf1584: 0xf1584, + 0xf1585: 0xf1585, + 0xf1586: 0xf1586, + 0xf1587: 0xf1587, + 0xf1588: 0xf1588, + 0xf1589: 0xf1589, + 0xf158a: 0xf158a, + 0xf158b: 0xf158b, + 0xf158c: 0xf158c, + 0xf158d: 0xf158d, + 0xf158e: 0xf158e, + 0xf158f: 0xf158f, + 0xf1590: 0xf1590, + 0xf1591: 0xf1591, + 0xf1592: 0xf1592, + 0xf1593: 0xf1593, + 0xf1594: 0xf1594, + 0xf1595: 0xf1595, + 0xf1596: 0xf1596, + 0xf1597: 0xf1597, + 0xf1598: 0xf1598, + 0xf1599: 0xf1599, + 0xf159a: 0xf159a, + 0xf159b: 0xf159b, + 0xf159c: 0xf159c, + 0xf159d: 0xf159d, + 0xf159e: 0xf159e, + 0xf159f: 0xf159f, + 0xf15a0: 0xf15a0, + 0xf15a1: 0xf15a1, + 0xf15a2: 0xf15a2, + 0xf15a3: 0xf15a3, + 0xf15a4: 0xf15a4, + 0xf15a5: 0xf15a5, + 0xf15a6: 0xf15a6, + 0xf15a7: 0xf15a7, + 0xf15a8: 0xf15a8, + 0xf15a9: 0xf15a9, + 0xf15aa: 0xf15aa, + 0xf15ab: 0xf15ab, + 0xf15ac: 0xf15ac, + 0xf15ad: 0xf15ad, + 0xf15ae: 0xf15ae, + 0xf15af: 0xf15af, + 0xf15b0: 0xf15b0, + 0xf15b1: 0xf15b1, + 0xf15b2: 0xf15b2, + 0xf15b3: 0xf15b3, + 0xf15b4: 0xf15b4, + 0xf15b5: 0xf15b5, + 0xf15b6: 0xf15b6, + 0xf15b7: 0xf15b7, + 0xf15b8: 0xf15b8, + 0xf15b9: 0xf15b9, + 0xf15ba: 0xf15ba, + 0xf15bb: 0xf15bb, + 0xf15bc: 0xf15bc, + 0xf15bd: 0xf15bd, + 0xf15be: 0xf15be, + 0xf15bf: 0xf15bf, + 0xf15c0: 0xf15c0, + 0xf15c1: 0xf15c1, + 0xf15c2: 0xf15c2, + 0xf15c3: 0xf15c3, + 0xf15c4: 0xf15c4, + 0xf15c5: 0xf15c5, + 0xf15c6: 0xf15c6, + 0xf15c7: 0xf15c7, + 0xf15c8: 0xf15c8, + 0xf15c9: 0xf15c9, + 0xf15ca: 0xf15ca, + 0xf15cb: 0xf15cb, + 0xf15cc: 0xf15cc, + 0xf15cd: 0xf15cd, + 0xf15ce: 0xf15ce, + 0xf15cf: 0xf15cf, + 0xf15d0: 0xf15d0, + 0xf15d1: 0xf15d1, + 0xf15d2: 0xf15d2, + 0xf15d3: 0xf15d3, + 0xf15d4: 0xf15d4, + 0xf15d5: 0xf15d5, + 0xf15d6: 0xf15d6, + 0xf15d7: 0xf15d7, + 0xf15d8: 0xf15d8, + 0xf15d9: 0xf15d9, + 0xf15da: 0xf15da, + 0xf15db: 0xf15db, + 0xf15dc: 0xf15dc, + 0xf15dd: 0xf15dd, + 0xf15de: 0xf15de, + 0xf15df: 0xf15df, + 0xf15e0: 0xf15e0, + 0xf15e1: 0xf15e1, + 0xf15e2: 0xf15e2, + 0xf15e3: 0xf15e3, + 0xf15e4: 0xf15e4, + 0xf15e5: 0xf15e5, + 0xf15e6: 0xf15e6, + 0xf15e7: 0xf15e7, + 0xf15e8: 0xf15e8, + 0xf15e9: 0xf15e9, + 0xf15ea: 0xf15ea, + 0xf15eb: 0xf15eb, + 0xf15ec: 0xf15ec, + 0xf15ed: 0xf15ed, + 0xf15ee: 0xf15ee, + 0xf15ef: 0xf15ef, + 0xf15f0: 0xf15f0, + 0xf15f1: 0xf15f1, + 0xf15f2: 0xf15f2, + 0xf15f3: 0xf15f3, + 0xf15f4: 0xf15f4, + 0xf15f5: 0xf15f5, + 0xf15f6: 0xf15f6, + 0xf15f7: 0xf15f7, + 0xf15f8: 0xf15f8, + 0xf15f9: 0xf15f9, + 0xf15fa: 0xf15fa, + 0xf15fb: 0xf15fb, + 0xf15fc: 0xf15fc, + 0xf15fd: 0xf15fd, + 0xf15fe: 0xf15fe, + 0xf15ff: 0xf15ff, + 0xf1600: 0xf1600, + 0xf1601: 0xf1601, + 0xf1602: 0xf1602, + 0xf1603: 0xf1603, + 0xf1604: 0xf1604, + 0xf1605: 0xf1605, + 0xf1606: 0xf1606, + 0xf1607: 0xf1607, + 0xf1608: 0xf1608, + 0xf1609: 0xf1609, + 0xf160a: 0xf160a, + 0xf160b: 0xf160b, + 0xf160c: 0xf160c, + 0xf160d: 0xf160d, + 0xf160e: 0xf160e, + 0xf160f: 0xf160f, + 0xf1610: 0xf1610, + 0xf1611: 0xf1611, + 0xf1612: 0xf1612, + 0xf1613: 0xf1613, + 0xf1614: 0xf1614, + 0xf1615: 0xf1615, + 0xf1616: 0xf1616, + 0xf1617: 0xf1617, + 0xf1618: 0xf1618, + 0xf1619: 0xf1619, + 0xf161a: 0xf161a, + 0xf161b: 0xf161b, + 0xf161c: 0xf161c, + 0xf161d: 0xf161d, + 0xf161e: 0xf161e, + 0xf161f: 0xf161f, + 0xf1620: 0xf1620, + 0xf1621: 0xf1621, + 0xf1622: 0xf1622, + 0xf1623: 0xf1623, + 0xf1624: 0xf1624, + 0xf1625: 0xf1625, + 0xf1626: 0xf1626, + 0xf1627: 0xf1627, + 0xf1628: 0xf1628, + 0xf1629: 0xf1629, + 0xf162a: 0xf162a, + 0xf162b: 0xf162b, + 0xf162c: 0xf162c, + 0xf162d: 0xf162d, + 0xf162e: 0xf162e, + 0xf162f: 0xf162f, + 0xf1630: 0xf1630, + 0xf1631: 0xf1631, + 0xf1632: 0xf1632, + 0xf1633: 0xf1633, + 0xf1634: 0xf1634, + 0xf1635: 0xf1635, + 0xf1636: 0xf1636, + 0xf1637: 0xf1637, + 0xf1638: 0xf1638, + 0xf1639: 0xf1639, + 0xf163a: 0xf163a, + 0xf163b: 0xf163b, + 0xf163c: 0xf163c, + 0xf163d: 0xf163d, + 0xf163e: 0xf163e, + 0xf163f: 0xf163f, + 0xf1640: 0xf1640, + 0xf1641: 0xf1641, + 0xf1642: 0xf1642, + 0xf1643: 0xf1643, + 0xf1644: 0xf1644, + 0xf1645: 0xf1645, + 0xf1646: 0xf1646, + 0xf1647: 0xf1647, + 0xf1648: 0xf1648, + 0xf1649: 0xf1649, + 0xf164a: 0xf164a, + 0xf164b: 0xf164b, + 0xf164c: 0xf164c, + 0xf164d: 0xf164d, + 0xf164e: 0xf164e, + 0xf164f: 0xf164f, + 0xf1650: 0xf1650, + 0xf1651: 0xf1651, + 0xf1652: 0xf1652, + 0xf1653: 0xf1653, + 0xf1654: 0xf1654, + 0xf1655: 0xf1655, + 0xf1656: 0xf1656, + 0xf1657: 0xf1657, + 0xf1658: 0xf1658, + 0xf1659: 0xf1659, + 0xf165a: 0xf165a, + 0xf165b: 0xf165b, + 0xf165c: 0xf165c, + 0xf165d: 0xf165d, + 0xf165e: 0xf165e, + 0xf165f: 0xf165f, + 0xf1660: 0xf1660, + 0xf1661: 0xf1661, + 0xf1662: 0xf1662, + 0xf1663: 0xf1663, + 0xf1664: 0xf1664, + 0xf1665: 0xf1665, + 0xf1666: 0xf1666, + 0xf1667: 0xf1667, + 0xf1668: 0xf1668, + 0xf1669: 0xf1669, + 0xf166a: 0xf166a, + 0xf166b: 0xf166b, + 0xf166c: 0xf166c, + 0xf166d: 0xf166d, + 0xf166e: 0xf166e, + 0xf166f: 0xf166f, + 0xf1670: 0xf1670, + 0xf1671: 0xf1671, + 0xf1672: 0xf1672, + 0xf1673: 0xf1673, + 0xf1674: 0xf1674, + 0xf1675: 0xf1675, + 0xf1676: 0xf1676, + 0xf1677: 0xf1677, + 0xf1678: 0xf1678, + 0xf1679: 0xf1679, + 0xf167a: 0xf167a, + 0xf167b: 0xf167b, + 0xf167c: 0xf167c, + 0xf167d: 0xf167d, + 0xf167e: 0xf167e, + 0xf167f: 0xf167f, + 0xf1680: 0xf1680, + 0xf1681: 0xf1681, + 0xf1682: 0xf1682, + 0xf1683: 0xf1683, + 0xf1684: 0xf1684, + 0xf1685: 0xf1685, + 0xf1686: 0xf1686, + 0xf1687: 0xf1687, + 0xf1688: 0xf1688, + 0xf1689: 0xf1689, + 0xf168a: 0xf168a, + 0xf168b: 0xf168b, + 0xf168c: 0xf168c, + 0xf168d: 0xf168d, + 0xf168e: 0xf168e, + 0xf168f: 0xf168f, + 0xf1690: 0xf1690, + 0xf1691: 0xf1691, + 0xf1692: 0xf1692, + 0xf1693: 0xf1693, + 0xf1694: 0xf1694, + 0xf1695: 0xf1695, + 0xf1696: 0xf1696, + 0xf1697: 0xf1697, + 0xf1698: 0xf1698, + 0xf1699: 0xf1699, + 0xf169a: 0xf169a, + 0xf169b: 0xf169b, + 0xf169c: 0xf169c, + 0xf169d: 0xf169d, + 0xf169e: 0xf169e, + 0xf169f: 0xf169f, + 0xf16a0: 0xf16a0, + 0xf16a1: 0xf16a1, + 0xf16a2: 0xf16a2, + 0xf16a3: 0xf16a3, + 0xf16a4: 0xf16a4, + 0xf16a5: 0xf16a5, + 0xf16a6: 0xf16a6, + 0xf16a7: 0xf16a7, + 0xf16a8: 0xf16a8, + 0xf16a9: 0xf16a9, + 0xf16aa: 0xf16aa, + 0xf16ab: 0xf16ab, + 0xf16ac: 0xf16ac, + 0xf16ad: 0xf16ad, + 0xf16ae: 0xf16ae, + 0xf16af: 0xf16af, + 0xf16b0: 0xf16b0, + 0xf16b1: 0xf16b1, + 0xf16b2: 0xf16b2, + 0xf16b3: 0xf16b3, + 0xf16b4: 0xf16b4, + 0xf16b5: 0xf16b5, + 0xf16b6: 0xf16b6, + 0xf16b7: 0xf16b7, + 0xf16b8: 0xf16b8, + 0xf16b9: 0xf16b9, + 0xf16ba: 0xf16ba, + 0xf16bb: 0xf16bb, + 0xf16bc: 0xf16bc, + 0xf16bd: 0xf16bd, + 0xf16be: 0xf16be, + 0xf16bf: 0xf16bf, + 0xf16c0: 0xf16c0, + 0xf16c1: 0xf16c1, + 0xf16c2: 0xf16c2, + 0xf16c3: 0xf16c3, + 0xf16c4: 0xf16c4, + 0xf16c5: 0xf16c5, + 0xf16c6: 0xf16c6, + 0xf16c7: 0xf16c7, + 0xf16c8: 0xf16c8, + 0xf16c9: 0xf16c9, + 0xf16ca: 0xf16ca, + 0xf16cb: 0xf16cb, + 0xf16cc: 0xf16cc, + 0xf16cd: 0xf16cd, + 0xf16ce: 0xf16ce, + 0xf16cf: 0xf16cf, + 0xf16d0: 0xf16d0, + 0xf16d1: 0xf16d1, + 0xf16d2: 0xf16d2, + 0xf16d3: 0xf16d3, + 0xf16d4: 0xf16d4, + 0xf16d5: 0xf16d5, + 0xf16d6: 0xf16d6, + 0xf16d7: 0xf16d7, + 0xf16d8: 0xf16d8, + 0xf16d9: 0xf16d9, + 0xf16da: 0xf16da, + 0xf16db: 0xf16db, + 0xf16dc: 0xf16dc, + 0xf16dd: 0xf16dd, + 0xf16de: 0xf16de, + 0xf16df: 0xf16df, + 0xf16e0: 0xf16e0, + 0xf16e1: 0xf16e1, + 0xf16e2: 0xf16e2, + 0xf16e3: 0xf16e3, + 0xf16e4: 0xf16e4, + 0xf16e5: 0xf16e5, + 0xf16e6: 0xf16e6, + 0xf16e7: 0xf16e7, + 0xf16e8: 0xf16e8, + 0xf16e9: 0xf16e9, + 0xf16ea: 0xf16ea, + 0xf16eb: 0xf16eb, + 0xf16ec: 0xf16ec, + 0xf16ed: 0xf16ed, + 0xf16ee: 0xf16ee, + 0xf16ef: 0xf16ef, + 0xf16f0: 0xf16f0, + 0xf16f1: 0xf16f1, + 0xf16f2: 0xf16f2, + 0xf16f3: 0xf16f3, + 0xf16f4: 0xf16f4, + 0xf16f5: 0xf16f5, + 0xf16f6: 0xf16f6, + 0xf16f7: 0xf16f7, + 0xf16f8: 0xf16f8, + 0xf16f9: 0xf16f9, + 0xf16fa: 0xf16fa, + 0xf16fb: 0xf16fb, + 0xf16fc: 0xf16fc, + 0xf16fd: 0xf16fd, + 0xf16fe: 0xf16fe, + 0xf16ff: 0xf16ff, + 0xf1700: 0xf1700, + 0xf1701: 0xf1701, + 0xf1702: 0xf1702, + 0xf1703: 0xf1703, + 0xf1704: 0xf1704, + 0xf1705: 0xf1705, + 0xf1706: 0xf1706, + 0xf1707: 0xf1707, + 0xf1708: 0xf1708, + 0xf1709: 0xf1709, + 0xf170a: 0xf170a, + 0xf170b: 0xf170b, + 0xf170c: 0xf170c, + 0xf170d: 0xf170d, + 0xf170e: 0xf170e, + 0xf170f: 0xf170f, + 0xf1710: 0xf1710, + 0xf1711: 0xf1711, + 0xf1712: 0xf1712, + 0xf1713: 0xf1713, + 0xf1714: 0xf1714, + 0xf1715: 0xf1715, + 0xf1716: 0xf1716, + 0xf1717: 0xf1717, + 0xf1718: 0xf1718, + 0xf1719: 0xf1719, + 0xf171a: 0xf171a, + 0xf171b: 0xf171b, + 0xf171c: 0xf171c, + 0xf171d: 0xf171d, + 0xf171e: 0xf171e, + 0xf171f: 0xf171f, + 0xf1720: 0xf1720, + 0xf1721: 0xf1721, + 0xf1722: 0xf1722, + 0xf1723: 0xf1723, + 0xf1724: 0xf1724, + 0xf1725: 0xf1725, + 0xf1726: 0xf1726, + 0xf1727: 0xf1727, + 0xf1728: 0xf1728, + 0xf1729: 0xf1729, + 0xf172a: 0xf172a, + 0xf172b: 0xf172b, + 0xf172c: 0xf172c, + 0xf172d: 0xf172d, + 0xf172e: 0xf172e, + 0xf172f: 0xf172f, + 0xf1730: 0xf1730, + 0xf1731: 0xf1731, + 0xf1732: 0xf1732, + 0xf1733: 0xf1733, + 0xf1734: 0xf1734, + 0xf1735: 0xf1735, + 0xf1736: 0xf1736, + 0xf1737: 0xf1737, + 0xf1738: 0xf1738, + 0xf1739: 0xf1739, + 0xf173a: 0xf173a, + 0xf173b: 0xf173b, + 0xf173c: 0xf173c, + 0xf173d: 0xf173d, + 0xf173e: 0xf173e, + 0xf173f: 0xf173f, + 0xf1740: 0xf1740, + 0xf1741: 0xf1741, + 0xf1742: 0xf1742, + 0xf1743: 0xf1743, + 0xf1744: 0xf1744, + 0xf1745: 0xf1745, + 0xf1746: 0xf1746, + 0xf1747: 0xf1747, + 0xf1748: 0xf1748, + 0xf1749: 0xf1749, + 0xf174a: 0xf174a, + 0xf174b: 0xf174b, + 0xf174c: 0xf174c, + 0xf174d: 0xf174d, + 0xf174e: 0xf174e, + 0xf174f: 0xf174f, + 0xf1750: 0xf1750, + 0xf1751: 0xf1751, + 0xf1752: 0xf1752, + 0xf1753: 0xf1753, + 0xf1754: 0xf1754, + 0xf1755: 0xf1755, + 0xf1756: 0xf1756, + 0xf1757: 0xf1757, + 0xf1758: 0xf1758, + 0xf1759: 0xf1759, + 0xf175a: 0xf175a, + 0xf175b: 0xf175b, + 0xf175c: 0xf175c, + 0xf175d: 0xf175d, + 0xf175e: 0xf175e, + 0xf175f: 0xf175f, + 0xf1760: 0xf1760, + 0xf1761: 0xf1761, + 0xf1762: 0xf1762, + 0xf1763: 0xf1763, + 0xf1764: 0xf1764, + 0xf1765: 0xf1765, + 0xf1766: 0xf1766, + 0xf1767: 0xf1767, + 0xf1768: 0xf1768, + 0xf1769: 0xf1769, + 0xf176a: 0xf176a, + 0xf176b: 0xf176b, + 0xf176c: 0xf176c, + 0xf176d: 0xf176d, + 0xf176e: 0xf176e, + 0xf176f: 0xf176f, + 0xf1770: 0xf1770, + 0xf1771: 0xf1771, + 0xf1772: 0xf1772, + 0xf1773: 0xf1773, + 0xf1774: 0xf1774, + 0xf1775: 0xf1775, + 0xf1776: 0xf1776, + 0xf1777: 0xf1777, + 0xf1778: 0xf1778, + 0xf1779: 0xf1779, + 0xf177a: 0xf177a, + 0xf177b: 0xf177b, + 0xf177c: 0xf177c, + 0xf177d: 0xf177d, + 0xf177e: 0xf177e, + 0xf177f: 0xf177f, + 0xf1780: 0xf1780, + 0xf1781: 0xf1781, + 0xf1782: 0xf1782, + 0xf1783: 0xf1783, + 0xf1784: 0xf1784, + 0xf1785: 0xf1785, + 0xf1786: 0xf1786, + 0xf1787: 0xf1787, + 0xf1788: 0xf1788, + 0xf1789: 0xf1789, + 0xf178a: 0xf178a, + 0xf178b: 0xf178b, + 0xf178c: 0xf178c, + 0xf178d: 0xf178d, + 0xf178e: 0xf178e, + 0xf178f: 0xf178f, + 0xf1790: 0xf1790, + 0xf1791: 0xf1791, + 0xf1792: 0xf1792, + 0xf1793: 0xf1793, + 0xf1794: 0xf1794, + 0xf1795: 0xf1795, + 0xf1796: 0xf1796, + 0xf1797: 0xf1797, + 0xf1798: 0xf1798, + 0xf1799: 0xf1799, + 0xf179a: 0xf179a, + 0xf179b: 0xf179b, + 0xf179c: 0xf179c, + 0xf179d: 0xf179d, + 0xf179e: 0xf179e, + 0xf179f: 0xf179f, + 0xf17a0: 0xf17a0, + 0xf17a1: 0xf17a1, + 0xf17a2: 0xf17a2, + 0xf17a3: 0xf17a3, + 0xf17a4: 0xf17a4, + 0xf17a5: 0xf17a5, + 0xf17a6: 0xf17a6, + 0xf17a7: 0xf17a7, + 0xf17a8: 0xf17a8, + 0xf17a9: 0xf17a9, + 0xf17aa: 0xf17aa, + 0xf17ab: 0xf17ab, + 0xf17ac: 0xf17ac, + 0xf17ad: 0xf17ad, + 0xf17ae: 0xf17ae, + 0xf17af: 0xf17af, + 0xf17b0: 0xf17b0, + 0xf17b1: 0xf17b1, + 0xf17b2: 0xf17b2, + 0xf17b3: 0xf17b3, + 0xf17b4: 0xf17b4, + 0xf17b5: 0xf17b5, + 0xf17b6: 0xf17b6, + 0xf17b7: 0xf17b7, + 0xf17b8: 0xf17b8, + 0xf17b9: 0xf17b9, + 0xf17ba: 0xf17ba, + 0xf17bb: 0xf17bb, + 0xf17bc: 0xf17bc, + 0xf17bd: 0xf17bd, + 0xf17be: 0xf17be, + 0xf17bf: 0xf17bf, + 0xf17c0: 0xf17c0, + 0xf17c1: 0xf17c1, + 0xf17c2: 0xf17c2, + 0xf17c3: 0xf17c3, + 0xf17c4: 0xf17c4, + 0xf17c5: 0xf17c5, + 0xf17c6: 0xf17c6, + 0xf17c7: 0xf17c7, + 0xf17c8: 0xf17c8, + 0xf17c9: 0xf17c9, + 0xf17ca: 0xf17ca, + 0xf17cb: 0xf17cb, + 0xf17cc: 0xf17cc, + 0xf17cd: 0xf17cd, + 0xf17ce: 0xf17ce, + 0xf17cf: 0xf17cf, + 0xf17d0: 0xf17d0, + 0xf17d1: 0xf17d1, + 0xf17d2: 0xf17d2, + 0xf17d3: 0xf17d3, + 0xf17d4: 0xf17d4, + 0xf17d5: 0xf17d5, + 0xf17d6: 0xf17d6, + 0xf17d7: 0xf17d7, + 0xf17d8: 0xf17d8, + 0xf17d9: 0xf17d9, + 0xf17da: 0xf17da, + 0xf17db: 0xf17db, + 0xf17dc: 0xf17dc, + 0xf17dd: 0xf17dd, + 0xf17de: 0xf17de, + 0xf17df: 0xf17df, + 0xf17e0: 0xf17e0, + 0xf17e1: 0xf17e1, + 0xf17e2: 0xf17e2, + 0xf17e3: 0xf17e3, + 0xf17e4: 0xf17e4, + 0xf17e5: 0xf17e5, + 0xf17e6: 0xf17e6, + 0xf17e7: 0xf17e7, + 0xf17e8: 0xf17e8, + 0xf17e9: 0xf17e9, + 0xf17ea: 0xf17ea, + 0xf17eb: 0xf17eb, + 0xf17ec: 0xf17ec, + 0xf17ed: 0xf17ed, + 0xf17ee: 0xf17ee, + 0xf17ef: 0xf17ef, + 0xf17f0: 0xf17f0, + 0xf17f1: 0xf17f1, + 0xf17f2: 0xf17f2, + 0xf17f3: 0xf17f3, + 0xf17f4: 0xf17f4, + 0xf17f5: 0xf17f5, + 0xf17f6: 0xf17f6, + 0xf17f7: 0xf17f7, + 0xf17f8: 0xf17f8, + 0xf17f9: 0xf17f9, + 0xf17fa: 0xf17fa, + 0xf17fb: 0xf17fb, + 0xf17fc: 0xf17fc, + 0xf17fd: 0xf17fd, + 0xf17fe: 0xf17fe, + 0xf17ff: 0xf17ff, + 0xf1800: 0xf1800, + 0xf1801: 0xf1801, + 0xf1802: 0xf1802, + 0xf1803: 0xf1803, + 0xf1804: 0xf1804, + 0xf1805: 0xf1805, + 0xf1806: 0xf1806, + 0xf1807: 0xf1807, + 0xf1808: 0xf1808, + 0xf1809: 0xf1809, + 0xf180a: 0xf180a, + 0xf180b: 0xf180b, + 0xf180c: 0xf180c, + 0xf180d: 0xf180d, + 0xf180e: 0xf180e, + 0xf180f: 0xf180f, + 0xf1810: 0xf1810, + 0xf1811: 0xf1811, + 0xf1812: 0xf1812, + 0xf1813: 0xf1813, + 0xf1814: 0xf1814, + 0xf1815: 0xf1815, + 0xf1816: 0xf1816, + 0xf1817: 0xf1817, + 0xf1818: 0xf1818, + 0xf1819: 0xf1819, + 0xf181a: 0xf181a, + 0xf181b: 0xf181b, + 0xf181c: 0xf181c, + 0xf181d: 0xf181d, + 0xf181e: 0xf181e, + 0xf181f: 0xf181f, + 0xf1820: 0xf1820, + 0xf1821: 0xf1821, + 0xf1822: 0xf1822, + 0xf1823: 0xf1823, + 0xf1824: 0xf1824, + 0xf1825: 0xf1825, + 0xf1826: 0xf1826, + 0xf1827: 0xf1827, + 0xf1828: 0xf1828, + 0xf1829: 0xf1829, + 0xf182a: 0xf182a, + 0xf182b: 0xf182b, + 0xf182c: 0xf182c, + 0xf182d: 0xf182d, + 0xf182e: 0xf182e, + 0xf182f: 0xf182f, + 0xf1830: 0xf1830, + 0xf1831: 0xf1831, + 0xf1832: 0xf1832, + 0xf1833: 0xf1833, + 0xf1834: 0xf1834, + 0xf1835: 0xf1835, + 0xf1836: 0xf1836, + 0xf1837: 0xf1837, + 0xf1838: 0xf1838, + 0xf1839: 0xf1839, + 0xf183a: 0xf183a, + 0xf183b: 0xf183b, + 0xf183c: 0xf183c, + 0xf183d: 0xf183d, + 0xf183e: 0xf183e, + 0xf183f: 0xf183f, + 0xf1840: 0xf1840, + 0xf1841: 0xf1841, + 0xf1842: 0xf1842, + 0xf1843: 0xf1843, + 0xf1844: 0xf1844, + 0xf1845: 0xf1845, + 0xf1846: 0xf1846, + 0xf1847: 0xf1847, + 0xf1848: 0xf1848, + 0xf1849: 0xf1849, + 0xf184a: 0xf184a, + 0xf184b: 0xf184b, + 0xf184c: 0xf184c, + 0xf184d: 0xf184d, + 0xf184e: 0xf184e, + 0xf184f: 0xf184f, + 0xf1850: 0xf1850, + 0xf1851: 0xf1851, + 0xf1852: 0xf1852, + 0xf1853: 0xf1853, + 0xf1854: 0xf1854, + 0xf1855: 0xf1855, + 0xf1856: 0xf1856, + 0xf1857: 0xf1857, + 0xf1858: 0xf1858, + 0xf1859: 0xf1859, + 0xf185a: 0xf185a, + 0xf185b: 0xf185b, + 0xf185c: 0xf185c, + 0xf185d: 0xf185d, + 0xf185e: 0xf185e, + 0xf185f: 0xf185f, + 0xf1860: 0xf1860, + 0xf1861: 0xf1861, + 0xf1862: 0xf1862, + 0xf1863: 0xf1863, + 0xf1864: 0xf1864, + 0xf1865: 0xf1865, + 0xf1866: 0xf1866, + 0xf1867: 0xf1867, + 0xf1868: 0xf1868, + 0xf1869: 0xf1869, + 0xf186a: 0xf186a, + 0xf186b: 0xf186b, + 0xf186c: 0xf186c, + 0xf186d: 0xf186d, + 0xf186e: 0xf186e, + 0xf186f: 0xf186f, + 0xf1870: 0xf1870, + 0xf1871: 0xf1871, + 0xf1872: 0xf1872, + 0xf1873: 0xf1873, + 0xf1874: 0xf1874, + 0xf1875: 0xf1875, + 0xf1876: 0xf1876, + 0xf1877: 0xf1877, + 0xf1878: 0xf1878, + 0xf1879: 0xf1879, + 0xf187a: 0xf187a, + 0xf187b: 0xf187b, + 0xf187c: 0xf187c, + 0xf187d: 0xf187d, + 0xf187e: 0xf187e, + 0xf187f: 0xf187f, + 0xf1880: 0xf1880, + 0xf1881: 0xf1881, + 0xf1882: 0xf1882, + 0xf1883: 0xf1883, + 0xf1884: 0xf1884, + 0xf1885: 0xf1885, + 0xf1886: 0xf1886, + 0xf1887: 0xf1887, + 0xf1888: 0xf1888, + 0xf1889: 0xf1889, + 0xf188a: 0xf188a, + 0xf188b: 0xf188b, + 0xf188c: 0xf188c, + 0xf188d: 0xf188d, + 0xf188e: 0xf188e, + 0xf188f: 0xf188f, + 0xf1890: 0xf1890, + 0xf1891: 0xf1891, + 0xf1892: 0xf1892, + 0xf1893: 0xf1893, + 0xf1894: 0xf1894, + 0xf1895: 0xf1895, + 0xf1896: 0xf1896, + 0xf1897: 0xf1897, + 0xf1898: 0xf1898, + 0xf1899: 0xf1899, + 0xf189a: 0xf189a, + 0xf189b: 0xf189b, + 0xf189c: 0xf189c, + 0xf189d: 0xf189d, + 0xf189e: 0xf189e, + 0xf189f: 0xf189f, + 0xf18a0: 0xf18a0, + 0xf18a1: 0xf18a1, + 0xf18a2: 0xf18a2, + 0xf18a3: 0xf18a3, + 0xf18a4: 0xf18a4, + 0xf18a5: 0xf18a5, + 0xf18a6: 0xf18a6, + 0xf18a7: 0xf18a7, + 0xf18a8: 0xf18a8, + 0xf18a9: 0xf18a9, + 0xf18aa: 0xf18aa, + 0xf18ab: 0xf18ab, + 0xf18ac: 0xf18ac, + 0xf18ad: 0xf18ad, + 0xf18ae: 0xf18ae, + 0xf18af: 0xf18af, + 0xf18b0: 0xf18b0, + 0xf18b1: 0xf18b1, + 0xf18b2: 0xf18b2, + 0xf18b3: 0xf18b3, + 0xf18b4: 0xf18b4, + 0xf18b5: 0xf18b5, + 0xf18b6: 0xf18b6, + 0xf18b7: 0xf18b7, + 0xf18b8: 0xf18b8, + 0xf18b9: 0xf18b9, + 0xf18ba: 0xf18ba, + 0xf18bb: 0xf18bb, + 0xf18bc: 0xf18bc, + 0xf18bd: 0xf18bd, + 0xf18be: 0xf18be, + 0xf18bf: 0xf18bf, + 0xf18c0: 0xf18c0, + 0xf18c1: 0xf18c1, + 0xf18c2: 0xf18c2, + 0xf18c3: 0xf18c3, + 0xf18c4: 0xf18c4, + 0xf18c5: 0xf18c5, + 0xf18c6: 0xf18c6, + 0xf18c7: 0xf18c7, + 0xf18c8: 0xf18c8, + 0xf18c9: 0xf18c9, + 0xf18ca: 0xf18ca, + 0xf18cb: 0xf18cb, + 0xf18cc: 0xf18cc, + 0xf18cd: 0xf18cd, + 0xf18ce: 0xf18ce, + 0xf18cf: 0xf18cf, + 0xf18d0: 0xf18d0, + 0xf18d1: 0xf18d1, + 0xf18d2: 0xf18d2, + 0xf18d3: 0xf18d3, + 0xf18d4: 0xf18d4, + 0xf18d5: 0xf18d5, + 0xf18d6: 0xf18d6, + 0xf18d7: 0xf18d7, + 0xf18d8: 0xf18d8, + 0xf18d9: 0xf18d9, + 0xf18da: 0xf18da, + 0xf18db: 0xf18db, + 0xf18dc: 0xf18dc, + 0xf18dd: 0xf18dd, + 0xf18de: 0xf18de, + 0xf18df: 0xf18df, + 0xf18e0: 0xf18e0, + 0xf18e1: 0xf18e1, + 0xf18e2: 0xf18e2, + 0xf18e3: 0xf18e3, + 0xf18e4: 0xf18e4, + 0xf18e5: 0xf18e5, + 0xf18e6: 0xf18e6, + 0xf18e7: 0xf18e7, + 0xf18e8: 0xf18e8, + 0xf18e9: 0xf18e9, + 0xf18ea: 0xf18ea, + 0xf18eb: 0xf18eb, + 0xf18ec: 0xf18ec, + 0xf18ed: 0xf18ed, + 0xf18ee: 0xf18ee, + 0xf18ef: 0xf18ef, + 0xf18f0: 0xf18f0, + 0xf18f1: 0xf18f1, + 0xf18f2: 0xf18f2, + 0xf18f3: 0xf18f3, + 0xf18f4: 0xf18f4, + 0xf18f5: 0xf18f5, + 0xf18f6: 0xf18f6, + 0xf18f7: 0xf18f7, + 0xf18f8: 0xf18f8, + 0xf18f9: 0xf18f9, + 0xf18fa: 0xf18fa, + 0xf18fb: 0xf18fb, + 0xf18fc: 0xf18fc, + 0xf18fd: 0xf18fd, + 0xf18fe: 0xf18fe, + 0xf18ff: 0xf18ff, + 0xf1900: 0xf1900, + 0xf1901: 0xf1901, + 0xf1902: 0xf1902, + 0xf1903: 0xf1903, + 0xf1904: 0xf1904, + 0xf1905: 0xf1905, + 0xf1906: 0xf1906, + 0xf1907: 0xf1907, + 0xf1908: 0xf1908, + 0xf1909: 0xf1909, + 0xf190a: 0xf190a, + 0xf190b: 0xf190b, + 0xf190c: 0xf190c, + 0xf190d: 0xf190d, + 0xf190e: 0xf190e, + 0xf190f: 0xf190f, + 0xf1910: 0xf1910, + 0xf1911: 0xf1911, + 0xf1912: 0xf1912, + 0xf1913: 0xf1913, + 0xf1914: 0xf1914, + 0xf1915: 0xf1915, + 0xf1916: 0xf1916, + 0xf1917: 0xf1917, + 0xf1918: 0xf1918, + 0xf1919: 0xf1919, + 0xf191a: 0xf191a, + 0xf191b: 0xf191b, + 0xf191c: 0xf191c, + 0xf191d: 0xf191d, + 0xf191e: 0xf191e, + 0xf191f: 0xf191f, + 0xf1920: 0xf1920, + 0xf1921: 0xf1921, + 0xf1922: 0xf1922, + 0xf1923: 0xf1923, + 0xf1924: 0xf1924, + 0xf1925: 0xf1925, + 0xf1926: 0xf1926, + 0xf1927: 0xf1927, + 0xf1928: 0xf1928, + 0xf1929: 0xf1929, + 0xf192a: 0xf192a, + 0xf192b: 0xf192b, + 0xf192c: 0xf192c, + 0xf192d: 0xf192d, + 0xf192e: 0xf192e, + 0xf192f: 0xf192f, + 0xf1930: 0xf1930, + 0xf1931: 0xf1931, + 0xf1932: 0xf1932, + 0xf1933: 0xf1933, + 0xf1934: 0xf1934, + 0xf1935: 0xf1935, + 0xf1936: 0xf1936, + 0xf1937: 0xf1937, + 0xf1938: 0xf1938, + 0xf1939: 0xf1939, + 0xf193a: 0xf193a, + 0xf193b: 0xf193b, + 0xf193c: 0xf193c, + 0xf193d: 0xf193d, + 0xf193e: 0xf193e, + 0xf193f: 0xf193f, + 0xf1940: 0xf1940, + 0xf1941: 0xf1941, + 0xf1942: 0xf1942, + 0xf1943: 0xf1943, + 0xf1944: 0xf1944, + 0xf1945: 0xf1945, + 0xf1946: 0xf1946, + 0xf1947: 0xf1947, + 0xf1948: 0xf1948, + 0xf1949: 0xf1949, + 0xf194a: 0xf194a, + 0xf194b: 0xf194b, + 0xf194c: 0xf194c, + 0xf194d: 0xf194d, + 0xf194e: 0xf194e, + 0xf194f: 0xf194f, + 0xf1950: 0xf1950, + 0xf1951: 0xf1951, + 0xf1952: 0xf1952, + 0xf1953: 0xf1953, + 0xf1954: 0xf1954, + 0xf1955: 0xf1955, + 0xf1956: 0xf1956, + 0xf1957: 0xf1957, + 0xf1958: 0xf1958, + 0xf1959: 0xf1959, + 0xf195a: 0xf195a, + 0xf195b: 0xf195b, + 0xf195c: 0xf195c, + 0xf195d: 0xf195d, + 0xf195e: 0xf195e, + 0xf195f: 0xf195f, + 0xf1960: 0xf1960, + 0xf1961: 0xf1961, + 0xf1962: 0xf1962, + 0xf1963: 0xf1963, + 0xf1964: 0xf1964, + 0xf1965: 0xf1965, + 0xf1966: 0xf1966, + 0xf1967: 0xf1967, + 0xf1968: 0xf1968, + 0xf1969: 0xf1969, + 0xf196a: 0xf196a, + 0xf196b: 0xf196b, + 0xf196c: 0xf196c, + 0xf196d: 0xf196d, + 0xf196e: 0xf196e, + 0xf196f: 0xf196f, + 0xf1970: 0xf1970, + 0xf1971: 0xf1971, + 0xf1972: 0xf1972, + 0xf1973: 0xf1973, + 0xf1974: 0xf1974, + 0xf1975: 0xf1975, + 0xf1976: 0xf1976, + 0xf1977: 0xf1977, + 0xf1978: 0xf1978, + 0xf1979: 0xf1979, + 0xf197a: 0xf197a, + 0xf197b: 0xf197b, + 0xf197c: 0xf197c, + 0xf197d: 0xf197d, + 0xf197e: 0xf197e, + 0xf197f: 0xf197f, + 0xf1980: 0xf1980, + 0xf1981: 0xf1981, + 0xf1982: 0xf1982, + 0xf1983: 0xf1983, + 0xf1984: 0xf1984, + 0xf1985: 0xf1985, + 0xf1986: 0xf1986, + 0xf1987: 0xf1987, + 0xf1988: 0xf1988, + 0xf1989: 0xf1989, + 0xf198a: 0xf198a, + 0xf198b: 0xf198b, + 0xf198c: 0xf198c, + 0xf198d: 0xf198d, + 0xf198e: 0xf198e, + 0xf198f: 0xf198f, + 0xf1990: 0xf1990, + 0xf1991: 0xf1991, + 0xf1992: 0xf1992, + 0xf1993: 0xf1993, + 0xf1994: 0xf1994, + 0xf1995: 0xf1995, + 0xf1996: 0xf1996, + 0xf1997: 0xf1997, + 0xf1998: 0xf1998, + 0xf1999: 0xf1999, + 0xf199a: 0xf199a, + 0xf199b: 0xf199b, + 0xf199c: 0xf199c, + 0xf199d: 0xf199d, + 0xf199e: 0xf199e, + 0xf199f: 0xf199f, + 0xf19a0: 0xf19a0, + 0xf19a1: 0xf19a1, + 0xf19a2: 0xf19a2, + 0xf19a3: 0xf19a3, + 0xf19a4: 0xf19a4, + 0xf19a5: 0xf19a5, + 0xf19a6: 0xf19a6, + 0xf19a7: 0xf19a7, + 0xf19a8: 0xf19a8, + 0xf19a9: 0xf19a9, + 0xf19aa: 0xf19aa, + 0xf19ab: 0xf19ab, + 0xf19ac: 0xf19ac, + 0xf19ad: 0xf19ad, + 0xf19ae: 0xf19ae, + 0xf19af: 0xf19af, + 0xf19b0: 0xf19b0, + 0xf19b1: 0xf19b1, + 0xf19b2: 0xf19b2, + 0xf19b3: 0xf19b3, + 0xf19b4: 0xf19b4, + 0xf19b5: 0xf19b5, + 0xf19b6: 0xf19b6, + 0xf19b7: 0xf19b7, + 0xf19b8: 0xf19b8, + 0xf19b9: 0xf19b9, + 0xf19ba: 0xf19ba, + 0xf19bb: 0xf19bb, + 0xf19bc: 0xf19bc, + 0xf19bd: 0xf19bd, + 0xf19be: 0xf19be, + 0xf19bf: 0xf19bf, + 0xf19c0: 0xf19c0, + 0xf19c1: 0xf19c1, + 0xf19c2: 0xf19c2, + 0xf19c3: 0xf19c3, + 0xf19c4: 0xf19c4, + 0xf19c5: 0xf19c5, + 0xf19c6: 0xf19c6, + 0xf19c7: 0xf19c7, + 0xf19c8: 0xf19c8, + 0xf19c9: 0xf19c9, + 0xf19ca: 0xf19ca, + 0xf19cb: 0xf19cb, + 0xf19cc: 0xf19cc, + 0xf19cd: 0xf19cd, + 0xf19ce: 0xf19ce, + 0xf19cf: 0xf19cf, + 0xf19d0: 0xf19d0, + 0xf19d1: 0xf19d1, + 0xf19d2: 0xf19d2, + 0xf19d3: 0xf19d3, + 0xf19d4: 0xf19d4, + 0xf19d5: 0xf19d5, + 0xf19d6: 0xf19d6, + 0xf19d7: 0xf19d7, + 0xf19d8: 0xf19d8, + 0xf19d9: 0xf19d9, + 0xf19da: 0xf19da, + 0xf19db: 0xf19db, + 0xf19dc: 0xf19dc, + 0xf19dd: 0xf19dd, + 0xf19de: 0xf19de, + 0xf19df: 0xf19df, + 0xf19e0: 0xf19e0, + 0xf19e1: 0xf19e1, + 0xf19e2: 0xf19e2, + 0xf19e3: 0xf19e3, + 0xf19e4: 0xf19e4, + 0xf19e5: 0xf19e5, + 0xf19e6: 0xf19e6, + 0xf19e7: 0xf19e7, + 0xf19e8: 0xf19e8, + 0xf19e9: 0xf19e9, + 0xf19ea: 0xf19ea, + 0xf19eb: 0xf19eb, + 0xf19ec: 0xf19ec, + 0xf19ed: 0xf19ed, + 0xf19ee: 0xf19ee, + 0xf19ef: 0xf19ef, + 0xf19f0: 0xf19f0, + 0xf19f1: 0xf19f1, + 0xf19f2: 0xf19f2, + 0xf19f3: 0xf19f3, + 0xf19f4: 0xf19f4, + 0xf19f5: 0xf19f5, + 0xf19f6: 0xf19f6, + 0xf19f7: 0xf19f7, + 0xf19f8: 0xf19f8, + 0xf19f9: 0xf19f9, + 0xf19fa: 0xf19fa, + 0xf19fb: 0xf19fb, + 0xf19fc: 0xf19fc, + 0xf19fd: 0xf19fd, + 0xf19fe: 0xf19fe, + 0xf19ff: 0xf19ff, + 0xf1a00: 0xf1a00, + 0xf1a01: 0xf1a01, + 0xf1a02: 0xf1a02, + 0xf1a03: 0xf1a03, + 0xf1a04: 0xf1a04, + 0xf1a05: 0xf1a05, + 0xf1a06: 0xf1a06, + 0xf1a07: 0xf1a07, + 0xf1a08: 0xf1a08, + 0xf1a09: 0xf1a09, + 0xf1a0a: 0xf1a0a, + 0xf1a0b: 0xf1a0b, + 0xf1a0c: 0xf1a0c, + 0xf1a0d: 0xf1a0d, + 0xf1a0e: 0xf1a0e, + 0xf1a0f: 0xf1a0f, + 0xf1a10: 0xf1a10, + 0xf1a11: 0xf1a11, + 0xf1a12: 0xf1a12, + 0xf1a13: 0xf1a13, + 0xf1a14: 0xf1a14, + 0xf1a15: 0xf1a15, + 0xf1a16: 0xf1a16, + 0xf1a17: 0xf1a17, + 0xf1a18: 0xf1a18, + 0xf1a19: 0xf1a19, + 0xf1a1a: 0xf1a1a, + 0xf1a1b: 0xf1a1b, + 0xf1a1c: 0xf1a1c, + 0xf1a1d: 0xf1a1d, + 0xf1a1e: 0xf1a1e, + 0xf1a1f: 0xf1a1f, + 0xf1a20: 0xf1a20, + 0xf1a21: 0xf1a21, + 0xf1a22: 0xf1a22, + 0xf1a23: 0xf1a23, + 0xf1a24: 0xf1a24, + 0xf1a25: 0xf1a25, + 0xf1a26: 0xf1a26, + 0xf1a27: 0xf1a27, + 0xf1a28: 0xf1a28, + 0xf1a29: 0xf1a29, + 0xf1a2a: 0xf1a2a, + 0xf1a2b: 0xf1a2b, + 0xf1a2c: 0xf1a2c, + 0xf1a2d: 0xf1a2d, + 0xf1a2e: 0xf1a2e, + 0xf1a2f: 0xf1a2f, + 0xf1a30: 0xf1a30, + 0xf1a31: 0xf1a31, + 0xf1a32: 0xf1a32, + 0xf1a33: 0xf1a33, + 0xf1a34: 0xf1a34, + 0xf1a35: 0xf1a35, + 0xf1a36: 0xf1a36, + 0xf1a37: 0xf1a37, + 0xf1a38: 0xf1a38, + 0xf1a39: 0xf1a39, + 0xf1a3a: 0xf1a3a, + 0xf1a3b: 0xf1a3b, + 0xf1a3c: 0xf1a3c, + 0xf1a3d: 0xf1a3d, + 0xf1a3e: 0xf1a3e, + 0xf1a3f: 0xf1a3f, + 0xf1a40: 0xf1a40, + 0xf1a41: 0xf1a41, + 0xf1a42: 0xf1a42, + 0xf1a43: 0xf1a43, + 0xf1a44: 0xf1a44, + 0xf1a45: 0xf1a45, + 0xf1a46: 0xf1a46, + 0xf1a47: 0xf1a47, + 0xf1a48: 0xf1a48, + 0xf1a49: 0xf1a49, + 0xf1a4a: 0xf1a4a, + 0xf1a4b: 0xf1a4b, + 0xf1a4c: 0xf1a4c, + 0xf1a4d: 0xf1a4d, + 0xf1a4e: 0xf1a4e, + 0xf1a4f: 0xf1a4f, + 0xf1a50: 0xf1a50, + 0xf1a51: 0xf1a51, + 0xf1a52: 0xf1a52, + 0xf1a53: 0xf1a53, + 0xf1a54: 0xf1a54, + 0xf1a55: 0xf1a55, + 0xf1a56: 0xf1a56, + 0xf1a57: 0xf1a57, + 0xf1a58: 0xf1a58, + 0xf1a59: 0xf1a59, + 0xf1a5a: 0xf1a5a, + 0xf1a5b: 0xf1a5b, + 0xf1a5c: 0xf1a5c, + 0xf1a5d: 0xf1a5d, + 0xf1a5e: 0xf1a5e, + 0xf1a5f: 0xf1a5f, + 0xf1a60: 0xf1a60, + 0xf1a61: 0xf1a61, + 0xf1a62: 0xf1a62, + 0xf1a63: 0xf1a63, + 0xf1a64: 0xf1a64, + 0xf1a65: 0xf1a65, + 0xf1a66: 0xf1a66, + 0xf1a67: 0xf1a67, + 0xf1a68: 0xf1a68, + 0xf1a69: 0xf1a69, + 0xf1a6a: 0xf1a6a, + 0xf1a6b: 0xf1a6b, + 0xf1a6c: 0xf1a6c, + 0xf1a6d: 0xf1a6d, + 0xf1a6e: 0xf1a6e, + 0xf1a6f: 0xf1a6f, + 0xf1a70: 0xf1a70, + 0xf1a71: 0xf1a71, + 0xf1a72: 0xf1a72, + 0xf1a73: 0xf1a73, + 0xf1a74: 0xf1a74, + 0xf1a75: 0xf1a75, + 0xf1a76: 0xf1a76, + 0xf1a77: 0xf1a77, + 0xf1a78: 0xf1a78, + 0xf1a79: 0xf1a79, + 0xf1a7a: 0xf1a7a, + 0xf1a7b: 0xf1a7b, + 0xf1a7c: 0xf1a7c, + 0xf1a7d: 0xf1a7d, + 0xf1a7e: 0xf1a7e, + 0xf1a7f: 0xf1a7f, + 0xf1a80: 0xf1a80, + 0xf1a81: 0xf1a81, + 0xf1a82: 0xf1a82, + 0xf1a83: 0xf1a83, + 0xf1a84: 0xf1a84, + 0xf1a85: 0xf1a85, + 0xf1a86: 0xf1a86, + 0xf1a87: 0xf1a87, + 0xf1a88: 0xf1a88, + 0xf1a89: 0xf1a89, + 0xf1a8a: 0xf1a8a, + 0xf1a8b: 0xf1a8b, + 0xf1a8c: 0xf1a8c, + 0xf1a8d: 0xf1a8d, + 0xf1a8e: 0xf1a8e, + 0xf1a8f: 0xf1a8f, + 0xf1a90: 0xf1a90, + 0xf1a91: 0xf1a91, + 0xf1a92: 0xf1a92, + 0xf1a93: 0xf1a93, + 0xf1a94: 0xf1a94, + 0xf1a95: 0xf1a95, + 0xf1a96: 0xf1a96, + 0xf1a97: 0xf1a97, + 0xf1a98: 0xf1a98, + 0xf1a99: 0xf1a99, + 0xf1a9a: 0xf1a9a, + 0xf1a9b: 0xf1a9b, + 0xf1a9c: 0xf1a9c, + 0xf1a9d: 0xf1a9d, + 0xf1a9e: 0xf1a9e, + 0xf1a9f: 0xf1a9f, + 0xf1aa0: 0xf1aa0, + 0xf1aa1: 0xf1aa1, + 0xf1aa2: 0xf1aa2, + 0xf1aa3: 0xf1aa3, + 0xf1aa4: 0xf1aa4, + 0xf1aa5: 0xf1aa5, + 0xf1aa6: 0xf1aa6, + 0xf1aa7: 0xf1aa7, + 0xf1aa8: 0xf1aa8, + 0xf1aa9: 0xf1aa9, + 0xf1aaa: 0xf1aaa, + 0xf1aab: 0xf1aab, + 0xf1aac: 0xf1aac, + 0xf1aad: 0xf1aad, + 0xf1aae: 0xf1aae, + 0xf1aaf: 0xf1aaf, + 0xf1ab0: 0xf1ab0, + 0xf1ab1: 0xf1ab1, + 0xf1ab2: 0xf1ab2, + 0xf1ab3: 0xf1ab3, + 0xf1ab4: 0xf1ab4, + 0xf1ab5: 0xf1ab5, + 0xf1ab6: 0xf1ab6, + 0xf1ab7: 0xf1ab7, + 0xf1ab8: 0xf1ab8, + 0xf1ab9: 0xf1ab9, + 0xf1aba: 0xf1aba, + 0xf1abb: 0xf1abb, + 0xf1abc: 0xf1abc, + 0xf1abd: 0xf1abd, + 0xf1abe: 0xf1abe, + 0xf1abf: 0xf1abf, + 0xf1ac0: 0xf1ac0, + 0xf1ac1: 0xf1ac1, + 0xf1ac2: 0xf1ac2, + 0xf1ac3: 0xf1ac3, + 0xf1ac4: 0xf1ac4, + 0xf1ac5: 0xf1ac5, + 0xf1ac6: 0xf1ac6, + 0xf1ac7: 0xf1ac7, + 0xf1ac8: 0xf1ac8, + 0xf1ac9: 0xf1ac9, + 0xf1aca: 0xf1aca, + 0xf1acb: 0xf1acb, + 0xf1acc: 0xf1acc, + 0xf1acd: 0xf1acd, + 0xf1ace: 0xf1ace, + 0xf1acf: 0xf1acf, + 0xf1ad0: 0xf1ad0, + 0xf1ad1: 0xf1ad1, + 0xf1ad2: 0xf1ad2, + 0xf1ad3: 0xf1ad3, + 0xf1ad4: 0xf1ad4, + 0xf1ad5: 0xf1ad5, + 0xf1ad6: 0xf1ad6, + 0xf1ad7: 0xf1ad7, + 0xf1ad8: 0xf1ad8, + 0xf1ad9: 0xf1ad9, + 0xf1ada: 0xf1ada, + 0xf1adb: 0xf1adb, + 0xf1adc: 0xf1adc, + 0xf1add: 0xf1add, + 0xf1ade: 0xf1ade, + 0xf1adf: 0xf1adf, + 0xf1ae0: 0xf1ae0, + 0xf1ae1: 0xf1ae1, + 0xf1ae2: 0xf1ae2, + 0xf1ae3: 0xf1ae3, + 0xf1ae4: 0xf1ae4, + 0xf1ae5: 0xf1ae5, + 0xf1ae6: 0xf1ae6, + 0xf1ae7: 0xf1ae7, + 0xf1ae8: 0xf1ae8, + 0xf1ae9: 0xf1ae9, + 0xf1aea: 0xf1aea, + 0xf1aeb: 0xf1aeb, + 0xf1aec: 0xf1aec, + 0xf1aed: 0xf1aed, + 0xf1aee: 0xf1aee, + 0xf1aef: 0xf1aef, + 0xf1af0: 0xf1af0, + }, + "Weather Icons": { + 0xf000: 0xe300, + 0xf001: 0xe301, + 0xf002: 0xe302, + 0xf003: 0xe303, + 0xf004: 0xe304, + 0xf005: 0xe305, + 0xf006: 0xe306, + 0xf007: 0xe307, + 0xf008: 0xe308, + 0xf009: 0xe309, + 0xf00a: 0xe30a, + 0xf00b: 0xe30b, + 0xf00c: 0xe30c, + 0xf00d: 0xe30d, + 0xf00e: 0xe30e, + 0xf010: 0xe30f, + 0xf011: 0xe310, + 0xf012: 0xe311, + 0xf013: 0xe312, + 0xf014: 0xe313, + 0xf015: 0xe314, + 0xf016: 0xe315, + 0xf017: 0xe316, + 0xf018: 0xe317, + 0xf019: 0xe318, + 0xf01a: 0xe319, + 0xf01b: 0xe31a, + 0xf01c: 0xe31b, + 0xf01d: 0xe31c, + 0xf01e: 0xe31d, + 0xf021: 0xe31e, + 0xf022: 0xe31f, + 0xf023: 0xe320, + 0xf024: 0xe321, + 0xf025: 0xe322, + 0xf026: 0xe323, + 0xf027: 0xe324, + 0xf028: 0xe325, + 0xf029: 0xe326, + 0xf02a: 0xe327, + 0xf02b: 0xe328, + 0xf02c: 0xe329, + 0xf02d: 0xe32a, + 0xf02e: 0xe32b, + 0xf02f: 0xe32c, + 0xf030: 0xe32d, + 0xf031: 0xe32e, + 0xf032: 0xe32f, + 0xf033: 0xe330, + 0xf034: 0xe331, + 0xf035: 0xe332, + 0xf036: 0xe333, + 0xf037: 0xe334, + 0xf038: 0xe335, + 0xf039: 0xe336, + 0xf03a: 0xe337, + 0xf03b: 0xe338, + 0xf03c: 0xe339, + 0xf03d: 0xe33a, + 0xf03e: 0xe33b, + 0xf040: 0xe33c, + 0xf041: 0xe33d, + 0xf042: 0xe33e, + 0xf043: 0xe33f, + 0xf044: 0xe340, + 0xf045: 0xe341, + 0xf046: 0xe342, + 0xf047: 0xe343, + 0xf048: 0xe344, + 0xf049: 0xe345, + 0xf04a: 0xe346, + 0xf04b: 0xe347, + 0xf04c: 0xe348, + 0xf04d: 0xe349, + 0xf04e: 0xe34a, + 0xf050: 0xe34b, + 0xf051: 0xe34c, + 0xf052: 0xe34d, + 0xf053: 0xe34e, + 0xf054: 0xe34f, + 0xf055: 0xe350, + 0xf056: 0xe351, + 0xf057: 0xe352, + 0xf058: 0xe353, + 0xf059: 0xe354, + 0xf05a: 0xe355, + 0xf05b: 0xe356, + 0xf05c: 0xe357, + 0xf05d: 0xe358, + 0xf05e: 0xe359, + 0xf060: 0xe35a, + 0xf061: 0xe35b, + 0xf062: 0xe35c, + 0xf063: 0xe35d, + 0xf064: 0xe35e, + 0xf065: 0xe35f, + 0xf066: 0xe360, + 0xf067: 0xe361, + 0xf068: 0xe362, + 0xf069: 0xe363, + 0xf06a: 0xe364, + 0xf06b: 0xe365, + 0xf06c: 0xe366, + 0xf06d: 0xe367, + 0xf06e: 0xe368, + 0xf070: 0xe369, + 0xf071: 0xe36a, + 0xf072: 0xe36b, + 0xf073: 0xe36c, + 0xf074: 0xe36d, + 0xf075: 0xe36e, + 0xf076: 0xe36f, + 0xf077: 0xe370, + 0xf078: 0xe371, + 0xf079: 0xe372, + 0xf07a: 0xe373, + 0xf07b: 0xe374, + 0xf07c: 0xe375, + 0xf07d: 0xe376, + 0xf07e: 0xe377, + 0xf080: 0xe378, + 0xf081: 0xe379, + 0xf082: 0xe37a, + 0xf083: 0xe37b, + 0xf084: 0xe37c, + 0xf085: 0xe37d, + 0xf086: 0xe37e, + 0xf087: 0xe37f, + 0xf088: 0xe380, + 0xf089: 0xe381, + 0xf08a: 0xe382, + 0xf08b: 0xe383, + 0xf08c: 0xe384, + 0xf08d: 0xe385, + 0xf08e: 0xe386, + 0xf08f: 0xe387, + 0xf090: 0xe388, + 0xf091: 0xe389, + 0xf092: 0xe38a, + 0xf093: 0xe38b, + 0xf094: 0xe38c, + 0xf095: 0xe38d, + 0xf096: 0xe38e, + 0xf097: 0xe38f, + 0xf098: 0xe390, + 0xf099: 0xe391, + 0xf09a: 0xe392, + 0xf09b: 0xe393, + 0xf09c: 0xe394, + 0xf09d: 0xe395, + 0xf09e: 0xe396, + 0xf09f: 0xe397, + 0xf0a0: 0xe398, + 0xf0a1: 0xe399, + 0xf0a2: 0xe39a, + 0xf0a3: 0xe39b, + 0xf0a4: 0xe39c, + 0xf0a5: 0xe39d, + 0xf0a6: 0xe39e, + 0xf0a7: 0xe39f, + 0xf0a8: 0xe3a0, + 0xf0a9: 0xe3a1, + 0xf0aa: 0xe3a2, + 0xf0ab: 0xe3a3, + 0xf0ac: 0xe3a4, + 0xf0ad: 0xe3a5, + 0xf0ae: 0xe3a6, + 0xf0af: 0xe3a7, + 0xf0b0: 0xe3a8, + 0xf0b1: 0xe3a9, + 0xf0b2: 0xe3aa, + 0xf0b3: 0xe3ab, + 0xf0b4: 0xe3ac, + 0xf0b5: 0xe3ad, + 0xf0b6: 0xe3ae, + 0xf0b7: 0xe3af, + 0xf0b8: 0xe3b0, + 0xf0b9: 0xe3b1, + 0xf0ba: 0xe3b2, + 0xf0bb: 0xe3b3, + 0xf0bc: 0xe3b4, + 0xf0bd: 0xe3b5, + 0xf0be: 0xe3b6, + 0xf0bf: 0xe3b7, + 0xf0c0: 0xe3b8, + 0xf0c1: 0xe3b9, + 0xf0c2: 0xe3ba, + 0xf0c3: 0xe3bb, + 0xf0c4: 0xe3bc, + 0xf0c5: 0xe3bd, + 0xf0c6: 0xe3be, + 0xf0c7: 0xe3bf, + 0xf0c8: 0xe3c0, + 0xf0c9: 0xe3c1, + 0xf0ca: 0xe3c2, + 0xf0cb: 0xe3c3, + 0xf0cc: 0xe3c4, + 0xf0cd: 0xe3c5, + 0xf0ce: 0xe3c6, + 0xf0cf: 0xe3c7, + 0xf0d0: 0xe3c8, + 0xf0d1: 0xe3c9, + 0xf0d2: 0xe3ca, + 0xf0d3: 0xe3cb, + 0xf0d4: 0xe3cc, + 0xf0d5: 0xe3cd, + 0xf0d6: 0xe3ce, + 0xf0d7: 0xe3cf, + 0xf0d8: 0xe3d0, + 0xf0d9: 0xe3d1, + 0xf0da: 0xe3d2, + 0xf0db: 0xe3d3, + 0xf0dc: 0xe3d4, + 0xf0dd: 0xe3d5, + 0xf0de: 0xe3d6, + 0xf0df: 0xe3d7, + 0xf0e0: 0xe3d8, + 0xf0e1: 0xe3d9, + 0xf0e2: 0xe3da, + 0xf0e3: 0xe3db, + 0xf0e4: 0xe3dc, + 0xf0e5: 0xe3dd, + 0xf0e6: 0xe3de, + 0xf0e7: 0xe3df, + 0xf0e8: 0xe3e0, + 0xf0e9: 0xe3e1, + 0xf0ea: 0xe3e2, + 0xf0eb: 0xe3e3, + }, + "Font Logos": { + 0xf300: 0xf300, + 0xf301: 0xf301, + 0xf302: 0xf302, + 0xf303: 0xf303, + 0xf304: 0xf304, + 0xf305: 0xf305, + 0xf306: 0xf306, + 0xf307: 0xf307, + 0xf308: 0xf308, + 0xf309: 0xf309, + 0xf30a: 0xf30a, + 0xf30b: 0xf30b, + 0xf30c: 0xf30c, + 0xf30d: 0xf30d, + 0xf30e: 0xf30e, + 0xf30f: 0xf30f, + 0xf310: 0xf310, + 0xf311: 0xf311, + 0xf312: 0xf312, + 0xf313: 0xf313, + 0xf314: 0xf314, + 0xf315: 0xf315, + 0xf316: 0xf316, + 0xf317: 0xf317, + 0xf318: 0xf318, + 0xf319: 0xf319, + 0xf31a: 0xf31a, + 0xf31b: 0xf31b, + 0xf31c: 0xf31c, + 0xf31d: 0xf31d, + 0xf31e: 0xf31e, + 0xf31f: 0xf31f, + 0xf320: 0xf320, + 0xf321: 0xf321, + 0xf322: 0xf322, + 0xf323: 0xf323, + 0xf324: 0xf324, + 0xf325: 0xf325, + 0xf326: 0xf326, + 0xf327: 0xf327, + 0xf328: 0xf328, + 0xf329: 0xf329, + 0xf32a: 0xf32a, + 0xf32b: 0xf32b, + 0xf32c: 0xf32c, + 0xf32d: 0xf32d, + 0xf32e: 0xf32e, + 0xf32f: 0xf32f, + 0xf330: 0xf330, + 0xf331: 0xf331, + 0xf332: 0xf332, + 0xf333: 0xf333, + 0xf334: 0xf334, + 0xf335: 0xf335, + 0xf336: 0xf336, + 0xf337: 0xf337, + 0xf338: 0xf338, + 0xf339: 0xf339, + 0xf33a: 0xf33a, + 0xf33b: 0xf33b, + 0xf33c: 0xf33c, + 0xf33d: 0xf33d, + 0xf33e: 0xf33e, + 0xf33f: 0xf33f, + 0xf340: 0xf340, + 0xf341: 0xf341, + 0xf342: 0xf342, + 0xf343: 0xf343, + 0xf344: 0xf344, + 0xf345: 0xf345, + 0xf346: 0xf346, + 0xf347: 0xf347, + 0xf348: 0xf348, + 0xf349: 0xf349, + 0xf34a: 0xf34a, + 0xf34b: 0xf34b, + 0xf34c: 0xf34c, + 0xf34d: 0xf34d, + 0xf34e: 0xf34e, + 0xf34f: 0xf34f, + 0xf350: 0xf350, + 0xf351: 0xf351, + 0xf352: 0xf352, + 0xf353: 0xf353, + 0xf354: 0xf354, + 0xf355: 0xf355, + 0xf356: 0xf356, + 0xf357: 0xf357, + 0xf358: 0xf358, + 0xf359: 0xf359, + 0xf35a: 0xf35a, + 0xf35b: 0xf35b, + 0xf35c: 0xf35c, + 0xf35d: 0xf35d, + 0xf35e: 0xf35e, + 0xf35f: 0xf35f, + 0xf360: 0xf360, + 0xf361: 0xf361, + 0xf362: 0xf362, + 0xf363: 0xf363, + 0xf364: 0xf364, + 0xf365: 0xf365, + 0xf366: 0xf366, + 0xf367: 0xf367, + 0xf368: 0xf368, + 0xf369: 0xf369, + 0xf36a: 0xf36a, + 0xf36b: 0xf36b, + 0xf36c: 0xf36c, + 0xf36d: 0xf36d, + 0xf36e: 0xf36e, + 0xf36f: 0xf36f, + 0xf370: 0xf370, + 0xf371: 0xf371, + 0xf372: 0xf372, + 0xf373: 0xf373, + 0xf374: 0xf374, + 0xf375: 0xf375, + 0xf376: 0xf376, + 0xf377: 0xf377, + 0xf378: 0xf378, + 0xf379: 0xf379, + 0xf37a: 0xf37a, + 0xf37b: 0xf37b, + 0xf37c: 0xf37c, + 0xf37d: 0xf37d, + 0xf37e: 0xf37e, + 0xf37f: 0xf37f, + 0xf380: 0xf380, + 0xf381: 0xf381, + }, + "Octicons": { + 0xf000: 0xf400, + 0xf001: 0xf401, + 0xf002: 0xf402, + 0xf005: 0xf403, + 0xf006: 0xf404, + 0xf007: 0xf405, + 0xf008: 0xf406, + 0xf009: 0xf407, + 0xf00a: 0xf408, + 0xf00b: 0xf409, + 0xf00c: 0xf40a, + 0xf00d: 0xf40b, + 0xf00e: 0xf40c, + 0xf010: 0xf40d, + 0xf011: 0xf40e, + 0xf012: 0xf40f, + 0xf013: 0xf410, + 0xf014: 0xf411, + 0xf015: 0xf412, + 0xf016: 0xf413, + 0xf017: 0xf414, + 0xf018: 0xf415, + 0xf019: 0xf416, + 0xf01f: 0xf417, + 0xf020: 0xf418, + 0xf023: 0xf419, + 0xf024: 0xf41a, + 0xf026: 0xf41b, + 0xf027: 0xf41c, + 0xf028: 0xf41d, + 0xf02a: 0xf41e, + 0xf02b: 0xf41f, + 0xf02c: 0xf420, + 0xf02d: 0xf421, + 0xf02e: 0xf422, + 0xf02f: 0xf423, + 0xf030: 0xf424, + 0xf031: 0xf425, + 0xf032: 0xf426, + 0xf033: 0xf427, + 0xf034: 0xf428, + 0xf035: 0xf429, + 0xf036: 0xf42a, + 0xf037: 0xf42b, + 0xf038: 0xf42c, + 0xf039: 0xf42d, + 0xf03a: 0xf42e, + 0xf03b: 0xf42f, + 0xf03c: 0xf430, + 0xf03d: 0xf431, + 0xf03e: 0xf432, + 0xf03f: 0xf433, + 0xf040: 0xf434, + 0xf041: 0xf435, + 0xf042: 0xf436, + 0xf043: 0xf437, + 0xf044: 0xf438, + 0xf045: 0xf439, + 0xf046: 0xf43a, + 0xf047: 0xf43b, + 0xf048: 0xf43c, + 0xf049: 0xf43d, + 0xf04a: 0xf43e, + 0xf04c: 0xf43f, + 0xf04d: 0xf440, + 0xf04e: 0xf441, + 0xf04f: 0xf442, + 0xf051: 0xf443, + 0xf052: 0xf444, + 0xf053: 0xf445, + 0xf056: 0xf446, + 0xf057: 0xf447, + 0xf058: 0xf448, + 0xf059: 0xf449, + 0xf05a: 0xf44a, + 0xf05b: 0xf44b, + 0xf05c: 0xf44c, + 0xf05d: 0xf44d, + 0xf05e: 0xf44e, + 0xf05f: 0xf44f, + 0xf060: 0xf450, + 0xf061: 0xf451, + 0xf062: 0xf452, + 0xf063: 0xf453, + 0xf064: 0xf454, + 0xf068: 0xf455, + 0xf06a: 0xf456, + 0xf06b: 0xf457, + 0xf06c: 0xf458, + 0xf06d: 0xf459, + 0xf06e: 0xf45a, + 0xf070: 0xf45b, + 0xf071: 0xf45c, + 0xf075: 0xf45d, + 0xf076: 0xf45e, + 0xf077: 0xf45f, + 0xf078: 0xf460, + 0xf07b: 0xf461, + 0xf07c: 0xf462, + 0xf07d: 0xf463, + 0xf07e: 0xf464, + 0xf07f: 0xf465, + 0xf080: 0xf466, + 0xf081: 0xf467, + 0xf084: 0xf468, + 0xf085: 0xf469, + 0xf087: 0xf46a, + 0xf088: 0xf46b, + 0xf08c: 0xf46c, + 0xf08d: 0xf46d, + 0xf08f: 0xf46e, + 0xf091: 0xf46f, + 0xf092: 0xf470, + 0xf094: 0xf471, + 0xf096: 0xf472, + 0xf097: 0xf473, + 0xf099: 0xf474, + 0xf09a: 0xf475, + 0xf09c: 0xf476, + 0xf09d: 0xf477, + 0xf09f: 0xf478, + 0xf0a0: 0xf479, + 0xf0a1: 0xf47a, + 0xf0a2: 0xf47b, + 0xf0a3: 0xf47c, + 0xf0a4: 0xf47d, + 0xf0aa: 0xf47e, + 0xf0ac: 0xf47f, + 0xf0ad: 0xf480, + 0xf0b0: 0xf481, + 0xf0b1: 0xf482, + 0xf0b2: 0xf483, + 0xf0b6: 0xf484, + 0xf0ba: 0xf485, + 0xf0be: 0xf486, + 0xf0c4: 0xf487, + 0xf0c5: 0xf488, + 0xf0c8: 0xf489, + 0xf0c9: 0xf48a, + 0xf0ca: 0xf48b, + 0xf0cc: 0xf48c, + 0xf0cf: 0xf48d, + 0xf0d0: 0xf48e, + 0xf0d1: 0xf48f, + 0xf0d2: 0xf490, + 0xf0d3: 0xf491, + 0xf0d4: 0xf492, + 0xf0d6: 0xf493, + 0xf0d7: 0xf494, + 0xf0d8: 0xf495, + 0xf0da: 0xf496, + 0xf0db: 0xf497, + 0xf0dc: 0xf498, + 0xf0dd: 0xf499, + 0xf0de: 0xf49a, + 0xf0e0: 0xf49b, + 0xf0e1: 0xf49c, + 0xf0e2: 0xf49d, + 0xf0e3: 0xf49e, + 0xf0e4: 0xf49f, + 0xf0e5: 0xf4a0, + 0xf0e6: 0xf4a1, + 0xf0e7: 0xf4a2, + 0xf0e8: 0xf4a3, + 0xf101: 0xf4a4, + 0xf102: 0xf4a5, + 0xf103: 0xf4a6, + 0xf104: 0xf4a7, + 0xf105: 0xf4a8, + 0x2665: 0x2665, + 0x26a1: 0x26a1, + 0xf27c: 0xf4a9, + 0xf27d: 0xf4aa, + 0xf27e: 0xf4ab, + 0xf27f: 0xf4ac, + 0xf280: 0xf4ad, + 0xf281: 0xf4ae, + 0xf282: 0xf4af, + 0xf283: 0xf4b0, + 0xf284: 0xf4b1, + 0xf285: 0xf4b2, + 0xf286: 0xf4b3, + 0xf287: 0xf4b4, + 0xf288: 0xf4b5, + 0xf289: 0xf4b6, + 0xf28a: 0xf4b7, + 0xf28b: 0xf4b8, + 0xf28c: 0xf4b9, + 0xf28d: 0xf4ba, + 0xf28e: 0xf4bb, + 0xf28f: 0xf4bc, + 0xf290: 0xf4bd, + 0xf291: 0xf4be, + 0xf292: 0xf4bf, + 0xf293: 0xf4c0, + 0xf294: 0xf4c1, + 0xf295: 0xf4c2, + 0xf296: 0xf4c3, + 0xf297: 0xf4c4, + 0xf298: 0xf4c5, + 0xf299: 0xf4c6, + 0xf29a: 0xf4c7, + 0xf29b: 0xf4c8, + 0xf29c: 0xf4c9, + 0xf29d: 0xf4ca, + 0xf29e: 0xf4cb, + 0xf29f: 0xf4cc, + 0xf2a0: 0xf4cd, + 0xf2a1: 0xf4ce, + 0xf2a2: 0xf4cf, + 0xf2a3: 0xf4d0, + 0xf2a4: 0xf4d1, + 0xf2a5: 0xf4d2, + 0xf2a6: 0xf4d3, + 0xf2a7: 0xf4d4, + 0xf2a8: 0xf4d5, + 0xf2a9: 0xf4d6, + 0xf2aa: 0xf4d7, + 0xf2ab: 0xf4d8, + 0xf2ac: 0xf4d9, + 0xf2ad: 0xf4da, + 0xf2ae: 0xf4db, + 0xf2af: 0xf4dc, + 0xf2b0: 0xf4dd, + 0xf2b1: 0xf4de, + 0xf2b2: 0xf4df, + 0xf2b3: 0xf4e0, + 0xf2b4: 0xf4e1, + 0xf2b5: 0xf4e2, + 0xf2b6: 0xf4e3, + 0xf2b7: 0xf4e4, + 0xf2b8: 0xf4e5, + 0xf2b9: 0xf4e6, + 0xf2ba: 0xf4e7, + 0xf2bb: 0xf4e8, + 0xf2bc: 0xf4e9, + 0xf2bd: 0xf4ea, + 0xf2be: 0xf4eb, + 0xf2bf: 0xf4ec, + 0xf2c0: 0xf4ed, + 0xf2c1: 0xf4ee, + 0xf2c2: 0xf4ef, + 0xf2c3: 0xf4f0, + 0xf2c4: 0xf4f1, + 0xf2c5: 0xf4f2, + 0xf2c6: 0xf4f3, + 0xf2c7: 0xf4f4, + 0xf2c8: 0xf4f5, + 0xf2c9: 0xf4f6, + 0xf2ca: 0xf4f7, + 0xf2cb: 0xf4f8, + 0xf2cc: 0xf4f9, + 0xf2cd: 0xf4fa, + 0xf2ce: 0xf4fb, + 0xf2cf: 0xf4fc, + 0xf2d0: 0xf4fd, + 0xf2d1: 0xf4fe, + 0xf2d2: 0xf4ff, + 0xf2d3: 0xf500, + 0xf2d4: 0xf501, + 0xf2d5: 0xf502, + 0xf2d6: 0xf503, + 0xf2d7: 0xf504, + 0xf2d8: 0xf505, + 0xf2d9: 0xf506, + 0xf2da: 0xf507, + 0xf2db: 0xf508, + 0xf2dc: 0xf509, + 0xf2dd: 0xf50a, + 0xf2de: 0xf50b, + 0xf2df: 0xf50c, + 0xf2e0: 0xf50d, + 0xf2e1: 0xf50e, + 0xf2e2: 0xf50f, + 0xf2e3: 0xf510, + 0xf2e4: 0xf511, + 0xf2e5: 0xf512, + 0xf2e6: 0xf513, + 0xf2e7: 0xf514, + 0xf2e8: 0xf515, + 0xf2e9: 0xf516, + 0xf2ea: 0xf517, + 0xf2eb: 0xf518, + 0xf2ec: 0xf519, + 0xf2ed: 0xf51a, + 0xf2ee: 0xf51b, + 0xf2ef: 0xf51c, + 0xf2f0: 0xf51d, + 0xf2f1: 0xf51e, + 0xf2f2: 0xf51f, + 0xf2f3: 0xf520, + 0xf2f4: 0xf521, + 0xf2f5: 0xf522, + 0xf2f6: 0xf523, + 0xf2f7: 0xf524, + 0xf2f8: 0xf525, + 0xf2f9: 0xf526, + 0xf2fa: 0xf527, + 0xf2fb: 0xf528, + 0xf2fc: 0xf529, + 0xf2fd: 0xf52a, + 0xf2fe: 0xf52b, + 0xf2ff: 0xf52c, + 0xf300: 0xf52d, + 0xf301: 0xf52e, + 0xf302: 0xf52f, + 0xf303: 0xf530, + 0xf304: 0xf531, + 0xf305: 0xf532, + 0xf306: 0xf533, + }, + "Codicons": { + 0xea60: 0xea60, + 0xea61: 0xea61, + 0xea62: 0xea62, + 0xea63: 0xea63, + 0xea64: 0xea64, + 0xea65: 0xea65, + 0xea66: 0xea66, + 0xea67: 0xea67, + 0xea68: 0xea68, + 0xea69: 0xea69, + 0xea6a: 0xea6a, + 0xea6b: 0xea6b, + 0xea6c: 0xea6c, + 0xea6d: 0xea6d, + 0xea6e: 0xea6e, + 0xea6f: 0xea6f, + 0xea70: 0xea70, + 0xea71: 0xea71, + 0xea72: 0xea72, + 0xea73: 0xea73, + 0xea74: 0xea74, + 0xea75: 0xea75, + 0xea76: 0xea76, + 0xea77: 0xea77, + 0xea78: 0xea78, + 0xea79: 0xea79, + 0xea7a: 0xea7a, + 0xea7b: 0xea7b, + 0xea7c: 0xea7c, + 0xea7d: 0xea7d, + 0xea7e: 0xea7e, + 0xea7f: 0xea7f, + 0xea80: 0xea80, + 0xea81: 0xea81, + 0xea82: 0xea82, + 0xea83: 0xea83, + 0xea84: 0xea84, + 0xea85: 0xea85, + 0xea86: 0xea86, + 0xea87: 0xea87, + 0xea88: 0xea88, + 0xea8a: 0xea8a, + 0xea8b: 0xea8b, + 0xea8c: 0xea8c, + 0xea8f: 0xea8f, + 0xea90: 0xea90, + 0xea91: 0xea91, + 0xea92: 0xea92, + 0xea93: 0xea93, + 0xea94: 0xea94, + 0xea95: 0xea95, + 0xea96: 0xea96, + 0xea97: 0xea97, + 0xea98: 0xea98, + 0xea99: 0xea99, + 0xea9a: 0xea9a, + 0xea9b: 0xea9b, + 0xea9c: 0xea9c, + 0xea9d: 0xea9d, + 0xea9e: 0xea9e, + 0xea9f: 0xea9f, + 0xeaa0: 0xeaa0, + 0xeaa1: 0xeaa1, + 0xeaa2: 0xeaa2, + 0xeaa3: 0xeaa3, + 0xeaa4: 0xeaa4, + 0xeaa5: 0xeaa5, + 0xeaa6: 0xeaa6, + 0xeaa7: 0xeaa7, + 0xeaa8: 0xeaa8, + 0xeaa9: 0xeaa9, + 0xeaaa: 0xeaaa, + 0xeaab: 0xeaab, + 0xeaac: 0xeaac, + 0xeaad: 0xeaad, + 0xeaae: 0xeaae, + 0xeaaf: 0xeaaf, + 0xeab0: 0xeab0, + 0xeab1: 0xeab1, + 0xeab2: 0xeab2, + 0xeab3: 0xeab3, + 0xeab4: 0xeab4, + 0xeab5: 0xeab5, + 0xeab6: 0xeab6, + 0xeab7: 0xeab7, + 0xeab8: 0xeab8, + 0xeab9: 0xeab9, + 0xeaba: 0xeaba, + 0xeabb: 0xeabb, + 0xeabc: 0xeabc, + 0xeabd: 0xeabd, + 0xeabe: 0xeabe, + 0xeabf: 0xeabf, + 0xeac0: 0xeac0, + 0xeac1: 0xeac1, + 0xeac2: 0xeac2, + 0xeac3: 0xeac3, + 0xeac4: 0xeac4, + 0xeac5: 0xeac5, + 0xeac6: 0xeac6, + 0xeac7: 0xeac7, + 0xeac9: 0xeac9, + 0xeacc: 0xeacc, + 0xeacd: 0xeacd, + 0xeace: 0xeace, + 0xeacf: 0xeacf, + 0xead0: 0xead0, + 0xead1: 0xead1, + 0xead2: 0xead2, + 0xead3: 0xead3, + 0xead4: 0xead4, + 0xead5: 0xead5, + 0xead6: 0xead6, + 0xead7: 0xead7, + 0xead8: 0xead8, + 0xead9: 0xead9, + 0xeada: 0xeada, + 0xeadb: 0xeadb, + 0xeadc: 0xeadc, + 0xeadd: 0xeadd, + 0xeade: 0xeade, + 0xeadf: 0xeadf, + 0xeae0: 0xeae0, + 0xeae1: 0xeae1, + 0xeae2: 0xeae2, + 0xeae3: 0xeae3, + 0xeae4: 0xeae4, + 0xeae5: 0xeae5, + 0xeae6: 0xeae6, + 0xeae7: 0xeae7, + 0xeae8: 0xeae8, + 0xeae9: 0xeae9, + 0xeaea: 0xeaea, + 0xeaeb: 0xeaeb, + 0xeaec: 0xeaec, + 0xeaed: 0xeaed, + 0xeaee: 0xeaee, + 0xeaef: 0xeaef, + 0xeaf0: 0xeaf0, + 0xeaf1: 0xeaf1, + 0xeaf2: 0xeaf2, + 0xeaf3: 0xeaf3, + 0xeaf4: 0xeaf4, + 0xeaf5: 0xeaf5, + 0xeaf6: 0xeaf6, + 0xeaf7: 0xeaf7, + 0xeaf8: 0xeaf8, + 0xeaf9: 0xeaf9, + 0xeafa: 0xeafa, + 0xeafb: 0xeafb, + 0xeafc: 0xeafc, + 0xeafd: 0xeafd, + 0xeafe: 0xeafe, + 0xeaff: 0xeaff, + 0xeb00: 0xeb00, + 0xeb01: 0xeb01, + 0xeb02: 0xeb02, + 0xeb03: 0xeb03, + 0xeb04: 0xeb04, + 0xeb05: 0xeb05, + 0xeb06: 0xeb06, + 0xeb07: 0xeb07, + 0xeb08: 0xeb08, + 0xeb09: 0xeb09, + 0xeb0b: 0xeb0b, + 0xeb0c: 0xeb0c, + 0xeb0d: 0xeb0d, + 0xeb0e: 0xeb0e, + 0xeb0f: 0xeb0f, + 0xeb10: 0xeb10, + 0xeb11: 0xeb11, + 0xeb12: 0xeb12, + 0xeb13: 0xeb13, + 0xeb14: 0xeb14, + 0xeb15: 0xeb15, + 0xeb16: 0xeb16, + 0xeb17: 0xeb17, + 0xeb18: 0xeb18, + 0xeb19: 0xeb19, + 0xeb1a: 0xeb1a, + 0xeb1b: 0xeb1b, + 0xeb1c: 0xeb1c, + 0xeb1d: 0xeb1d, + 0xeb1e: 0xeb1e, + 0xeb1f: 0xeb1f, + 0xeb20: 0xeb20, + 0xeb21: 0xeb21, + 0xeb22: 0xeb22, + 0xeb23: 0xeb23, + 0xeb24: 0xeb24, + 0xeb25: 0xeb25, + 0xeb26: 0xeb26, + 0xeb27: 0xeb27, + 0xeb28: 0xeb28, + 0xeb29: 0xeb29, + 0xeb2a: 0xeb2a, + 0xeb2b: 0xeb2b, + 0xeb2c: 0xeb2c, + 0xeb2d: 0xeb2d, + 0xeb2e: 0xeb2e, + 0xeb2f: 0xeb2f, + 0xeb30: 0xeb30, + 0xeb31: 0xeb31, + 0xeb32: 0xeb32, + 0xeb33: 0xeb33, + 0xeb34: 0xeb34, + 0xeb35: 0xeb35, + 0xeb36: 0xeb36, + 0xeb37: 0xeb37, + 0xeb38: 0xeb38, + 0xeb39: 0xeb39, + 0xeb3a: 0xeb3a, + 0xeb3b: 0xeb3b, + 0xeb3c: 0xeb3c, + 0xeb3d: 0xeb3d, + 0xeb3e: 0xeb3e, + 0xeb3f: 0xeb3f, + 0xeb40: 0xeb40, + 0xeb41: 0xeb41, + 0xeb42: 0xeb42, + 0xeb43: 0xeb43, + 0xeb44: 0xeb44, + 0xeb45: 0xeb45, + 0xeb46: 0xeb46, + 0xeb47: 0xeb47, + 0xeb48: 0xeb48, + 0xeb49: 0xeb49, + 0xeb4a: 0xeb4a, + 0xeb4b: 0xeb4b, + 0xeb4c: 0xeb4c, + 0xeb4d: 0xeb4d, + 0xeb4e: 0xeb4e, + 0xeb50: 0xeb50, + 0xeb51: 0xeb51, + 0xeb52: 0xeb52, + 0xeb53: 0xeb53, + 0xeb54: 0xeb54, + 0xeb55: 0xeb55, + 0xeb56: 0xeb56, + 0xeb57: 0xeb57, + 0xeb58: 0xeb58, + 0xeb59: 0xeb59, + 0xeb5a: 0xeb5a, + 0xeb5b: 0xeb5b, + 0xeb5c: 0xeb5c, + 0xeb5d: 0xeb5d, + 0xeb5e: 0xeb5e, + 0xeb5f: 0xeb5f, + 0xeb60: 0xeb60, + 0xeb61: 0xeb61, + 0xeb62: 0xeb62, + 0xeb63: 0xeb63, + 0xeb64: 0xeb64, + 0xeb65: 0xeb65, + 0xeb66: 0xeb66, + 0xeb67: 0xeb67, + 0xeb68: 0xeb68, + 0xeb69: 0xeb69, + 0xeb6a: 0xeb6a, + 0xeb6b: 0xeb6b, + 0xeb6c: 0xeb6c, + 0xeb6d: 0xeb6d, + 0xeb6e: 0xeb6e, + 0xeb6f: 0xeb6f, + 0xeb70: 0xeb70, + 0xeb71: 0xeb71, + 0xeb72: 0xeb72, + 0xeb73: 0xeb73, + 0xeb74: 0xeb74, + 0xeb75: 0xeb75, + 0xeb76: 0xeb76, + 0xeb77: 0xeb77, + 0xeb78: 0xeb78, + 0xeb79: 0xeb79, + 0xeb7a: 0xeb7a, + 0xeb7b: 0xeb7b, + 0xeb7c: 0xeb7c, + 0xeb7d: 0xeb7d, + 0xeb7e: 0xeb7e, + 0xeb7f: 0xeb7f, + 0xeb80: 0xeb80, + 0xeb81: 0xeb81, + 0xeb82: 0xeb82, + 0xeb83: 0xeb83, + 0xeb84: 0xeb84, + 0xeb85: 0xeb85, + 0xeb86: 0xeb86, + 0xeb87: 0xeb87, + 0xeb88: 0xeb88, + 0xeb89: 0xeb89, + 0xeb8a: 0xeb8a, + 0xeb8b: 0xeb8b, + 0xeb8c: 0xeb8c, + 0xeb8d: 0xeb8d, + 0xeb8e: 0xeb8e, + 0xeb8f: 0xeb8f, + 0xeb90: 0xeb90, + 0xeb91: 0xeb91, + 0xeb92: 0xeb92, + 0xeb93: 0xeb93, + 0xeb94: 0xeb94, + 0xeb95: 0xeb95, + 0xeb96: 0xeb96, + 0xeb97: 0xeb97, + 0xeb98: 0xeb98, + 0xeb99: 0xeb99, + 0xeb9a: 0xeb9a, + 0xeb9b: 0xeb9b, + 0xeb9c: 0xeb9c, + 0xeb9d: 0xeb9d, + 0xeb9e: 0xeb9e, + 0xeb9f: 0xeb9f, + 0xeba0: 0xeba0, + 0xeba1: 0xeba1, + 0xeba2: 0xeba2, + 0xeba3: 0xeba3, + 0xeba4: 0xeba4, + 0xeba5: 0xeba5, + 0xeba6: 0xeba6, + 0xeba7: 0xeba7, + 0xeba8: 0xeba8, + 0xeba9: 0xeba9, + 0xebaa: 0xebaa, + 0xebab: 0xebab, + 0xebac: 0xebac, + 0xebad: 0xebad, + 0xebae: 0xebae, + 0xebaf: 0xebaf, + 0xebb0: 0xebb0, + 0xebb1: 0xebb1, + 0xebb2: 0xebb2, + 0xebb3: 0xebb3, + 0xebb4: 0xebb4, + 0xebb5: 0xebb5, + 0xebb6: 0xebb6, + 0xebb7: 0xebb7, + 0xebb8: 0xebb8, + 0xebb9: 0xebb9, + 0xebba: 0xebba, + 0xebbb: 0xebbb, + 0xebbc: 0xebbc, + 0xebbd: 0xebbd, + 0xebbe: 0xebbe, + 0xebbf: 0xebbf, + 0xebc0: 0xebc0, + 0xebc1: 0xebc1, + 0xebc2: 0xebc2, + 0xebc3: 0xebc3, + 0xebc4: 0xebc4, + 0xebc5: 0xebc5, + 0xebc6: 0xebc6, + 0xebc7: 0xebc7, + 0xebc8: 0xebc8, + 0xebc9: 0xebc9, + 0xebca: 0xebca, + 0xebcb: 0xebcb, + 0xebcc: 0xebcc, + 0xebcd: 0xebcd, + 0xebce: 0xebce, + 0xebcf: 0xebcf, + 0xebd0: 0xebd0, + 0xebd1: 0xebd1, + 0xebd2: 0xebd2, + 0xebd3: 0xebd3, + 0xebd4: 0xebd4, + 0xebd5: 0xebd5, + 0xebd6: 0xebd6, + 0xebd7: 0xebd7, + 0xebd8: 0xebd8, + 0xebd9: 0xebd9, + 0xebda: 0xebda, + 0xebdb: 0xebdb, + 0xebdc: 0xebdc, + 0xebdd: 0xebdd, + 0xebde: 0xebde, + 0xebdf: 0xebdf, + 0xebe0: 0xebe0, + 0xebe1: 0xebe1, + 0xebe2: 0xebe2, + 0xebe3: 0xebe3, + 0xebe4: 0xebe4, + 0xebe5: 0xebe5, + 0xebe6: 0xebe6, + 0xebe7: 0xebe7, + 0xebe8: 0xebe8, + 0xebe9: 0xebe9, + 0xebea: 0xebea, + 0xebeb: 0xebeb, + 0xebec: 0xebec, + 0xebed: 0xebed, + 0xebee: 0xebee, + 0xebef: 0xebef, + 0xebf0: 0xebf0, + 0xebf1: 0xebf1, + 0xebf2: 0xebf2, + 0xebf3: 0xebf3, + 0xebf4: 0xebf4, + 0xebf5: 0xebf5, + 0xebf6: 0xebf6, + 0xebf7: 0xebf7, + 0xebf8: 0xebf8, + 0xebf9: 0xebf9, + 0xebfa: 0xebfa, + 0xebfb: 0xebfb, + 0xebfc: 0xebfc, + 0xebfd: 0xebfd, + 0xebfe: 0xebfe, + 0xebff: 0xebff, + 0xec00: 0xec00, + 0xec01: 0xec01, + 0xec02: 0xec02, + 0xec03: 0xec03, + 0xec04: 0xec04, + 0xec05: 0xec05, + 0xec06: 0xec06, + 0xec07: 0xec07, + 0xec08: 0xec08, + 0xec09: 0xec09, + 0xec0a: 0xec0a, + 0xec0b: 0xec0b, + 0xec0c: 0xec0c, + 0xec0d: 0xec0d, + 0xec0e: 0xec0e, + 0xec0f: 0xec0f, + 0xec10: 0xec10, + 0xec11: 0xec11, + 0xec12: 0xec12, + 0xec13: 0xec13, + 0xec14: 0xec14, + 0xec15: 0xec15, + 0xec16: 0xec16, + 0xec17: 0xec17, + 0xec18: 0xec18, + 0xec19: 0xec19, + 0xec1a: 0xec1a, + 0xec1b: 0xec1b, + 0xec1c: 0xec1c, + 0xec1d: 0xec1d, + 0xec1e: 0xec1e, + }, +} diff --git a/src/font/opentype/sfnt.zig b/src/font/opentype/sfnt.zig index 82c118bce..d97d9e2d5 100644 --- a/src/font/opentype/sfnt.zig +++ b/src/font/opentype/sfnt.zig @@ -106,7 +106,7 @@ fn FixedPoint(comptime T: type, int_bits: u64, frac_bits: u64) type { self: Self, comptime fmt: []const u8, options: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { _ = fmt; _ = options; @@ -176,7 +176,7 @@ pub const SFNT = struct { self: OffsetSubtable, comptime fmt: []const u8, options: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { _ = fmt; _ = options; @@ -210,7 +210,7 @@ pub const SFNT = struct { self: TableRecord, comptime fmt: []const u8, options: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { _ = fmt; _ = options; diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 285a5a6b9..f1368679d 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -52,10 +52,10 @@ pub const Shaper = struct { /// The shared memory used for shaping results. cell_buf: CellBuf, - /// The cached writing direction value for shaping. This isn't - /// configurable we just use this as a cache to avoid creating - /// and releasing many objects when shaping. - writing_direction: *macos.foundation.Array, + /// 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. + typesetter_attr_dict: *macos.foundation.Dictionary, /// List where we cache fonts, so we don't have to remake them for /// every single shaping operation. @@ -174,21 +174,28 @@ pub const Shaper = struct { // // See: https://github.com/mitchellh/ghostty/issues/1737 // See: https://github.com/mitchellh/ghostty/issues/1442 - const writing_direction = array: { - const dir: macos.text.WritingDirection = .lro; - const num = try macos.foundation.Number.create( - .int, - &@intFromEnum(dir), - ); + // + // We used to do this by setting the writing direction attribute + // on the attributed string we used, but it seems like that will + // still allow some weird results, for example a single space at + // the end of a line composed of RTL characters will be cause it + // to output a run containing just that space, BEFORE it outputs + // the rest of the line as a separate run, very weirdly with the + // "right to left" flag set in the single space run's run status... + // + // So instead what we do is use a CTTypesetter to create our line, + // using the kCTTypesetterOptionForcedEmbeddingLevel attribute to + // force CoreText not to try doing any sort of BiDi, instead just + // treat all text as embedding level 0 (left to right). + const typesetter_attr_dict = dict: { + const num = try macos.foundation.Number.create(.int, &0); defer num.release(); - - var arr_init = [_]*const macos.foundation.Number{num}; - break :array try macos.foundation.Array.create( - macos.foundation.Number, - &arr_init, + break :dict try macos.foundation.Dictionary.create( + &.{macos.c.kCTTypesetterOptionForcedEmbeddingLevel}, + &.{num}, ); }; - errdefer writing_direction.release(); + errdefer typesetter_attr_dict.release(); // Create the CF release thread. var cf_release_thread = try alloc.create(CFReleaseThread); @@ -210,7 +217,7 @@ pub const Shaper = struct { .run_state = run_state, .features = features, .features_no_default = features_no_default, - .writing_direction = writing_direction, + .typesetter_attr_dict = typesetter_attr_dict, .cached_fonts = .{}, .cached_font_grid = 0, .cf_release_pool = .{}, @@ -224,7 +231,7 @@ pub const Shaper = struct { self.run_state.deinit(self.alloc); self.features.release(); self.features_no_default.release(); - self.writing_direction.release(); + self.typesetter_attr_dict.release(); { for (self.cached_fonts.items) |ft| { @@ -346,8 +353,8 @@ pub const Shaper = struct { run.font_index, ); - // Make room for the attributed string and the CTLine. - try self.cf_release_pool.ensureUnusedCapacity(self.alloc, 3); + // Make room for the attributed string, CTTypesetter, and CTLine. + try self.cf_release_pool.ensureUnusedCapacity(self.alloc, 4); const str = macos.foundation.String.createWithCharactersNoCopy(state.unichars.items); self.cf_release_pool.appendAssumeCapacity(str); @@ -359,8 +366,17 @@ pub const Shaper = struct { ); self.cf_release_pool.appendAssumeCapacity(attr_str); - // We should always have one run because we do our own run splitting. - const line = try macos.text.Line.createWithAttributedString(attr_str); + // Create a typesetter from the attributed string and the cached + // attr dict. (See comment in init for more info on the attr dict.) + const typesetter = + try macos.text.Typesetter.createWithAttributedStringAndOptions( + attr_str, + self.typesetter_attr_dict, + ); + self.cf_release_pool.appendAssumeCapacity(typesetter); + + // Create a line from the typesetter + 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. @@ -369,7 +385,12 @@ pub const Shaper = struct { x: f64 = 0, y: f64 = 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 + // instead of maybe allocating. self.cell_buf.clearRetainingCapacity(); + try self.cell_buf.ensureTotalCapacity(self.alloc, line.getGlyphCount()); // CoreText may generate multiple runs even though our input to // CoreText is already split into runs by our own run iterator. @@ -381,9 +402,9 @@ pub const Shaper = struct { const ctrun = runs.getValueAtIndex(macos.text.Run, i); // Get our glyphs and positions - const glyphs = try ctrun.getGlyphs(alloc); - const advances = try ctrun.getAdvances(alloc); - const indices = try ctrun.getStringIndices(alloc); + const glyphs = ctrun.getGlyphsPtr() orelse try ctrun.getGlyphs(alloc); + const advances = ctrun.getAdvancesPtr() orelse try ctrun.getAdvances(alloc); + const indices = ctrun.getStringIndicesPtr() orelse try ctrun.getStringIndices(alloc); assert(glyphs.len == advances.len); assert(glyphs.len == indices.len); @@ -406,7 +427,7 @@ pub const Shaper = struct { cell_offset = .{ .cluster = cluster }; } - try self.cell_buf.append(self.alloc, .{ + self.cell_buf.appendAssumeCapacity(.{ .x = @intCast(cluster), .x_offset = @intFromFloat(@round(cell_offset.x)), .y_offset = @intFromFloat(@round(cell_offset.y)), @@ -511,15 +532,10 @@ pub const Shaper = struct { // Get our font and use that get the attributes to set for the // attributed string so the whole string uses the same font. const attr_dict = dict: { - var keys = [_]?*const anyopaque{ - macos.text.StringAttribute.font.key(), - macos.text.StringAttribute.writing_direction.key(), - }; - var values = [_]?*const anyopaque{ - run_font, - self.writing_direction, - }; - break :dict try macos.foundation.Dictionary.create(&keys, &values); + break :dict try macos.foundation.Dictionary.create( + &.{macos.text.StringAttribute.font.key()}, + &.{run_font}, + ); }; self.cached_fonts.items[index_int] = attr_dict; diff --git a/src/font/shaper/feature.zig b/src/font/shaper/feature.zig index 5fce7d6eb..40770376b 100644 --- a/src/font/shaper/feature.zig +++ b/src/font/shaper/feature.zig @@ -201,7 +201,7 @@ pub const Feature = struct { self: Feature, comptime layout: []const u8, opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { _ = layout; _ = opts; @@ -262,7 +262,7 @@ pub const FeatureList = struct { self: FeatureList, comptime layout: []const u8, opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { for (self.features.items, 0..) |feature, i| { try feature.format(layout, opts, writer); diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 7bd019fd7..da3c51cee 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -356,8 +356,8 @@ pub const RunIterator = struct { // If this is a grapheme, we need to find a font that supports // all of the codepoints in the grapheme. const cps = self.opts.row.grapheme(cell) orelse return primary; - var candidates = try std.ArrayList(font.Collection.Index).initCapacity(alloc, cps.len + 1); - defer candidates.deinit(); + var candidates: std.ArrayList(font.Collection.Index) = try .initCapacity(alloc, cps.len + 1); + defer candidates.deinit(alloc); candidates.appendAssumeCapacity(primary); for (cps) |cp| { diff --git a/src/font/shaper/web_canvas.zig b/src/font/shaper/web_canvas.zig index 4ed4b7db6..e0f0e1a00 100644 --- a/src/font/shaper/web_canvas.zig +++ b/src/font/shaper/web_canvas.zig @@ -1,9 +1,9 @@ const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; -const ziglyph = @import("ziglyph"); const font = @import("../main.zig"); const terminal = @import("../../terminal/main.zig"); +const unicode = @import("../../unicode/main.zig"); const log = std.log.scoped(.font_shaper); @@ -111,7 +111,7 @@ pub const Shaper = struct { // font ligatures. However, we do support grapheme clustering. // This means we can render things like skin tone emoji but // we can't render things like single glyph "=>". - var break_state: u3 = 0; + var break_state: unicode.GraphemeBreakState = .{}; var cp1: u21 = @intCast(codepoints[0]); var start: usize = 0; @@ -126,7 +126,7 @@ pub const Shaper = struct { const cp2: u21 = @intCast(codepoints[i]); defer cp1 = cp2; - break :blk ziglyph.graphemeBreak( + break :blk unicode.graphemeBreak( cp1, cp2, &break_state, diff --git a/src/global.zig b/src/global.zig index e68ec7f74..8034fabe0 100644 --- a/src/global.zig +++ b/src/global.zig @@ -140,7 +140,7 @@ pub const GlobalState = struct { std.log.info("dependency fontconfig={d}", .{fontconfig.version()}); } std.log.info("renderer={}", .{renderer.Renderer}); - std.log.info("libxev default backend={s}", .{@tagName(xev.backend)}); + std.log.info("libxev default backend={t}", .{xev.backend}); // As early as possible, initialize our resource limits. self.rlimits = .init(); @@ -206,7 +206,7 @@ pub const GlobalState = struct { var sa: p.Sigaction = .{ .handler = .{ .handler = p.SIG.IGN }, - .mask = p.empty_sigset, + .mask = p.sigemptyset(), .flags = 0, }; diff --git a/src/helpgen.zig b/src/helpgen.zig index 57296fe86..fe30db10c 100644 --- a/src/helpgen.zig +++ b/src/helpgen.zig @@ -11,19 +11,22 @@ pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const alloc = gpa.allocator(); - const stdout = std.io.getStdOut().writer(); - try stdout.writeAll( + var buf: [4096]u8 = undefined; + var stdout = std.fs.File.stdout().writer(&buf); + const writer = &stdout.interface; + try writer.writeAll( \\// THIS FILE IS AUTO GENERATED \\ \\ ); - try genConfig(alloc, stdout); - try genActions(alloc, stdout); - try genKeybindActions(alloc, stdout); + try genConfig(alloc, writer); + try genActions(alloc, writer); + try genKeybindActions(alloc, writer); + try stdout.end(); } -fn genConfig(alloc: std.mem.Allocator, writer: anytype) !void { +fn genConfig(alloc: std.mem.Allocator, writer: *std.Io.Writer) !void { var ast = try std.zig.Ast.parse(alloc, @embedFile("config/Config.zig"), .zig); defer ast.deinit(alloc); @@ -44,7 +47,7 @@ fn genConfig(alloc: std.mem.Allocator, writer: anytype) !void { fn genConfigField( alloc: std.mem.Allocator, - writer: anytype, + writer: *std.Io.Writer, ast: std.zig.Ast, comptime field: []const u8, ) !void { @@ -69,7 +72,7 @@ fn genConfigField( } } -fn genActions(alloc: std.mem.Allocator, writer: anytype) !void { +fn genActions(alloc: std.mem.Allocator, writer: *std.Io.Writer) !void { try writer.writeAll( \\ \\/// Actions help @@ -115,7 +118,7 @@ fn genActions(alloc: std.mem.Allocator, writer: anytype) !void { try writer.writeAll("};\n"); } -fn genKeybindActions(alloc: std.mem.Allocator, writer: anytype) !void { +fn genKeybindActions(alloc: std.mem.Allocator, writer: *std.Io.Writer) !void { var ast = try std.zig.Ast.parse(alloc, @embedFile("input/Binding.zig"), .zig); defer ast.deinit(alloc); @@ -149,24 +152,24 @@ fn extractDocComments( } else unreachable; // Go through and build up the lines. - var lines = std.ArrayList([]const u8).init(alloc); - defer lines.deinit(); + var lines: std.ArrayList([]const u8) = .empty; + defer lines.deinit(alloc); for (start_idx..index + 1) |i| { const token = tokens[i]; if (token != .doc_comment) break; - try lines.append(ast.tokenSlice(@intCast(i))[3..]); + try lines.append(alloc, ast.tokenSlice(@intCast(i))[3..]); } // Convert the lines to a multiline string. - var buffer = std.ArrayList(u8).init(alloc); - const writer = buffer.writer(); + var buffer: std.Io.Writer.Allocating = .init(alloc); + defer buffer.deinit(); const prefix = findCommonPrefix(lines); for (lines.items) |line| { - try writer.writeAll(" \\\\"); - try writer.writeAll(line[@min(prefix, line.len)..]); - try writer.writeAll("\n"); + try buffer.writer.writeAll(" \\\\"); + try buffer.writer.writeAll(line[@min(prefix, line.len)..]); + try buffer.writer.writeAll("\n"); } - try writer.writeAll(";\n"); + try buffer.writer.writeAll(";\n"); return buffer.toOwnedSlice(); } diff --git a/src/input.zig b/src/input.zig index caaf80509..be84a60d6 100644 --- a/src/input.zig +++ b/src/input.zig @@ -1,6 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); +const config = @import("input/config.zig"); const mouse = @import("input/mouse.zig"); const key = @import("input/key.zig"); const keyboard = @import("input/keyboard.zig"); @@ -8,7 +9,9 @@ const keyboard = @import("input/keyboard.zig"); pub const command = @import("input/command.zig"); pub const function_keys = @import("input/function_keys.zig"); pub const keycodes = @import("input/keycodes.zig"); +pub const key_encode = @import("input/key_encode.zig"); pub const kitty = @import("input/kitty.zig"); +pub const paste = @import("input/paste.zig"); pub const ctrlOrSuper = key.ctrlOrSuper; pub const Action = key.Action; @@ -17,13 +20,13 @@ pub const Command = command.Command; pub const Link = @import("input/Link.zig"); pub const Key = key.Key; pub const KeyboardLayout = keyboard.Layout; -pub const KeyEncoder = @import("input/KeyEncoder.zig"); pub const KeyEvent = key.KeyEvent; pub const InspectorMode = Binding.Action.InspectorMode; pub const Mods = key.Mods; pub const MouseButton = mouse.Button; pub const MouseButtonState = mouse.ButtonState; pub const MousePressureStage = mouse.PressureStage; +pub const OptionAsAlt = config.OptionAsAlt; pub const ScrollMods = mouse.ScrollMods; pub const SplitFocusDirection = Binding.Action.SplitFocusDirection; pub const SplitResizeDirection = Binding.Action.SplitResizeDirection; diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 016f6a947..9bdd858c1 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -6,7 +6,8 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const build_config = @import("../build_config.zig"); -const ziglyph = @import("ziglyph"); +const uucode = @import("uucode"); +const EntryFormatter = @import("../config/formatter.zig").EntryFormatter; const key = @import("key.zig"); const KeyEvent = key.KeyEvent; @@ -346,6 +347,10 @@ pub const Action = union(enum) { /// Scroll to the selected text. scroll_to_selection, + /// Scroll to the given absolute row in the screen with 0 being + /// the first row. + scroll_to_row: usize, + /// Scroll the screen up by one page. scroll_page_up, @@ -632,6 +637,17 @@ pub const Action = union(enum) { /// Only implemented on macOS, as this uses a built-in system API. toggle_secure_input, + /// Toggle mouse reporting on or off. + /// + /// When mouse reporting is disabled, mouse events will not be reported to + /// terminal applications even if they request it. This allows you to always + /// use the mouse for selection and other terminal UI interactions without + /// applications capturing mouse input. + /// + /// This can also be controlled via the `mouse-reporting` configuration + /// option. + toggle_mouse_reporting, + /// Toggle the command palette. /// /// The command palette is a popup that lets you see what actions @@ -1076,6 +1092,7 @@ pub const Action = union(enum) { .scroll_to_top, .scroll_to_bottom, .scroll_to_selection, + .scroll_to_row, .scroll_page_up, .scroll_page_down, .scroll_page_fractional, @@ -1093,6 +1110,7 @@ pub const Action = union(enum) { .toggle_window_decorations, .toggle_window_float_on_top, .toggle_secure_input, + .toggle_mouse_reporting, .toggle_command_palette, .show_on_screen_keyboard, .reset_window_size, @@ -1184,13 +1202,8 @@ pub const Action = union(enum) { /// action back into the format used by parse. pub fn format( self: Action, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { - _ = layout; - _ = opts; - switch (self) { inline else => |value| { // All actions start with the tag. @@ -1206,16 +1219,16 @@ pub const Action = union(enum) { } fn formatValue( - writer: anytype, + writer: *std.Io.Writer, value: anytype, ) !void { const Value = @TypeOf(value); const value_info = @typeInfo(Value); switch (Value) { void => {}, - []const u8 => try std.zig.stringEscape(value, "", .{}, writer), + []const u8 => try std.zig.stringEscape(value, writer), else => switch (value_info) { - .@"enum" => try writer.print("{s}", .{@tagName(value)}), + .@"enum" => try writer.print("{t}", .{value}), .float => try writer.print("{d}", .{value}), .int => try writer.print("{d}", .{value}), .@"struct" => |info| if (!info.is_tuple) { @@ -1618,15 +1631,19 @@ pub const Trigger = struct { /// in more codepoints so we need to use a 3 element array. fn foldedCodepoint(cp: u21) [3]u21 { // ASCII fast path - if (ziglyph.letter.isAsciiLetter(cp)) { - return .{ ziglyph.letter.toLower(cp), 0, 0 }; + if (uucode.ascii.isAlphabetic(cp)) { + return .{ uucode.ascii.toLower(cp), 0, 0 }; } - // Unicode slow path. Case folding can resultin more codepoints. + // Unicode slow path. Case folding can result in more codepoints. // If more codepoints are produced then we return the codepoint // as-is which isn't correct but until we have a failing test // then I don't want to handle this. - return ziglyph.letter.toCaseFold(cp); + var buffer: [1]u21 = undefined; + const slice = uucode.get(.case_folding_full, cp).with(&buffer, cp); + var array: [3]u21 = [_]u21{0} ** 3; + @memcpy(array[0..slice.len], slice); + return array; } /// Convert the trigger to a C API compatible trigger. @@ -1644,13 +1661,8 @@ pub const Trigger = struct { /// Format implementation for fmt package. pub fn format( self: Trigger, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { - _ = layout; - _ = opts; - // Modifiers first if (self.mods.super) try writer.writeAll("super+"); if (self.mods.ctrl) try writer.writeAll("ctrl+"); @@ -1659,7 +1671,7 @@ pub const Trigger = struct { // Key switch (self.key) { - .physical => |k| try writer.print("{s}", .{@tagName(k)}), + .physical => |k| try writer.print("{t}", .{k}), .unicode => |c| try writer.print("{u}", .{c}), } } @@ -1717,13 +1729,8 @@ pub const Set = struct { /// action back into the format used by parse. pub fn format( self: Value, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { - _ = layout; - _ = opts; - switch (self) { .leader => |set| { // the leader key was already printed. @@ -1754,26 +1761,34 @@ pub const Set = struct { /// 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 /// to the buffer before flushing it to the formatter with 'c=x' and 'd=y'. - pub fn formatEntries(self: Value, buffer_stream: anytype, formatter: anytype) !void { + pub fn formatEntries( + self: Value, + buffer: *std.Io.Writer, + formatter: EntryFormatter, + ) !void { switch (self) { .leader => |set| { // We'll rewind to this position after each sub-entry, // sharing the prefix between siblings. - const pos = try buffer_stream.getPos(); + const pos = buffer.end; var iter = set.bindings.iterator(); while (iter.next()) |binding| { - buffer_stream.seekTo(pos) catch unreachable; // can't fail - std.fmt.format(buffer_stream.writer(), ">{s}", .{binding.key_ptr.*}) catch return error.OutOfMemory; - try binding.value_ptr.*.formatEntries(buffer_stream, formatter); + // I'm not exactly if this is safe for any arbitrary + // writer since the Writer interface does not have any + // rewind functions, but for our use case of a + // fixed-size buffer writer this should work just fine. + buffer.end = pos; + buffer.print(">{f}", .{binding.key_ptr.*}) catch return error.OutOfMemory; + try binding.value_ptr.*.formatEntries(buffer, formatter); } }, .leaf => |leaf| { // When we get to the leaf, the buffer_stream contains // the full sequence of keys needed to reach this action. - std.fmt.format(buffer_stream.writer(), "={s}", .{leaf.action}) catch return error.OutOfMemory; - try formatter.formatEntry([]const u8, buffer_stream.getWritten()); + buffer.print("={f}", .{leaf.action}) catch return error.OutOfMemory; + try formatter.formatEntry([]const u8, buffer.buffer[0..buffer.end]); }, } } @@ -3230,11 +3245,8 @@ test "action: format" { const a: Action = .{ .text = "👻" }; - var buf: std.ArrayListUnmanaged(u8) = .empty; - defer buf.deinit(alloc); - - const writer = buf.writer(alloc); - try a.format("", .{}, writer); - - try testing.expectEqualStrings("text:\\xf0\\x9f\\x91\\xbb", buf.items); + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try a.format(&buf.writer); + try testing.expectEqualStrings("text:\\xf0\\x9f\\x91\\xbb", buf.written()); } diff --git a/src/input/command.zig b/src/input/command.zig index bf5061c12..0904ef2bb 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -50,7 +50,7 @@ pub const Command = struct { return .{ .action_key = @tagName(self.action), - .action = std.fmt.comptimePrint("{s}", .{self.action}), + .action = std.fmt.comptimePrint("{t}", .{self.action}), .title = self.title, .description = self.description, }; @@ -94,6 +94,7 @@ pub const defaults: []const Command = defaults: { /// Defaults in C-compatible form. pub const defaultsC: []const Command.C = defaults: { + @setEvalBranchQuota(100_000); var result: [defaults.len]Command.C = undefined; for (defaults, 0..) |cmd, i| result[i] = cmd.comptimeCval(); const final = result; @@ -448,6 +449,12 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Toggle secure input mode.", }}, + .toggle_mouse_reporting => comptime &.{.{ + .action = .toggle_mouse_reporting, + .title = "Toggle Mouse Reporting", + .description = "Toggle whether mouse events are reported to terminal applications.", + }}, + .check_for_updates => comptime &.{.{ .action = .check_for_updates, .title = "Check for Updates", @@ -486,6 +493,7 @@ fn actionCommands(action: Action.Key) []const Command { .esc, .cursor_key, .set_font_size, + .scroll_to_row, .scroll_page_fractional, .scroll_page_lines, .adjust_selection, diff --git a/src/input/config.zig b/src/input/config.zig new file mode 100644 index 000000000..fd839a20e --- /dev/null +++ b/src/input/config.zig @@ -0,0 +1,8 @@ +/// Determines the macOS option key behavior. See the config +/// `macos-option-as-alt` for a lot more details. +pub const OptionAsAlt = enum(c_int) { + false, + true, + left, + right, +}; diff --git a/src/input/function_keys.zig b/src/input/function_keys.zig index 33a5b89c0..efe86d9e3 100644 --- a/src/input/function_keys.zig +++ b/src/input/function_keys.zig @@ -278,6 +278,7 @@ fn pcStyle(comptime fmt: []const u8) []Entry { // The comptime {} wrapper is superfluous but it prevents us from // accidentally running this function at runtime. comptime { + @setEvalBranchQuota(500_000); var entries: [modifiers.len]Entry = undefined; for (modifiers, 2.., 0..) |mods, code, i| { entries[i] = .{ @@ -292,6 +293,11 @@ fn pcStyle(comptime fmt: []const u8) []Entry { test "keys" { const testing = std.testing; + switch (@import("terminal_options").artifact) { + .ghostty => {}, + // Don't want to bring in termio into libghostty-vt + .lib => return error.SkipZigTest, + } // Force resolution for comptime evaluation. _ = keys; diff --git a/src/input/helpgen_actions.zig b/src/input/helpgen_actions.zig index 1382bbe95..4210f1f91 100644 --- a/src/input/helpgen_actions.zig +++ b/src/input/helpgen_actions.zig @@ -13,7 +13,7 @@ pub const Format = enum { /// Markdown formatted output markdown, - fn formatFieldName(self: Format, writer: anytype, field_name: []const u8) !void { + fn formatFieldName(self: Format, writer: *std.Io.Writer, field_name: []const u8) !void { switch (self) { .plaintext => { try writer.writeAll(field_name); @@ -27,16 +27,16 @@ pub const Format = enum { } } - fn formatDocLine(self: Format, writer: anytype, line: []const u8) !void { + fn formatDocLine(self: Format, writer: *std.Io.Writer, line: []const u8) !void { switch (self) { .plaintext => { - try writer.appendSlice(" "); - try writer.appendSlice(line); - try writer.appendSlice("\n"); + try writer.writeAll(" "); + try writer.writeAll(line); + try writer.writeAll("\n"); }, .markdown => { - try writer.appendSlice(line); - try writer.appendSlice("\n"); + try writer.writeAll(line); + try writer.writeAll("\n"); }, } } @@ -61,7 +61,7 @@ pub const Format = enum { /// Generate keybind actions documentation with the specified format pub fn generate( - writer: anytype, + writer: *std.Io.Writer, format: Format, show_docs: bool, page_allocator: std.mem.Allocator, @@ -70,8 +70,8 @@ pub fn generate( try writer.writeAll(header); } - var buffer = std.ArrayList(u8).init(page_allocator); - defer buffer.deinit(); + var stream: std.Io.Writer.Allocating = .init(page_allocator); + defer stream.deinit(); const fields = @typeInfo(KeybindAction).@"union".fields; inline for (fields) |field| { @@ -79,10 +79,9 @@ pub fn generate( // Write previously stored doc comment below all related actions if (show_docs and @hasDecl(help_strings.KeybindAction, field.name)) { - try writer.writeAll(buffer.items); + try writer.writeAll(stream.written()); try writer.writeAll("\n"); - - buffer.clearRetainingCapacity(); + stream.clearRetainingCapacity(); } if (show_docs) { @@ -101,13 +100,13 @@ pub fn generate( while (iter.next()) |s| { // If it is the last line and empty, then skip it. if (iter.peek() == null and s.len == 0) continue; - try format.formatDocLine(&buffer, s); + try format.formatDocLine(&stream.writer, s); } } } // Write any remaining buffered documentation - if (buffer.items.len > 0) { - try writer.writeAll(buffer.items); + if (stream.written().len > 0) { + try writer.writeAll(stream.written()); } } diff --git a/src/input/key.zig b/src/input/key.zig index a3814fb55..54c7491ae 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -2,7 +2,7 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const cimgui = @import("cimgui"); -const config = @import("../config.zig"); +const OptionAsAlt = @import("config.zig").OptionAsAlt; /// A generic key input event. This is the information that is necessary /// regardless of apprt in order to generate the proper terminal @@ -146,7 +146,7 @@ pub const Mods = packed struct(Mods.Backing) { /// 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: config.OptionAsAlt) Mods { + pub fn translation(self: Mods, option_as_alt: OptionAsAlt) Mods { var result = self; // macos-option-as-alt for darwin diff --git a/src/input/KeyEncoder.zig b/src/input/key_encode.zig similarity index 62% rename from src/input/KeyEncoder.zig rename to src/input/key_encode.zig index b5f18b5a2..f411deb19 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/key_encode.zig @@ -1,86 +1,138 @@ -/// KeyEncoder is responsible for processing keyboard input and generating -/// the proper VT sequence for any events. -/// -/// A new KeyEncoder should be created for each individual key press. -/// These encoders are not meant to be reused. -const KeyEncoder = @This(); - const std = @import("std"); const builtin = @import("builtin"); const testing = std.testing; - -const key = @import("key.zig"); -const config = @import("../config.zig"); +const KittyFlags = @import("../terminal/kitty/key.zig").Flags; +const OptionAsAlt = @import("config.zig").OptionAsAlt; +const Terminal = @import("../terminal/Terminal.zig"); const function_keys = @import("function_keys.zig"); -const terminal = @import("../terminal/main.zig"); +const key = @import("key.zig"); const KittyEntry = @import("kitty.zig").Entry; const kitty_entries = @import("kitty.zig").entries; -const KittyFlags = terminal.kitty.KeyFlags; -const log = std.log.scoped(.key_encoder); +/// Options that affect key encoding behavior. This is a mix of behavior +/// from terminal state as well as application configuration. +pub const Options = struct { + /// Terminal DEC mode 1 + cursor_key_application: bool = false, -event: key.KeyEvent, + /// Terminal DEC mode 66 + keypad_key_application: bool = false, -/// The state of various modes of a terminal that impact encoding. -macos_option_as_alt: config.OptionAsAlt = .false, -alt_esc_prefix: bool = false, -cursor_key_application: bool = false, -keypad_key_application: bool = false, -ignore_keypad_with_numlock: bool = false, -modify_other_keys_state_2: bool = false, -kitty_flags: KittyFlags = .{}, + /// Terminal DEC mode 1035 + ignore_keypad_with_numlock: bool = false, -/// Perform the proper encoding depending on the terminal state. + /// Terminal DEC mode 1036 + alt_esc_prefix: bool = false, + + /// xterm "modifyOtherKeys mode 2". Details here: + /// https://invisible-island.net/xterm/modified-keys.html + modify_other_keys_state_2: bool = false, + + /// Kitty keyboard protocol flags. + kitty_flags: KittyFlags = .disabled, + + /// Determines whether the "option" key on macOS is treated + /// as "alt" or not. See the Ghostty `macos_option-as-alt` config + /// docs for a more detailed description of why this is needed. + macos_option_as_alt: OptionAsAlt = .false, + + pub const default: Options = .{ + .cursor_key_application = false, + .keypad_key_application = false, + .ignore_keypad_with_numlock = false, + .alt_esc_prefix = false, + .modify_other_keys_state_2 = false, + .kitty_flags = .disabled, + .macos_option_as_alt = .false, + }; + + /// Initialize our options from the terminal state. + /// + /// Note that `macos_option_as_alt` cannot be determined from + /// terminal state so it must be set manually after this call. + pub fn fromTerminal(t: *const Terminal) Options { + return .{ + .alt_esc_prefix = t.modes.get(.alt_esc_prefix), + .cursor_key_application = t.modes.get(.cursor_keys), + .keypad_key_application = t.modes.get(.keypad_keys), + .ignore_keypad_with_numlock = t.modes.get(.ignore_keypad_with_numlock), + .modify_other_keys_state_2 = t.flags.modify_other_keys_2, + .kitty_flags = t.screen.kitty_keyboard.current(), + + // These can't be known from the terminal state. + .macos_option_as_alt = .false, + }; + } +}; + +/// Encode the key event to the writer in the proper format given +/// the options. For example, this will properly encode a key press +/// such as "ctrl+A" to Kitty format if Kitty encoding is enabled. +/// +/// Not all key events will result in output. It is up to the caller +/// to use a writer that can track whether any output was written if +/// they care about that. pub fn encode( - self: *const KeyEncoder, - buf: []u8, -) ![]const u8 { - // log.warn("KEYENCODER self={}", .{self.*}); - if (self.kitty_flags.int() != 0) return try self.kitty(buf); - return try self.legacy(buf); + writer: *std.Io.Writer, + event: key.KeyEvent, + opts: Options, +) std.Io.Writer.Error!void { + //std.log.warn("KEYENCODER event={} opts={}", .{ event, opts }); + return if (opts.kitty_flags.int() != 0) try kitty( + writer, + event, + opts, + ) else try legacy( + writer, + event, + opts, + ); } /// Perform Kitty keyboard protocol encoding of the key event. fn kitty( - self: *const KeyEncoder, - buf: []u8, -) ![]const u8 { + writer: *std.Io.Writer, + event: key.KeyEvent, + opts: Options, +) std.Io.Writer.Error!void { // This should never happen but we'll check anyway. - if (self.kitty_flags.int() == 0) return try self.legacy(buf); + if (opts.kitty_flags.int() == 0) return try legacy( + writer, + event, + opts, + ); // We only processed "press" events unless report events is active - if (self.event.action == .release) { - if (!self.kitty_flags.report_events) { - return ""; - } + if (event.action == .release) { + if (!opts.kitty_flags.report_events) return; // Enter, backspace, and tab do not report release events unless "report // all" is set - if (!self.kitty_flags.report_all) { - switch (self.event.key) { - .enter, .backspace, .tab => return "", + if (!opts.kitty_flags.report_all) { + switch (event.key) { + .enter, .backspace, .tab => return, else => {}, } } } - const all_mods = self.event.mods; - const effective_mods = self.event.effectiveMods(); + const all_mods = event.mods; + const effective_mods = event.effectiveMods(); const binding_mods = effective_mods.binding(); // Find the entry for this key in the kitty table. const entry_: ?KittyEntry = entry: { // Functional or predefined keys for (kitty_entries) |entry| { - if (entry.key == self.event.key) break :entry entry; + if (entry.key == event.key) break :entry entry; } // Otherwise, we use our unicode codepoint from UTF8. We // always use the unshifted value. - if (self.event.unshifted_codepoint > 0) { + if (event.unshifted_codepoint > 0) { break :entry .{ - .key = self.event.key, - .code = self.event.unshifted_codepoint, + .key = event.key, + .code = event.unshifted_codepoint, .final = 'u', .modifier = false, }; @@ -91,32 +143,32 @@ fn kitty( preprocessing: { // When composing, the only keys sent are plain modifiers. - if (self.event.composing) { + if (event.composing) { if (entry_) |entry| { if (entry.modifier) break :preprocessing; } - return ""; + return; } // 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 // on how to verify this. - if (self.event.utf8.len > 0) utf8: { - switch (self.event.key) { + if (event.utf8.len > 0) utf8: { + switch (event.key) { else => {}, inline .enter, .backspace => |tag| { // See legacy for why we handle this this way. - if (isControlUtf8(self.event.utf8)) break :utf8; - if (comptime tag == .backspace) return ""; - return try copyToBuf(buf, self.event.utf8); + if (isControlUtf8(event.utf8)) break :utf8; + if (comptime tag == .backspace) return; + return try writer.writeAll(event.utf8); }, } } // If we're reporting all then we always send CSI sequences. - if (!self.kitty_flags.report_all) { + if (!opts.kitty_flags.report_all) { // Quote: // The only exceptions are the Enter, Tab and Backspace keys which // still generate the same bytes as in legacy mode this is to allow the @@ -127,63 +179,73 @@ fn kitty( // Note that all keys are reported as escape codes, including Enter, // Tab, Backspace etc. if (effective_mods.empty()) { - switch (self.event.key) { - .enter => return try copyToBuf(buf, "\r"), - .tab => return try copyToBuf(buf, "\t"), - .backspace => return try copyToBuf(buf, "\x7F"), + switch (event.key) { + .enter => return try writer.writeByte('\r'), + .tab => return try writer.writeByte('\t'), + .backspace => return try writer.writeByte(0x7F), else => {}, } } // Send plain-text non-modified text directly to the terminal. // We don't send release events because those are specially encoded. - if (self.event.utf8.len > 0 and + if (event.utf8.len > 0 and binding_mods.empty() and - self.event.action != .release) + event.action != .release) plain_text: { // We only do this for printable characters. We should // inspect the real unicode codepoint properties here but // the real world issue is usually control characters. - const view = try std.unicode.Utf8View.init(self.event.utf8); + const view = std.unicode.Utf8View.init(event.utf8) catch { + // Invalid UTF-8 so let's fallback to encoding the + // key press as if it didn't produce UTF-8 text. I'm + // not sure what should happen here according to the spec, + // since it doesn't specify this behavior. Presumably + // this is a caller bug. + break :plain_text; + }; var it = view.iterator(); while (it.nextCodepoint()) |cp| { if (isControl(cp)) break :plain_text; } - return try copyToBuf(buf, self.event.utf8); + return try writer.writeAll(event.utf8); } } } - const entry = entry_ orelse return ""; + const entry = entry_ orelse return; // If this is just a modifier we require "report all" to send the sequence. - if (entry.modifier and !self.kitty_flags.report_all) return ""; + if (entry.modifier and !opts.kitty_flags.report_all) return; const seq: KittySequence = seq: { var seq: KittySequence = .{ .key = entry.code, .final = entry.final, .mods = .fromInput( - self.event.action, - self.event.key, + event.action, + event.key, all_mods, ), }; - if (self.kitty_flags.report_events) { - seq.event = switch (self.event.action) { + if (opts.kitty_flags.report_events) { + seq.event = switch (event.action) { .press => .press, .release => .release, .repeat => .repeat, }; } - if (self.kitty_flags.report_alternates) alternates: { + if (opts.kitty_flags.report_alternates) alternates: { // Break early if this is a control key if (isControl(seq.key)) break :alternates; - const view = try std.unicode.Utf8View.init(self.event.utf8); + const view = std.unicode.Utf8View.init(event.utf8) catch { + // Assume invalid UTF-8 means no UTF-8. + break :alternates; + }; var it = view.iterator(); // If we have a codepoint in our UTF-8 sequence, then we can @@ -198,7 +260,7 @@ fn kitty( // Set the base layout key. We only report this if this codepoint // differs from our pressed key. - if (self.event.key.codepoint()) |base| { + if (event.key.codepoint()) |base| { if (base != seq.key and (cp1 != base and !has_cp2)) { @@ -208,20 +270,20 @@ fn kitty( } else { // No UTF-8 so we can't report a shifted key but we can still // report a base layout key. - if (self.event.key.codepoint()) |base| { + if (event.key.codepoint()) |base| { if (base != seq.key) seq.alternates[1] = base; } } } - if (self.kitty_flags.report_associated and + if (opts.kitty_flags.report_associated and seq.event != .release) associated: { // Determine if the Alt modifier should be treated as an actual // modifier (in which case it prevents associated text) or as // the macOS Option key, which does not prevent associated text. const alt_prevents_text = if (comptime builtin.os.tag == .macos) - switch (self.macos_option_as_alt) { + switch (opts.macos_option_as_alt) { .left => all_mods.sides.alt == .left, .right => all_mods.sides.alt == .right, .true => true, @@ -232,13 +294,13 @@ fn kitty( if (seq.mods.preventsText(alt_prevents_text)) break :associated; - seq.text = self.event.utf8; + seq.text = event.utf8; } break :seq seq; }; - return try seq.encode(buf); + return try seq.encode(writer); } /// Perform legacy encoding of the key event. "Legacy" in this case @@ -248,28 +310,28 @@ fn kitty( /// meant to be extensions that do not change any existing behavior /// and therefore safe to combine. fn legacy( - self: *const KeyEncoder, - buf: []u8, -) ![]const u8 { - const all_mods = self.event.mods; - const effective_mods = self.event.effectiveMods(); + writer: *std.Io.Writer, + event: key.KeyEvent, + opts: Options, +) std.Io.Writer.Error!void { + const all_mods = event.mods; + const effective_mods = event.effectiveMods(); const binding_mods = effective_mods.binding(); // Legacy encoding only does press/repeat - if (self.event.action != .press and - self.event.action != .repeat) return ""; + if (event.action != .press and event.action != .repeat) return; // If we're in a dead key state then we never emit a sequence. - if (self.event.composing) return ""; + if (event.composing) return; // If we match a PC style function key then that is our result. if (pcStyleFunctionKey( - self.event.key, + event.key, all_mods, - self.cursor_key_application, - self.keypad_key_application, - self.ignore_keypad_with_numlock, - self.modify_other_keys_state_2, + opts.cursor_key_application, + opts.keypad_key_application, + opts.ignore_keypad_with_numlock, + opts.modify_other_keys_state_2, )) |sequence| pc_style: { // If we have UTF-8 text, then we never emit PC style function // keys. Many function keys (escape, enter, backspace) have @@ -280,65 +342,68 @@ fn legacy( // - Korean: escape commits the dead key state // - Korean: backspace should delete a single preedit char // - if (self.event.utf8.len > 0) utf8: { - switch (self.event.key) { + if (event.utf8.len > 0) utf8: { + switch (event.key) { else => {}, inline .backspace, .enter, .escape => |tag| { // We want to ignore control characters. This is because // some apprts (macOS) will send control characters as // UTF-8 encodings and we handle that manually. - if (isControlUtf8(self.event.utf8)) break :utf8; + if (isControlUtf8(event.utf8)) break :utf8; // Backspace encodes nothing because we modified IME. // Enter/escape don't encode the PC-style encoding // because we want to encode committed text. - if (comptime tag == .backspace) return ""; + if (comptime tag == .backspace) return; break :pc_style; }, } } - return copyToBuf(buf, sequence); + return try writer.writeAll(sequence); } // If we match a control sequence, we output that directly. For // ctrlSeq we have to use all mods because we want it to only // match ctrl+. if (ctrlSeq( - self.event.key, - self.event.utf8, - self.event.unshifted_codepoint, + event.key, + event.utf8, + event.unshifted_codepoint, all_mods, )) |char| { // C0 sequences support alt-as-esc prefixing. if (binding_mods.alt) { - if (buf.len < 2) return error.OutOfMemory; - buf[0] = 0x1B; - buf[1] = char; - return buf[0..2]; + try writer.writeByte(0x1B); + try writer.writeByte(char); + return; } - if (buf.len < 1) return error.OutOfMemory; - buf[0] = char; - return buf[0..1]; + try writer.writeByte(char); + return; } // If we have no UTF8 text then the only possibility is the // alt-prefix handling of unshifted codepoints... so we process that. - const utf8 = self.event.utf8; + const utf8 = event.utf8; if (utf8.len == 0) { - if (try self.legacyAltPrefix(binding_mods, all_mods)) |byte| { - return try std.fmt.bufPrint(buf, "\x1B{c}", .{byte}); - } - - return ""; + if (try legacyAltPrefix( + event, + binding_mods, + all_mods, + opts, + )) |byte| try writer.print("\x1B{c}", .{byte}); + return; } // In modify other keys state 2, we send the CSI 27 sequence // for any char with a modifier. Ctrl sequences like Ctrl+a // are already handled above. - if (self.modify_other_keys_state_2) modify_other: { - const view = try std.unicode.Utf8View.init(utf8); + if (opts.modify_other_keys_state_2) modify_other: { + const view = std.unicode.Utf8View.init(utf8) catch { + // Assume invalid UTF-8 means we no UTF-8. + break :modify_other; + }; var it = view.iterator(); const codepoint = it.nextCodepoint() orelse break :modify_other; @@ -346,6 +411,10 @@ fn legacy( // ever be a multi-codepoint sequence that triggers this. if (it.nextCodepoint() != null) break :modify_other; + // The mods we encode for this are just the binding mods (shift, ctrl, + // super, alt). + const mods = event.mods.binding(); + // This copies xterm's `ModifyOtherKeys` function that returns // whether modify other keys should be encoded for the given // input. @@ -355,7 +424,7 @@ fn legacy( break :should_modify true; // If we have anything other than shift pressed, encode. - var mods_no_shift = binding_mods; + var mods_no_shift = mods; mods_no_shift.shift = false; if (!mods_no_shift.empty()) break :should_modify true; @@ -370,9 +439,8 @@ fn legacy( if (should_modify) { for (function_keys.modifiers, 2..) |modset, code| { - if (!binding_mods.equal(modset)) continue; - return try std.fmt.bufPrint( - buf, + if (!mods.equal(modset)) continue; + return try writer.print( "\x1B[27;{};{}~", .{ code, codepoint }, ); @@ -383,17 +451,17 @@ fn legacy( // Let's see if we should apply fixterms to this codepoint. // At this stage of key processing, we only need to apply fixterms // to unicode codepoints if we have ctrl set. - if (self.event.mods.ctrl) csiu: { + if (event.mods.ctrl) csiu: { // Important: we want to use the original mods here, not the // effective mods. The fixterms spec states the shifted chars // should be sent uppercase but Kitty changes that behavior // so we'll send all the mods. const csi_u_mods, const char = mods: { - var mods = CsiUMods.fromInput(self.event.mods); + var mods = CsiUMods.fromInput(event.mods); // Get our codepoint. If we have more than one codepoint this // can't be valid CSIu. - const view = std.unicode.Utf8View.init(self.event.utf8) catch break :csiu; + const view = std.unicode.Utf8View.init(event.utf8) catch break :csiu; var it = view.iterator(); var char = it.nextCodepoint() orelse break :csiu; if (it.nextCodepoint() != null) break :csiu; @@ -414,25 +482,27 @@ fn legacy( // then we consider shift. Otherwise, we do not because the // shift key was used to obtain the character. This is specified // by fixterms. - if (self.event.unshifted_codepoint != char) { + if (event.unshifted_codepoint != char) { mods.shift = false; } break :mods .{ mods, char }; }; - const result = try std.fmt.bufPrint( - buf, + return try writer.print( "\x1B[{};{}u", .{ char, csi_u_mods.seqInt() }, ); - // std.log.warn("CSI_U: {s}", .{result}); - return result; } // If we have alt-pressed and alt-esc-prefix is enabled, then // we need to prefix the utf8 sequence with an esc. - if (try self.legacyAltPrefix(binding_mods, all_mods)) |byte| { - return try std.fmt.bufPrint(buf, "\x1B{c}", .{byte}); + if (try legacyAltPrefix( + event, + binding_mods, + all_mods, + opts, + )) |byte| { + return try writer.print("\x1B{c}", .{byte}); } // If we are on macOS, command+keys do not encode text. It isn't @@ -445,25 +515,26 @@ fn legacy( // For example on Gnome Console Super+b will encode a "b" character // with legacy encoding. if ((comptime builtin.os.tag == .macos) and all_mods.super) { - return ""; + return; } - return try copyToBuf(buf, utf8); + return try writer.writeAll(utf8); } fn legacyAltPrefix( - self: *const KeyEncoder, + event: key.KeyEvent, binding_mods: key.Mods, mods: key.Mods, + opts: Options, ) !?u8 { // This only takes effect with alt pressed - if (!binding_mods.alt or !self.alt_esc_prefix) return null; + if (!binding_mods.alt or !opts.alt_esc_prefix) return null; // On macOS, we only handle option like alt in certain // circumstances. Otherwise, macOS does a unicode translation // and we allow that to happen. if (comptime builtin.os.tag == .macos) { - switch (self.macos_option_as_alt) { + switch (opts.macos_option_as_alt) { .false => return null, .left => if (mods.sides.alt == .right) return null, .right => if (mods.sides.alt == .left) return null, @@ -472,7 +543,7 @@ fn legacyAltPrefix( } // Otherwise, we require utf8 to already have the byte represented. - const utf8 = self.event.utf8; + const utf8 = event.utf8; if (utf8.len == 1) { if (std.math.cast(u8, utf8[0])) |byte| { return byte; @@ -480,10 +551,10 @@ fn legacyAltPrefix( } // If UTF8 isn't set, we will allow unshifted codepoints through. - if (self.event.unshifted_codepoint > 0) { + if (event.unshifted_codepoint > 0) { if (std.math.cast( u8, - self.event.unshifted_codepoint, + event.unshifted_codepoint, )) |byte| { return byte; } @@ -897,19 +968,18 @@ const KittySequence = struct { release = 3, }; - pub fn encode(self: KittySequence, buf: []u8) ![]const u8 { - if (self.final == 'u' or self.final == '~') return try self.encodeFull(buf); - return try self.encodeSpecial(buf); + pub fn encode( + self: KittySequence, + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { + if (self.final == 'u' or self.final == '~') return try self.encodeFull(writer); + return try self.encodeSpecial(writer); } - fn encodeFull(self: KittySequence, buf: []u8) ![]const u8 { - // Boilerplate to basically create a string builder that writes - // over our buffer (but no more). - var fba = std.heap.FixedBufferAllocator.init(buf); - const alloc = fba.allocator(); - var builder = try std.ArrayListUnmanaged(u8).initCapacity(alloc, buf.len); - const writer = builder.writer(alloc); - + fn encodeFull( + self: KittySequence, + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { // Key section try writer.print("\x1B[{d}", .{self.key}); // Write our alternates @@ -937,8 +1007,11 @@ const KittySequence = struct { } // Text section - if (self.text.len > 0) { - const view = try std.unicode.Utf8View.init(self.text); + if (self.text.len > 0) text: { + const view = std.unicode.Utf8View.init(self.text) catch { + // Assume invalid UTF-8 means we have no text. + break :text; + }; var it = view.iterator(); var count: usize = 0; while (it.nextCodepoint()) |cp| { @@ -960,13 +1033,15 @@ const KittySequence = struct { } try writer.print("{c}", .{self.final}); - return builder.items; } - fn encodeSpecial(self: KittySequence, buf: []u8) ![]const u8 { + fn encodeSpecial( + self: KittySequence, + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { const mods = self.mods.seqInt(); if (self.event != .none) { - return try std.fmt.bufPrint(buf, "\x1B[1;{d}:{d}{c}", .{ + return try writer.print("\x1B[1;{d}:{d}{c}", .{ mods, @intFromEnum(self.event), self.final, @@ -974,13 +1049,13 @@ const KittySequence = struct { } if (mods > 1) { - return try std.fmt.bufPrint(buf, "\x1B[1;{d}{c}", .{ + return try writer.print("\x1B[1;{d}{c}", .{ mods, self.final, }); } - return try std.fmt.bufPrint(buf, "\x1B[{c}", .{self.final}); + return try writer.print("\x1B[{c}", .{self.final}); } }; @@ -989,27 +1064,30 @@ test "KittySequence: backspace" { // Plain { + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u' }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1B[127u", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1B[127u", writer.buffered()); } // Release event { + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .event = .release }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1B[127;1:3u", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1B[127;1:3u", writer.buffered()); } // Shift { + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .mods = .{ .shift = true }, }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1B[127;2u", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1B[127;2u", writer.buffered()); } } @@ -1018,221 +1096,214 @@ test "KittySequence: text" { // Plain { + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .text = "A", }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1B[127;;65u", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1B[127;;65u", writer.buffered()); } // Release { + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .event = .release, .text = "A", }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1B[127;1:3;65u", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1B[127;1:3;65u", writer.buffered()); } // Shift { + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .mods = .{ .shift = true }, .text = "A", }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1B[127;2;65u", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1B[127;2;65u", writer.buffered()); } } - +// test "KittySequence: text with control characters" { var buf: [128]u8 = undefined; // By itself { + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .text = "\n", }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1b[127u", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1b[127u", writer.buffered()); } // With other printables { + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .text = "A\n", }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1b[127;;65u", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1b[127;;65u", writer.buffered()); } } - +// test "KittySequence: special no mods" { var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 1, .final = 'A' }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1B[A", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1B[A", writer.buffered()); } test "KittySequence: special mods only" { var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 1, .final = 'A', .mods = .{ .shift = true } }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1B[1;2A", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1B[1;2A", writer.buffered()); } test "KittySequence: special mods and event" { var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 1, .final = 'A', .event = .release, .mods = .{ .shift = true }, }; - const actual = try seq.encode(&buf); - try testing.expectEqualStrings("\x1B[1;2:3A", actual); + try seq.encode(&writer); + try testing.expectEqualStrings("\x1B[1;2:3A", writer.buffered()); } test "kitty: plain text" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_a, - .mods = .{}, - .utf8 = "abcd", - }, - + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_a, + .mods = .{}, + .utf8 = "abcd", + }, .{ .kitty_flags = .{ .disambiguate = true }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("abcd", actual); + }); + try testing.expectEqualStrings("abcd", writer.buffered()); } test "kitty: repeat with just disambiguate" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_a, - .action = .repeat, - .mods = .{}, - .utf8 = "a", - }, - + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_a, + .action = .repeat, + .mods = .{}, + .utf8 = "a", + }, .{ .kitty_flags = .{ .disambiguate = true }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("a", actual); + }); + try testing.expectEqualStrings("a", writer.buffered()); } - +// test "kitty: enter, backspace, tab" { var buf: [128]u8 = undefined; { - var enc: KeyEncoder = .{ - .event = .{ .key = .enter, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .key = .enter, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\r", actual); + }); + try testing.expectEqualStrings("\r", writer.buffered()); } { - var enc: KeyEncoder = .{ - .event = .{ .key = .backspace, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .key = .backspace, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x7f", actual); + }); + try testing.expectEqualStrings("\x7f", writer.buffered()); } { - var enc: KeyEncoder = .{ - .event = .{ .key = .tab, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .key = .tab, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\t", actual); + }); + try testing.expectEqualStrings("\t", writer.buffered()); } // No release events if "report_all" is not set { - var enc: KeyEncoder = .{ - .event = .{ .action = .release, .key = .enter, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .action = .release, .key = .enter, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("", actual); + }); + try testing.expectEqualStrings("", writer.buffered()); } { - var enc: KeyEncoder = .{ - .event = .{ .action = .release, .key = .backspace, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .action = .release, .key = .backspace, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("", actual); + }); + try testing.expectEqualStrings("", writer.buffered()); } { - var enc: KeyEncoder = .{ - .event = .{ .action = .release, .key = .tab, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .action = .release, .key = .tab, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("", actual); + }); + try testing.expectEqualStrings("", writer.buffered()); } // Release events if "report_all" is set { - var enc: KeyEncoder = .{ - .event = .{ .action = .release, .key = .enter, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .action = .release, .key = .enter, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, .report_all = true, }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[13;1:3u", actual); + }); + try testing.expectEqualStrings("\x1b[13;1:3u", writer.buffered()); } { - var enc: KeyEncoder = .{ - .event = .{ .action = .release, .key = .backspace, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .action = .release, .key = .backspace, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, .report_all = true, }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[127;1:3u", actual); + }); + try testing.expectEqualStrings("\x1b[127;1:3u", writer.buffered()); } { - var enc: KeyEncoder = .{ - .event = .{ .action = .release, .key = .tab, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .action = .release, .key = .tab, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, .report_all = true, }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[9;1:3u", actual); + }); + try testing.expectEqualStrings("\x1b[9;1:3u", writer.buffered()); } } - +// test "kitty: enter with all flags" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ .key = .enter, .mods = .{}, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .key = .enter, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, @@ -1240,15 +1311,15 @@ test "kitty: enter with all flags" { .report_all = true, .report_associated = true, }, - }; - const actual = try enc.kitty(&buf); + }); + const actual = writer.buffered(); try testing.expectEqualStrings("[13u", actual[1..]); } - +// test "kitty: ctrl with all flags" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ .key = .control_left, .mods = .{ .ctrl = true }, .utf8 = "" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .key = .control_left, .mods = .{ .ctrl = true }, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, @@ -1256,20 +1327,20 @@ test "kitty: ctrl with all flags" { .report_all = true, .report_associated = true, }, - }; - const actual = try enc.kitty(&buf); + }); + const actual = writer.buffered(); try testing.expectEqualStrings("[57442;5u", actual[1..]); } test "kitty: ctrl release with ctrl mod set" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .action = .release, - .key = .control_left, - .mods = .{ .ctrl = true }, - .utf8 = "", - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .action = .release, + .key = .control_left, + .mods = .{ .ctrl = true }, + .utf8 = "", + }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, @@ -1277,210 +1348,191 @@ test "kitty: ctrl release with ctrl mod set" { .report_all = true, .report_associated = true, }, - }; - const actual = try enc.kitty(&buf); + }); + const actual = writer.buffered(); try testing.expectEqualStrings("[57442;5:3u", actual[1..]); } test "kitty: delete" { var buf: [128]u8 = undefined; { - var enc: KeyEncoder = .{ - .event = .{ .key = .delete, .mods = .{}, .utf8 = "\x7F" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ .key = .delete, .mods = .{}, .utf8 = "\x7F" }, .{ .kitty_flags = .{ .disambiguate = true }, - }; - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[3~", actual); + }); + try testing.expectEqualStrings("\x1b[3~", writer.buffered()); } } test "kitty: composing with no modifier" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_a, - .mods = .{ .shift = true }, - .composing = true, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_a, + .mods = .{ .shift = true }, + .composing = true, + }, .{ .kitty_flags = .{ .disambiguate = true }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("", actual); + }); + try testing.expectEqualStrings("", writer.buffered()); } test "kitty: composing with modifier" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .shift_left, - .mods = .{ .shift = true }, - .composing = true, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .shift_left, + .mods = .{ .shift = true }, + .composing = true, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[57441;2u", actual); + }); + try testing.expectEqualStrings("\x1b[57441;2u", writer.buffered()); } test "kitty: shift+a on US keyboard" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_a, - .mods = .{ .shift = true }, - .utf8 = "A", - .unshifted_codepoint = 97, // lowercase A - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_a, + .mods = .{ .shift = true }, + .utf8 = "A", + .unshifted_codepoint = 97, // lowercase A + }, .{ .kitty_flags = .{ .disambiguate = true, .report_alternates = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[97:65;2u", actual); + }); + try testing.expectEqualStrings("\x1b[97:65;2u", writer.buffered()); } test "kitty: matching unshifted codepoint" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_a, - .mods = .{ .shift = true }, - .utf8 = "A", - .unshifted_codepoint = 65, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_a, + .mods = .{ .shift = true }, + .utf8 = "A", + .unshifted_codepoint = 65, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_alternates = true, }, - }; - + }); // WARNING: This is not a valid encoding. This is a hypothetical encoding // just to test that our logic is correct around matching unshifted // codepoints. We get an alternate here because the unshifted_codepoint does // not match the base key - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[65::97;2u", actual); + try testing.expectEqualStrings("\x1b[65::97;2u", writer.buffered()); } test "kitty: report alternates with caps" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_j, - .mods = .{ .caps_lock = true }, - .utf8 = "J", - .unshifted_codepoint = 106, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_j, + .mods = .{ .caps_lock = true }, + .utf8 = "J", + .unshifted_codepoint = 106, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[106;65;74u", actual); + }); + try testing.expectEqualStrings("\x1b[106;65;74u", writer.buffered()); } test "kitty: report alternates colon (shift+';')" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .semicolon, - .mods = .{ .shift = true }, - .utf8 = ":", - .unshifted_codepoint = ';', - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .semicolon, + .mods = .{ .shift = true }, + .utf8 = ":", + .unshifted_codepoint = ';', + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[59:58;2;58u", actual); + }); + try testing.expectEqualStrings("\x1b[59:58;2;58u", writer.buffered()); } test "kitty: report alternates with ru layout" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .semicolon, - .mods = .{}, - .utf8 = "ч", - .unshifted_codepoint = 1095, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .semicolon, + .mods = .{}, + .utf8 = "ч", + .unshifted_codepoint = 1095, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[1095::59;;1095u", actual); + }); + try testing.expectEqualStrings("\x1b[1095::59;;1095u", writer.buffered()); } test "kitty: report alternates with ru layout shifted" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .semicolon, - .mods = .{ .shift = true }, - .utf8 = "Ч", - .unshifted_codepoint = 1095, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .semicolon, + .mods = .{ .shift = true }, + .utf8 = "Ч", + .unshifted_codepoint = 1095, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[1095:1063:59;2;1063u", actual); + }); + try testing.expectEqualStrings("\x1b[1095:1063:59;2;1063u", writer.buffered()); } test "kitty: report alternates with ru layout caps lock" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .semicolon, - .mods = .{ .caps_lock = true }, - .utf8 = "Ч", - .unshifted_codepoint = 1095, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .semicolon, + .mods = .{ .caps_lock = true }, + .utf8 = "Ч", + .unshifted_codepoint = 1095, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[1095::59;65;1063u", actual); + }); + try testing.expectEqualStrings("\x1b[1095::59;65;1063u", writer.buffered()); } test "kitty: report alternates with hu layout release" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .action = .release, - .key = .bracket_left, - .mods = .{ .ctrl = true }, - .utf8 = "", - .unshifted_codepoint = 337, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .action = .release, + .key = .bracket_left, + .mods = .{ .ctrl = true }, + .utf8 = "", + .unshifted_codepoint = 337, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, @@ -1488,88 +1540,75 @@ test "kitty: report alternates with hu layout release" { .report_associated = true, .report_events = true, }, - }; - - const actual = try enc.kitty(&buf); + }); + const actual = writer.buffered(); try testing.expectEqualStrings("[337::91;5:3u", actual[1..]); } // macOS generates utf8 text for arrow keys. test "kitty: up arrow with utf8" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .arrow_up, - .mods = .{}, - .utf8 = &.{30}, - }, - + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .arrow_up, + .mods = .{}, + .utf8 = &.{30}, + }, .{ .kitty_flags = .{ .disambiguate = true }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[A", actual); + }); + try testing.expectEqualStrings("\x1b[A", writer.buffered()); } test "kitty: shift+tab" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .tab, - .mods = .{ .shift = true }, - .utf8 = "", // tab - }, - + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .tab, + .mods = .{ .shift = true }, + .utf8 = "", // tab + }, .{ .kitty_flags = .{ .disambiguate = true, .report_alternates = true }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[9;2u", actual); + }); + try testing.expectEqualStrings("\x1b[9;2u", writer.buffered()); } test "kitty: left shift" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .shift_left, - .mods = .{}, - .utf8 = "", - }, - + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .shift_left, + .mods = .{}, + .utf8 = "", + }, .{ .kitty_flags = .{ .disambiguate = true, .report_alternates = true }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("", actual); + }); + try testing.expectEqualStrings("", writer.buffered()); } test "kitty: left shift with report all" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .shift_left, - .mods = .{}, - .utf8 = "", - }, - + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .shift_left, + .mods = .{}, + .utf8 = "", + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[57441u", actual); + }); + try testing.expectEqualStrings("\x1b[57441u", writer.buffered()); } test "kitty: report associated with alt text on macOS with option" { if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest; var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_w, - .mods = .{ .alt = true }, - .utf8 = "∑", - .unshifted_codepoint = 119, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_w, + .mods = .{ .alt = true }, + .utf8 = "∑", + .unshifted_codepoint = 119, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, @@ -1577,10 +1616,8 @@ test "kitty: report associated with alt text on macOS with option" { .report_associated = true, }, .macos_option_as_alt = .false, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[119;3;8721u", actual); + }); + try testing.expectEqualStrings("\x1b[119;3;8721u", writer.buffered()); } test "kitty: report associated with alt text on macOS with alt" { @@ -1589,13 +1626,13 @@ test "kitty: report associated with alt text on macOS with alt" { { // With Alt modifier var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_w, - .mods = .{ .alt = true }, - .utf8 = "∑", - .unshifted_codepoint = 119, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_w, + .mods = .{ .alt = true }, + .utf8 = "∑", + .unshifted_codepoint = 119, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, @@ -1603,22 +1640,20 @@ test "kitty: report associated with alt text on macOS with alt" { .report_associated = true, }, .macos_option_as_alt = .true, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[119;3u", actual); + }); + try testing.expectEqualStrings("\x1b[119;3u", writer.buffered()); } { // Without Alt modifier var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_w, - .mods = .{}, - .utf8 = "∑", - .unshifted_codepoint = 119, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_w, + .mods = .{}, + .utf8 = "∑", + .unshifted_codepoint = 119, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, @@ -1626,65 +1661,59 @@ test "kitty: report associated with alt text on macOS with alt" { .report_associated = true, }, .macos_option_as_alt = .true, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[119;;8721u", actual); + }); + try testing.expectEqualStrings("\x1b[119;;8721u", writer.buffered()); } } test "kitty: report associated with modifiers" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_j, - .mods = .{ .ctrl = true }, - .utf8 = "j", - .unshifted_codepoint = 106, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_j, + .mods = .{ .ctrl = true }, + .utf8 = "j", + .unshifted_codepoint = 106, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[106;5u", actual); + }); + try testing.expectEqualStrings("\x1b[106;5u", writer.buffered()); } test "kitty: report associated" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_j, - .mods = .{ .shift = true }, - .utf8 = "J", - .unshifted_codepoint = 106, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .key_j, + .mods = .{ .shift = true }, + .utf8 = "J", + .unshifted_codepoint = 106, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[106:74;2;74u", actual); + }); + try testing.expectEqualStrings("\x1b[106:74;2;74u", writer.buffered()); } test "kitty: report associated on release" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .action = .release, - .key = .key_j, - .mods = .{ .shift = true }, - .utf8 = "J", - .unshifted_codepoint = 106, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .action = .release, + .key = .key_j, + .mods = .{ .shift = true }, + .utf8 = "J", + .unshifted_codepoint = 106, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, @@ -1692,54 +1721,53 @@ test "kitty: report associated on release" { .report_associated = true, .report_events = true, }, - }; - - const actual = try enc.kitty(&buf); + }); + const actual = writer.buffered(); try testing.expectEqualStrings("[106:74;2:3u", actual[1..]); } test "kitty: alternates omit control characters" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .delete, - .mods = .{}, - .utf8 = &.{0x7F}, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .delete, + .mods = .{}, + .utf8 = &.{0x7F}, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_alternates = true, .report_all = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("\x1b[3~", actual); + }); + try testing.expectEqualStrings("\x1b[3~", writer.buffered()); } test "kitty: enter with utf8 (dead key state)" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .enter, - .utf8 = "A", - .unshifted_codepoint = 0x0D, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .enter, + .utf8 = "A", + .unshifted_codepoint = 0x0D, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_alternates = true, .report_all = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("A", actual); + }); + try testing.expectEqualStrings("A", writer.buffered()); } test "kitty: keypad number" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ .key = .numpad_1, .mods = .{}, .utf8 = "1" }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .numpad_1, + .mods = .{}, + .utf8 = "1", + }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, @@ -1747,19 +1775,19 @@ test "kitty: keypad number" { .report_all = true, .report_associated = true, }, - }; - const actual = try enc.kitty(&buf); + }); + const actual = writer.buffered(); try testing.expectEqualStrings("[57400;;49u", actual[1..]); } test "kitty: backspace with utf8 (dead key state)" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .backspace, - .utf8 = "A", - .unshifted_codepoint = 0x0D, - }, + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .backspace, + .utf8 = "A", + .unshifted_codepoint = 0x0D, + }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, @@ -1767,261 +1795,237 @@ test "kitty: backspace with utf8 (dead key state)" { .report_all = true, .report_associated = true, }, - }; - - const actual = try enc.kitty(&buf); - try testing.expectEqualStrings("", actual); + }); + try testing.expectEqualStrings("", writer.buffered()); } test "legacy: backspace with utf8 (dead key state)" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .backspace, - .utf8 = "A", - .unshifted_codepoint = 0x0D, - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .backspace, + .utf8 = "A", + .unshifted_codepoint = 0x0D, + }, .{}); + try testing.expectEqualStrings("", writer.buffered()); } test "legacy: enter with utf8 (dead key state)" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .enter, - .utf8 = "A", - .unshifted_codepoint = 0x0D, - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("A", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .enter, + .utf8 = "A", + .unshifted_codepoint = 0x0D, + }, .{}); + try testing.expectEqualStrings("A", writer.buffered()); } test "legacy: esc with utf8 (dead key state)" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .escape, - .utf8 = "A", - .unshifted_codepoint = 0x0D, - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("A", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .escape, + .utf8 = "A", + .unshifted_codepoint = 0x0D, + }, .{}); + try testing.expectEqualStrings("A", writer.buffered()); } test "legacy: ctrl+shift+minus (underscore on US)" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .minus, - .mods = .{ .ctrl = true, .shift = true }, - .utf8 = "_", - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1F", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .minus, + .mods = .{ .ctrl = true, .shift = true }, + .utf8 = "_", + }, .{}); + try testing.expectEqualStrings("\x1F", writer.buffered()); } test "legacy: ctrl+alt+c" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_c, - .mods = .{ .ctrl = true, .alt = true }, - .utf8 = "c", - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b\x03", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_c, + .mods = .{ .ctrl = true, .alt = true }, + .utf8 = "c", + }, .{}); + try testing.expectEqualStrings("\x1b\x03", writer.buffered()); } test "legacy: alt+c" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_c, - .utf8 = "c", - .mods = .{ .alt = true }, - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_c, + .utf8 = "c", + .mods = .{ .alt = true }, + }, .{ .alt_esc_prefix = true, .macos_option_as_alt = .true, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1Bc", actual); + }); + try testing.expectEqualStrings("\x1Bc", writer.buffered()); } test "legacy: alt+e only unshifted" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_e, - .unshifted_codepoint = 'e', - .mods = .{ .alt = true }, - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_e, + .unshifted_codepoint = 'e', + .mods = .{ .alt = true }, + }, .{ .alt_esc_prefix = true, .macos_option_as_alt = .true, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1Be", actual); + }); + try testing.expectEqualStrings("\x1Be", writer.buffered()); } test "legacy: alt+x macos" { if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest; var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_c, - .utf8 = "≈", - .unshifted_codepoint = 'c', - .mods = .{ .alt = true }, - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_c, + .utf8 = "≈", + .unshifted_codepoint = 'c', + .mods = .{ .alt = true }, + }, .{ .alt_esc_prefix = true, .macos_option_as_alt = .true, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1Bc", actual); + }); + try testing.expectEqualStrings("\x1Bc", writer.buffered()); } test "legacy: shift+alt+. macos" { if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest; var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .period, - .utf8 = ">", - .unshifted_codepoint = '.', - .mods = .{ .alt = true, .shift = true }, - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .period, + .utf8 = ">", + .unshifted_codepoint = '.', + .mods = .{ .alt = true, .shift = true }, + }, .{ .alt_esc_prefix = true, .macos_option_as_alt = .true, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1B>", actual); + }); + try testing.expectEqualStrings("\x1B>", writer.buffered()); } test "legacy: alt+ф" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_f, - .utf8 = "ф", - .mods = .{ .alt = true }, - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_f, + .utf8 = "ф", + .mods = .{ .alt = true }, + }, .{ .alt_esc_prefix = true, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("ф", actual); + }); + try testing.expectEqualStrings("ф", writer.buffered()); } test "legacy: ctrl+c" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_c, - .mods = .{ .ctrl = true }, - .utf8 = "c", - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x03", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_c, + .mods = .{ .ctrl = true }, + .utf8 = "c", + }, .{}); + try testing.expectEqualStrings("\x03", writer.buffered()); } test "legacy: ctrl+space" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .space, - .mods = .{ .ctrl = true }, - .utf8 = " ", - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x00", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .space, + .mods = .{ .ctrl = true }, + .utf8 = " ", + }, .{}); + try testing.expectEqualStrings("\x00", writer.buffered()); } test "legacy: ctrl+shift+backspace" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .backspace, - .mods = .{ .ctrl = true, .shift = true }, - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x08", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .backspace, + .mods = .{ .ctrl = true, .shift = true }, + }, .{}); + try testing.expectEqualStrings("\x08", writer.buffered()); } test "legacy: ctrl+shift+char with modify other state 2" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_h, - .mods = .{ .ctrl = true, .shift = true }, - .utf8 = "H", - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_h, + .mods = .{ .ctrl = true, .shift = true }, + .utf8 = "H", + }, .{ .modify_other_keys_state_2 = true, - }; + }); + try testing.expectEqualStrings("\x1b[27;6;72~", writer.buffered()); +} - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[27;6;72~", actual); +test "legacy: ctrl+shift+char with modify other state 2 and consumed mods" { + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_h, + .mods = .{ .ctrl = true, .shift = true }, + .consumed_mods = .{ .shift = true }, + .utf8 = "H", + }, .{ + .modify_other_keys_state_2 = true, + }); + try testing.expectEqualStrings("\x1b[27;6;72~", writer.buffered()); } test "legacy: fixterm awkward letters" { var buf: [128]u8 = undefined; { - var enc: KeyEncoder = .{ .event = .{ + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ .key = .key_i, .mods = .{ .ctrl = true }, .utf8 = "i", - } }; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[105;5u", actual); + }, .{}); + try testing.expectEqualStrings("\x1b[105;5u", writer.buffered()); } { - var enc: KeyEncoder = .{ .event = .{ + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ .key = .key_m, .mods = .{ .ctrl = true }, .utf8 = "m", - } }; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[109;5u", actual); + }, .{}); + try testing.expectEqualStrings("\x1b[109;5u", writer.buffered()); } { - var enc: KeyEncoder = .{ .event = .{ + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ .key = .bracket_left, .mods = .{ .ctrl = true }, .utf8 = "[", - } }; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[91;5u", actual); + }, .{}); + try testing.expectEqualStrings("\x1b[91;5u", writer.buffered()); } { - var enc: KeyEncoder = .{ .event = .{ + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ .key = .digit_2, .mods = .{ .ctrl = true, .shift = true }, .utf8 = "@", .unshifted_codepoint = '2', - } }; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[64;5u", actual); + }, .{}); + try testing.expectEqualStrings("\x1b[64;5u", writer.buffered()); } } @@ -2030,199 +2034,189 @@ test "legacy: fixterm awkward letters" { test "legacy: ctrl+shift+letter ascii" { var buf: [128]u8 = undefined; { - var enc: KeyEncoder = .{ .event = .{ + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ .key = .key_m, .mods = .{ .ctrl = true, .shift = true }, .utf8 = "M", .unshifted_codepoint = 'm', - } }; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[109;6u", actual); + }, .{}); + try testing.expectEqualStrings("\x1b[109;6u", writer.buffered()); } } test "legacy: shift+function key should use all mods" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .arrow_up, - .mods = .{ .shift = true }, - .consumed_mods = .{ .shift = true }, - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[1;2A", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .arrow_up, + .mods = .{ .shift = true }, + .consumed_mods = .{ .shift = true }, + }, .{}); + try testing.expectEqualStrings("\x1b[1;2A", writer.buffered()); } test "legacy: keypad enter" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .numpad_enter, - .mods = .{}, - .consumed_mods = .{}, - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\r", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .numpad_enter, + .mods = .{}, + .consumed_mods = .{}, + }, .{}); + try testing.expectEqualStrings("\r", writer.buffered()); } test "legacy: keypad 1" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .numpad_1, - .mods = .{}, - .consumed_mods = .{}, - .utf8 = "1", - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("1", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .numpad_1, + .mods = .{}, + .consumed_mods = .{}, + .utf8 = "1", + }, .{}); + try testing.expectEqualStrings("1", writer.buffered()); } test "legacy: keypad 1 with application keypad" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .numpad_1, - .mods = .{}, - .consumed_mods = .{}, - .utf8 = "1", - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .numpad_1, + .mods = .{}, + .consumed_mods = .{}, + .utf8 = "1", + }, .{ .keypad_key_application = true, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1bOq", actual); + }); + try testing.expectEqualStrings("\x1bOq", writer.buffered()); } test "legacy: keypad 1 with application keypad and numlock" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .numpad_1, - .mods = .{ .num_lock = true }, - .consumed_mods = .{}, - .utf8 = "1", - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .numpad_1, + .mods = .{ .num_lock = true }, + .consumed_mods = .{}, + .utf8 = "1", + }, .{ .keypad_key_application = true, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1bOq", actual); + }); + try testing.expectEqualStrings("\x1bOq", writer.buffered()); } test "legacy: keypad 1 with application keypad and numlock ignore" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .numpad_1, - .mods = .{ .num_lock = false }, - .consumed_mods = .{}, - .utf8 = "1", - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .numpad_1, + .mods = .{ .num_lock = false }, + .consumed_mods = .{}, + .utf8 = "1", + }, .{ .keypad_key_application = true, .ignore_keypad_with_numlock = true, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("1", actual); + }); + try testing.expectEqualStrings("1", writer.buffered()); } test "legacy: f1" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .f1, - .mods = .{ .ctrl = true }, - .consumed_mods = .{}, - }, - }; // F1 { - enc.event.key = .f1; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[1;5P", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .f1, + .mods = .{ .ctrl = true }, + .consumed_mods = .{}, + }, .{}); + try testing.expectEqualStrings("\x1b[1;5P", writer.buffered()); } // F2 { - enc.event.key = .f2; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[1;5Q", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .f2, + .mods = .{ .ctrl = true }, + .consumed_mods = .{}, + }, .{}); + try testing.expectEqualStrings("\x1b[1;5Q", writer.buffered()); } // F3 { - enc.event.key = .f3; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[13;5~", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .f3, + .mods = .{ .ctrl = true }, + .consumed_mods = .{}, + }, .{}); + try testing.expectEqualStrings("\x1b[13;5~", writer.buffered()); } // F4 { - enc.event.key = .f4; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[1;5S", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .f4, + .mods = .{ .ctrl = true }, + .consumed_mods = .{}, + }, .{}); + try testing.expectEqualStrings("\x1b[1;5S", writer.buffered()); } // F5 uses new encoding { - enc.event.key = .f5; - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[15;5~", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .f5, + .mods = .{ .ctrl = true }, + .consumed_mods = .{}, + }, .{}); + try testing.expectEqualStrings("\x1b[15;5~", writer.buffered()); } } test "legacy: left_shift+tab" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .tab, - .mods = .{ - .shift = true, - .sides = .{ .shift = .left }, - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .tab, + .mods = .{ + .shift = true, + .sides = .{ .shift = .left }, }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[Z", actual); + }, .{}); + try testing.expectEqualStrings("\x1b[Z", writer.buffered()); } test "legacy: right_shift+tab" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .tab, - .mods = .{ - .shift = true, - .sides = .{ .shift = .right }, - }, + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .tab, + .mods = .{ + .shift = true, + .sides = .{ .shift = .right }, }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x1b[Z", actual); + }, .{}); + try testing.expectEqualStrings("\x1b[Z", writer.buffered()); } test "legacy: hu layout ctrl+ő sends proper codepoint" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .bracket_left, - .mods = .{ .ctrl = true }, - .utf8 = "ő", - .unshifted_codepoint = 337, - }, - }; - - const actual = try enc.legacy(&buf); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .bracket_left, + .mods = .{ .ctrl = true }, + .utf8 = "ő", + .unshifted_codepoint = 337, + }, .{}); + const actual = writer.buffered(); try testing.expectEqualStrings("[337;5u", actual[1..]); } @@ -2230,46 +2224,37 @@ test "legacy: super-only on macOS with text" { if (comptime builtin.os.tag != .macos) return error.SkipZigTest; var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_b, - .utf8 = "b", - .mods = .{ .super = true }, - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_b, + .utf8 = "b", + .mods = .{ .super = true }, + }, .{}); + try testing.expectEqualStrings("", writer.buffered()); } test "legacy: super and other mods on macOS with text" { if (comptime builtin.os.tag != .macos) return error.SkipZigTest; var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .key_b, - .utf8 = "B", - .mods = .{ .super = true, .shift = true }, - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_b, + .utf8 = "B", + .mods = .{ .super = true, .shift = true }, + }, .{}); + try testing.expectEqualStrings("", writer.buffered()); } test "legacy: backspace with DEL utf8" { var buf: [128]u8 = undefined; - var enc: KeyEncoder = .{ - .event = .{ - .key = .backspace, - .utf8 = &.{0x7F}, - .unshifted_codepoint = 0x08, - }, - }; - - const actual = try enc.legacy(&buf); - try testing.expectEqualStrings("\x7F", actual); + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .backspace, + .utf8 = &.{0x7F}, + .unshifted_codepoint = 0x08, + }, .{}); + try testing.expectEqualStrings("\x7F", writer.buffered()); } test "ctrlseq: normal ctrl c" { diff --git a/src/input/keyboard.zig b/src/input/keyboard.zig index 73674df2c..d2882a23a 100644 --- a/src/input/keyboard.zig +++ b/src/input/keyboard.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const OptionAsAlt = @import("../config.zig").OptionAsAlt; +const OptionAsAlt = @import("config.zig").OptionAsAlt; /// Keyboard layouts. /// diff --git a/src/input/paste.zig b/src/input/paste.zig new file mode 100644 index 000000000..29787c385 --- /dev/null +++ b/src/input/paste.zig @@ -0,0 +1,146 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Terminal = @import("../terminal/Terminal.zig"); + +pub const Options = struct { + /// True if bracketed paste mode is on. + bracketed: bool, + + /// Return the encoding options based on the current terminal state. + pub fn fromTerminal(t: *const Terminal) Options { + return .{ + .bracketed = t.modes.get(.bracketed_paste), + }; + } +}; + +/// Encode the given data for pasting. The resulting value can be written +/// to the pty to perform a paste of the input data. +/// +/// The data can be either a `[]u8` or a `[]const u8`. If the data +/// type is const then `EncodeError` may be returned. If the data type +/// is mutable then this function can't return an error. +/// +/// This slightly complex calling style allows for initially const +/// data to be passed in without an allocation, since it is rare in normal +/// use cases that the data will need to be modified. In the unlikely case +/// data does need to be modified, the caller can make a mutable copy +/// after seeing the error. +/// +/// The data is returned as a set of slices to limit allocations. The caller +/// can combine the slices into a single buffer if desired. +/// +/// WARNING: The input data is not checked for safety. See the `isSafe` +/// function to check if the data is safe to paste. +pub fn encode( + data: anytype, + opts: Options, +) switch (@TypeOf(data)) { + []u8 => [3][]const u8, + []const u8 => Error![3][]const u8, + else => unreachable, +} { + const mutable = @TypeOf(data) == []u8; + + var result: [3][]const u8 = .{ "", data, "" }; + + // Bracketed paste mode (mode 2004) wraps pasted data in + // fenceposts so that the terminal can ignore things like newlines. + if (opts.bracketed) { + result[0] = "\x1b[200~"; + result[2] = "\x1b[201~"; + return result; + } + + // Non-bracketed. We have to replace newline with `\r`. This matches + // the behavior of xterm and other terminals. For `\r\n` this will + // result in `\r\r` which does match xterm. + if (comptime mutable) { + std.mem.replaceScalar(u8, data, '\n', '\r'); + } else if (std.mem.indexOfScalar(u8, data, '\n') != null) { + return Error.MutableRequired; + } + + return result; +} + +pub const Error = error{ + /// Returned if encoding requires a mutable copy of the data. This + /// can only be returned if the input data type is const. + MutableRequired, +}; + +/// Returns true if the data looks safe to paste. Data is considered +/// unsafe if it contains any of the following: +/// +/// - `\n`: Newlines can be used to inject commands. +/// - `\x1b[201~`: This is the end of a bracketed paste. This cane be used +/// to exit a bracketed paste and inject commands. +/// +/// We consider any scenario unsafe regardless of current terminal state. +/// For example, even if bracketed paste mode is not active, we still +/// consider `\x1b[201~` unsafe. The existence of these types of bytes +/// should raise suspicion that the producer of the paste data is +/// acting strangely. +pub fn isSafe(data: []const u8) bool { + return std.mem.indexOf(u8, data, "\n") == null and + std.mem.indexOf(u8, data, "\x1b[201~") == null; +} + +test isSafe { + const testing = std.testing; + try testing.expect(isSafe("hello")); + try testing.expect(!isSafe("hello\n")); + try testing.expect(!isSafe("hello\nworld")); + try testing.expect(!isSafe("he\x1b[201~llo")); +} + +test "encode bracketed" { + const testing = std.testing; + const result = try encode( + @as([]const u8, "hello"), + .{ .bracketed = true }, + ); + try testing.expectEqualStrings("\x1b[200~", result[0]); + try testing.expectEqualStrings("hello", result[1]); + try testing.expectEqualStrings("\x1b[201~", result[2]); +} + +test "encode unbracketed no newlines" { + const testing = std.testing; + const result = try encode( + @as([]const u8, "hello"), + .{ .bracketed = false }, + ); + try testing.expectEqualStrings("", result[0]); + try testing.expectEqualStrings("hello", result[1]); + try testing.expectEqualStrings("", result[2]); +} + +test "encode unbracketed newlines const" { + const testing = std.testing; + try testing.expectError(Error.MutableRequired, encode( + @as([]const u8, "hello\nworld"), + .{ .bracketed = false }, + )); +} + +test "encode unbracketed newlines" { + const testing = std.testing; + const data: []u8 = try testing.allocator.dupe(u8, "hello\nworld"); + defer testing.allocator.free(data); + const result = encode(data, .{ .bracketed = false }); + try testing.expectEqualStrings("", result[0]); + try testing.expectEqualStrings("hello\rworld", result[1]); + try testing.expectEqualStrings("", result[2]); +} + +test "encode unbracketed windows-stye newline" { + const testing = std.testing; + const data: []u8 = try testing.allocator.dupe(u8, "hello\r\nworld"); + defer testing.allocator.free(data); + const result = encode(data, .{ .bracketed = false }); + try testing.expectEqualStrings("", result[0]); + try testing.expectEqualStrings("hello\r\rworld", result[1]); + try testing.expectEqualStrings("", result[2]); +} diff --git a/src/inspector/Inspector.zig b/src/inspector/Inspector.zig index 27abb8657..49b05bd7f 100644 --- a/src/inspector/Inspector.zig +++ b/src/inspector/Inspector.zig @@ -149,7 +149,7 @@ pub fn setup() void { font_config.FontDataOwnedByAtlas = false; _ = cimgui.c.ImFontAtlas_AddFontFromMemoryTTF( io.Fonts, - @constCast(@ptrCast(font.embedded.regular)), + @ptrCast(@constCast(font.embedded.regular)), font.embedded.regular.len, font_size, font_config, @@ -600,6 +600,7 @@ fn renderModesWindow(self: *Inspector) void { const t = self.surface.renderer_state.terminal; inline for (@typeInfo(terminal.Mode).@"enum".fields) |field| { + @setEvalBranchQuota(6000); const tag: terminal.modes.ModeTag = @bitCast(@as(terminal.modes.ModeTag.Backing, field.value)); cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); diff --git a/src/inspector/termio.zig b/src/inspector/termio.zig index 5ab9d3cd4..212f0ea4a 100644 --- a/src/inspector/termio.zig +++ b/src/inspector/termio.zig @@ -43,9 +43,9 @@ pub const VTEvent = struct { ) !VTEvent { var md = Metadata.init(alloc); errdefer md.deinit(); - var buf = std.ArrayList(u8).init(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); defer buf.deinit(); - try encodeAction(alloc, buf.writer(), &md, action); + try encodeAction(alloc, &buf.writer, &md, action); const str = try buf.toOwnedSliceSentinel(0); errdefer alloc.free(str); @@ -115,7 +115,7 @@ pub const VTEvent = struct { /// Encode a parser action as a string that we show in the logs. fn encodeAction( alloc: Allocator, - writer: anytype, + writer: *std.Io.Writer, md: *Metadata, action: terminal.Parser.Action, ) !void { @@ -125,16 +125,16 @@ pub const VTEvent = struct { .csi_dispatch => |v| try encodeCSI(writer, v), .esc_dispatch => |v| try encodeEsc(writer, v), .osc_dispatch => |v| try encodeOSC(alloc, writer, md, v), - else => try writer.print("{}", .{action}), + else => try writer.print("{f}", .{action}), } } - fn encodePrint(writer: anytype, action: terminal.Parser.Action) !void { + fn encodePrint(writer: *std.Io.Writer, action: terminal.Parser.Action) !void { const ch = action.print; try writer.print("'{u}' (U+{X})", .{ ch, ch }); } - fn encodeExecute(writer: anytype, action: terminal.Parser.Action) !void { + fn encodeExecute(writer: *std.Io.Writer, action: terminal.Parser.Action) !void { const ch = action.execute; switch (ch) { 0x00 => try writer.writeAll("NUL"), @@ -158,7 +158,7 @@ pub const VTEvent = struct { try writer.print(" (0x{X})", .{ch}); } - fn encodeCSI(writer: anytype, csi: terminal.Parser.Action.CSI) !void { + fn encodeCSI(writer: *std.Io.Writer, csi: terminal.Parser.Action.CSI) !void { for (csi.intermediates) |v| try writer.print("{c} ", .{v}); for (csi.params, 0..) |v, i| { if (i != 0) try writer.writeByte(';'); @@ -168,14 +168,14 @@ pub const VTEvent = struct { try writer.writeByte(csi.final); } - fn encodeEsc(writer: anytype, esc: terminal.Parser.Action.ESC) !void { + fn encodeEsc(writer: *std.Io.Writer, esc: terminal.Parser.Action.ESC) !void { for (esc.intermediates) |v| try writer.print("{c} ", .{v}); try writer.writeByte(esc.final); } fn encodeOSC( alloc: Allocator, - writer: anytype, + writer: *std.Io.Writer, md: *Metadata, osc: terminal.osc.Command, ) !void { @@ -197,7 +197,9 @@ pub const VTEvent = struct { ) !void { switch (@TypeOf(v)) { void => {}, - []const u8 => try md.put("data", try alloc.dupeZ(u8, v)), + []const u8, + [:0]const u8, + => try md.put("data", try alloc.dupeZ(u8, v)), else => |T| switch (@typeInfo(T)) { .@"struct" => |info| inline for (info.fields) |field| { try encodeMetadataSingle( @@ -262,11 +264,16 @@ pub const VTEvent = struct { if (std.mem.eql(u8, field.name, tag_name)) { const s = if (field.type == void) try alloc.dupeZ(u8, tag_name) - else - try std.fmt.allocPrintZ(alloc, "{s}={}", .{ + else if (field.type == [:0]const u8 or field.type == []const u8) + try std.fmt.allocPrintSentinel(alloc, "{s}={s}", .{ tag_name, @field(value, field.name), - }); + }, 0) + else + try std.fmt.allocPrintSentinel(alloc, "{s}={}", .{ + tag_name, + @field(value, field.name), + }, 0); try md.put(key, s); } @@ -281,10 +288,12 @@ pub const VTEvent = struct { else => switch (Value) { u8, u16 => try md.put( key, - try std.fmt.allocPrintZ(alloc, "{}", .{value}), + try std.fmt.allocPrintSentinel(alloc, "{}", .{value}, 0), ), - []const u8 => try md.put(key, try alloc.dupeZ(u8, value)), + []const u8, + [:0]const u8, + => try md.put(key, try alloc.dupeZ(u8, value)), else => |T| { @compileLog(T); diff --git a/src/lib/allocator.zig b/src/lib/allocator.zig index bcd7f9dcc..ccea7ae29 100644 --- a/src/lib/allocator.zig +++ b/src/lib/allocator.zig @@ -20,11 +20,17 @@ pub fn default(c_alloc_: ?*const Allocator) std.mem.Allocator { // If we're given an allocator, use it. if (c_alloc_) |c_alloc| return c_alloc.zig(); + // Tests always use the test allocator so we can detect leaks. + if (comptime builtin.is_test) return testing.allocator; + // If we have libc, use that. We prefer libc if we have it because // its generally fast but also lets the embedder easily override // malloc/free with custom allocators like mimalloc or something. if (comptime builtin.link_libc) return std.heap.c_allocator; + // Wasm + if (comptime builtin.target.cpu.arch.isWasm()) return std.heap.wasm_allocator; + // No libc, use the preferred allocator for releases which is the // Zig SMP allocator. return std.heap.smp_allocator; diff --git a/src/lib/enum.zig b/src/lib/enum.zig new file mode 100644 index 000000000..c3971ebde --- /dev/null +++ b/src/lib/enum.zig @@ -0,0 +1,97 @@ +const std = @import("std"); + +/// Create an enum type with the given keys that is C ABI compatible +/// if we're targeting C, otherwise a Zig enum with smallest possible +/// backing type. +/// +/// In all cases, the enum keys will be created in the order given. +/// For C ABI, this means that the order MUST NOT be changed in order +/// to preserve ABI compatibility. You can set a key to null to +/// remove it from the Zig enum while keeping the "hole" in the C enum +/// to preserve ABI compatibility. +/// +/// C detection is up to the caller, since there are multiple ways +/// to do that. We rely on the `target` parameter to determine whether we +/// should create a C compatible enum or a Zig enum. +/// +/// For the Zig enum, the enum value is not guaranteed to be stable, so +/// it shouldn't be relied for things like serialization. +pub fn Enum( + target: Target, + keys: []const ?[:0]const u8, +) type { + var fields: [keys.len]std.builtin.Type.EnumField = undefined; + var fields_i: usize = 0; + var holes: usize = 0; + for (keys) |key_| { + const key: [:0]const u8 = key_ orelse { + switch (target) { + // For Zig we don't track holes because the enum value + // isn't guaranteed to be stable and we want to use the + // smallest possible backing type. + .zig => {}, + + // For C we must track holes to preserve ABI compatibility + // with subsequent values. + .c => holes += 1, + } + continue; + }; + + fields[fields_i] = .{ + .name = key, + .value = fields_i + holes, + }; + fields_i += 1; + } + + // Assigned to var so that the type name is nicer in stack traces. + const Result = @Type(.{ .@"enum" = .{ + .tag_type = switch (target) { + .c => c_int, + .zig => std.math.IntFittingRange(0, fields_i - 1), + }, + .fields = fields[0..fields_i], + .decls = &.{}, + .is_exhaustive = true, + } }); + return Result; +} + +pub const Target = union(enum) { + c, + zig, +}; + +test "zig" { + const testing = std.testing; + const T = Enum(.zig, &.{ "a", "b", "c", "d" }); + const info = @typeInfo(T).@"enum"; + try testing.expectEqual(u2, info.tag_type); +} + +test "c" { + const testing = std.testing; + const T = Enum(.c, &.{ "a", "b", "c", "d" }); + const info = @typeInfo(T).@"enum"; + try testing.expectEqual(c_int, info.tag_type); +} + +test "abi by removing a key" { + const testing = std.testing; + // C + { + const T = Enum(.c, &.{ "a", "b", null, "d" }); + const info = @typeInfo(T).@"enum"; + try testing.expectEqual(c_int, info.tag_type); + try testing.expectEqual(3, @intFromEnum(T.d)); + } + + // Zig + { + const T = Enum(.zig, &.{ "a", "b", null, "d" }); + const info = @typeInfo(T).@"enum"; + try testing.expectEqual(u2, info.tag_type); + try testing.expectEqual(2, @intFromEnum(T.d)); + } +} diff --git a/src/lib/main.zig b/src/lib/main.zig new file mode 100644 index 000000000..4ef8dcb2d --- /dev/null +++ b/src/lib/main.zig @@ -0,0 +1,10 @@ +const std = @import("std"); +const enumpkg = @import("enum.zig"); + +pub const allocator = @import("allocator.zig"); +pub const Enum = enumpkg.Enum; +pub const EnumTarget = enumpkg.Target; + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 656509cce..322f391ab 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -7,6 +7,10 @@ //! by thousands of users for years. However, the API itself (functions, //! types, etc.) may change without warning. We're working on stabilizing //! this in the future. +const lib = @This(); + +const std = @import("std"); +const builtin = @import("builtin"); // The public API below reproduces a lot of terminal/main.zig but // is separate because (1) we need our root file to be in `src/` @@ -30,8 +34,8 @@ pub const size = terminal.size; pub const x11_color = terminal.x11_color; pub const Charset = terminal.Charset; -pub const CharsetSlot = terminal.Slots; -pub const CharsetActiveSlot = terminal.ActiveSlot; +pub const CharsetSlot = terminal.CharsetSlot; +pub const CharsetActiveSlot = terminal.CharsetActiveSlot; pub const Cell = page.Cell; pub const Coordinate = point.Coordinate; pub const CSI = Parser.Action.CSI; @@ -65,19 +69,91 @@ pub const EraseLine = terminal.EraseLine; pub const TabClear = terminal.TabClear; pub const Attribute = terminal.Attribute; +/// Terminal-specific input encoding is also part of libghostty-vt. +pub const input = struct { + // We have to be careful to only import targeted files within + // the input package because the full package brings in too many + // other dependencies. + const paste = @import("input/paste.zig"); + const key = @import("input/key.zig"); + const key_encode = @import("input/key_encode.zig"); + + // Paste-related APIs + pub const PasteError = paste.Error; + pub const PasteOptions = paste.Options; + pub const isSafePaste = paste.isSafe; + pub const encodePaste = paste.encode; + + // Key encoding + pub const Key = key.Key; + pub const KeyAction = key.Action; + pub const KeyEvent = key.KeyEvent; + pub const KeyMods = key.Mods; + pub const KeyEncodeOptions = key_encode.Options; + pub const encodeKey = key_encode.encode; +}; + comptime { // If we're building the C library (vs. the Zig module) then // we want to reference the C API so that it gets exported. - if (terminal.is_c_lib) { + if (@import("root") == lib) { const c = terminal.c_api; @export(&c.osc_new, .{ .name = "ghostty_osc_new" }); @export(&c.osc_free, .{ .name = "ghostty_osc_free" }); + @export(&c.osc_next, .{ .name = "ghostty_osc_next" }); + @export(&c.osc_reset, .{ .name = "ghostty_osc_reset" }); + @export(&c.osc_end, .{ .name = "ghostty_osc_end" }); + @export(&c.osc_command_type, .{ .name = "ghostty_osc_command_type" }); + @export(&c.osc_command_data, .{ .name = "ghostty_osc_command_data" }); + @export(&c.key_event_new, .{ .name = "ghostty_key_event_new" }); + @export(&c.key_event_free, .{ .name = "ghostty_key_event_free" }); + @export(&c.key_event_set_action, .{ .name = "ghostty_key_event_set_action" }); + @export(&c.key_event_get_action, .{ .name = "ghostty_key_event_get_action" }); + @export(&c.key_event_set_key, .{ .name = "ghostty_key_event_set_key" }); + @export(&c.key_event_get_key, .{ .name = "ghostty_key_event_get_key" }); + @export(&c.key_event_set_mods, .{ .name = "ghostty_key_event_set_mods" }); + @export(&c.key_event_get_mods, .{ .name = "ghostty_key_event_get_mods" }); + @export(&c.key_event_set_consumed_mods, .{ .name = "ghostty_key_event_set_consumed_mods" }); + @export(&c.key_event_get_consumed_mods, .{ .name = "ghostty_key_event_get_consumed_mods" }); + @export(&c.key_event_set_composing, .{ .name = "ghostty_key_event_set_composing" }); + @export(&c.key_event_get_composing, .{ .name = "ghostty_key_event_get_composing" }); + @export(&c.key_event_set_utf8, .{ .name = "ghostty_key_event_set_utf8" }); + @export(&c.key_event_get_utf8, .{ .name = "ghostty_key_event_get_utf8" }); + @export(&c.key_event_set_unshifted_codepoint, .{ .name = "ghostty_key_event_set_unshifted_codepoint" }); + @export(&c.key_event_get_unshifted_codepoint, .{ .name = "ghostty_key_event_get_unshifted_codepoint" }); + @export(&c.key_encoder_new, .{ .name = "ghostty_key_encoder_new" }); + @export(&c.key_encoder_free, .{ .name = "ghostty_key_encoder_free" }); + @export(&c.key_encoder_setopt, .{ .name = "ghostty_key_encoder_setopt" }); + @export(&c.key_encoder_encode, .{ .name = "ghostty_key_encoder_encode" }); + @export(&c.paste_is_safe, .{ .name = "ghostty_paste_is_safe" }); } } +pub const std_options: std.Options = options: { + if (builtin.target.cpu.arch.isWasm()) break :options .{ + // Wasm builds we specifically want to optimize for space with small + // releases so we bump up to warn. Everything else acts pretty normal. + .log_level = switch (builtin.mode) { + .Debug => .debug, + .ReleaseSmall => .warn, + else => .info, + }, + + // Wasm doesn't have access to stdio so we have a custom log function. + .logFn = @import("os/wasm/log.zig").log, + }; + + // For everything else we currently use defaults. Longer term I'm + // SURE this isn't right (e.g. we definitely want to customize the log + // function for the C lib at least). + break :options .{}; +}; + test { _ = terminal; - - // Tests always test the C API - _ = terminal.c_api; + _ = @import("lib/main.zig"); + @import("std").testing.refAllDecls(input); + if (comptime terminal.options.c_abi) { + _ = terminal.c_api; + } } diff --git a/src/main_build_data.zig b/src/main_build_data.zig index 13e604389..9dd1da395 100644 --- a/src/main_build_data.zig +++ b/src/main_build_data.zig @@ -33,7 +33,9 @@ pub fn main() !void { const action = action_ orelse return error.NoAction; // Our output always goes to stdout. - const writer = std.io.getStdOut().writer(); + var buffer: [1024]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const writer = &stdout_writer.interface; switch (action) { .bash => try writer.writeAll(@import("extra/bash.zig").completions), .fish => try writer.writeAll(@import("extra/fish.zig").completions), @@ -45,4 +47,5 @@ pub fn main() !void { .@"vim-compiler" => try writer.writeAll(@import("extra/vim.zig").compiler), .terminfo => try @import("terminfo/ghostty.zig").ghostty.encode(writer), } + try stdout_writer.end(); } diff --git a/src/main_c.zig b/src/main_c.zig index 9a9bcc6d2..d3fb753ef 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -63,18 +63,42 @@ const Info = extern struct { pub const String = extern struct { ptr: ?[*]const u8, len: usize, + sentinel: bool, pub const empty: String = .{ .ptr = null, .len = 0, + .sentinel = false, }; - pub fn fromSlice(slice: []const u8) String { + pub fn fromSlice(slice: anytype) String { return .{ .ptr = slice.ptr, .len = slice.len, + .sentinel = sentinel: { + const info = @typeInfo(@TypeOf(slice)); + switch (info) { + .pointer => |p| { + if (p.size != .slice) @compileError("only slices supported"); + if (p.child != u8) @compileError("only u8 slices supported"); + const sentinel_ = p.sentinel(); + if (sentinel_) |sentinel| if (sentinel != 0) @compileError("only 0 is supported for sentinels"); + break :sentinel sentinel_ != null; + }, + else => @compileError("only []const u8 and [:0]const u8"), + } + }, }; } + + pub fn deinit(self: *const String) void { + const ptr = self.ptr orelse return; + if (self.sentinel) { + state.alloc.free(ptr[0..self.len :0]); + } else { + state.alloc.free(ptr[0..self.len]); + } + } }; /// Initialize ghostty global state. @@ -129,5 +153,45 @@ pub export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 { /// Free a string allocated by Ghostty. pub export fn ghostty_string_free(str: String) void { - state.alloc.free(str.ptr.?[0..str.len]); + str.deinit(); +} + +test "ghostty_string_s empty string" { + const testing = std.testing; + const empty_string = String.empty; + defer empty_string.deinit(); + + try testing.expect(empty_string.len == 0); + try testing.expect(empty_string.sentinel == false); +} + +test "ghostty_string_s c string" { + const testing = std.testing; + state.alloc = testing.allocator; + + const slice: [:0]const u8 = "hello"; + const allocated_slice = try testing.allocator.dupeZ(u8, slice); + const c_null_string = String.fromSlice(allocated_slice); + defer c_null_string.deinit(); + + try testing.expect(allocated_slice[5] == 0); + try testing.expect(@TypeOf(slice) == [:0]const u8); + try testing.expect(@TypeOf(allocated_slice) == [:0]u8); + try testing.expect(c_null_string.len == 5); + try testing.expect(c_null_string.sentinel == true); +} + +test "ghostty_string_s zig string" { + const testing = std.testing; + state.alloc = testing.allocator; + + const slice: []const u8 = "hello"; + const allocated_slice = try testing.allocator.dupe(u8, slice); + const zig_string = String.fromSlice(allocated_slice); + defer zig_string.deinit(); + + try testing.expect(@TypeOf(slice) == []const u8); + try testing.expect(@TypeOf(allocated_slice) == []u8); + try testing.expect(zig_string.len == 5); + try testing.expect(zig_string.sentinel == false); } diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index 555dd16bf..decfc609c 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -35,7 +35,9 @@ pub fn main() !MainReturn { // a global is because the C API needs to be able to access this state; // no other Zig code should EVER access the global state. state.init() catch |err| { - const stderr = std.io.getStdErr().writer(); + var buffer: [1024]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&buffer); + const stderr = &stderr_writer.interface; defer posix.exit(1); const ErrSet = @TypeOf(err) || error{Unknown}; switch (@as(ErrSet, @errorCast(err))) { @@ -54,6 +56,7 @@ pub fn main() !MainReturn { else => try stderr.print("invalid CLI invocation err={}\n", .{err}), } + try stderr.flush(); }; defer state.deinit(); const alloc = state.alloc; @@ -154,8 +157,12 @@ fn logFn( .stderr => { // Always try default to send to stderr - const stderr = std.io.getStdErr().writer(); - nosuspend stderr.print(level_txt ++ prefix ++ format ++ "\n", args) catch return; + 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 {}; }, } } @@ -191,8 +198,8 @@ test { _ = @import("simd/main.zig"); _ = @import("synthetic/main.zig"); _ = @import("unicode/main.zig"); - _ = @import("unicode/props_ziglyph.zig"); - _ = @import("unicode/symbols_ziglyph.zig"); + _ = @import("unicode/props_uucode.zig"); + _ = @import("unicode/symbols_uucode.zig"); // Extra _ = @import("extra/bash.zig"); diff --git a/src/os/cgroup.zig b/src/os/cgroup.zig index 4f13921c5..4b5ccc4d3 100644 --- a/src/os/cgroup.zig +++ b/src/os/cgroup.zig @@ -19,8 +19,7 @@ pub fn current(alloc: Allocator, pid: std.os.linux.pid_t) !?[]const u8 { defer file.close(); // Read it all into memory -- we don't expect this file to ever be that large. - var buf_reader = std.io.bufferedReader(file.reader()); - const contents = try buf_reader.reader().readAllAlloc( + const contents = try file.readToEndAlloc( alloc, 1 * 1024 * 1024, // 1MB ); @@ -52,7 +51,11 @@ pub fn create( ); const file = try std.fs.cwd().openFile(pid_path, .{ .mode = .write_only }); defer file.close(); - try file.writer().print("{}", .{pid}); + + var file_buf: [64]u8 = undefined; + var writer = file.writer(&file_buf); + try writer.interface.print("{}", .{pid}); + try writer.interface.flush(); } } @@ -182,8 +185,7 @@ pub fn controllers(alloc: Allocator, cgroup: []const u8) ![]const u8 { // Read it all into memory -- we don't expect this file to ever // be that large. - var buf_reader = std.io.bufferedReader(file.reader()); - const contents = try buf_reader.reader().readAllAlloc( + const contents = try file.readToEndAlloc( alloc, 1 * 1024 * 1024, // 1MB ); @@ -213,7 +215,10 @@ pub fn configureControllers( defer file.close(); // Write - try file.writer().writeAll(v); + var writer_buf: [4096]u8 = undefined; + var writer = file.writer(&writer_buf); + try writer.interface.writeAll(v); + try writer.interface.flush(); } pub const Limit = union(enum) { @@ -242,5 +247,8 @@ pub fn configureLimit(cgroup: []const u8, limit: Limit) !void { defer file.close(); // Write our limit in bytes - try file.writer().print("{}", .{size}); + var writer_buf: [4096]u8 = undefined; + var writer = file.writer(&writer_buf); + try writer.interface.print("{}", .{size}); + try writer.interface.flush(); } diff --git a/src/os/hostname.zig b/src/os/hostname.zig index a75ca1cbb..f728a2455 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -1,157 +1,100 @@ const std = @import("std"); -const builtin = @import("builtin"); const posix = std.posix; -pub const HostnameParsingError = error{ - NoHostnameInUri, - NoSpaceLeft, -}; - -pub const UrlParsingError = std.Uri.ParseError || error{ - HostnameIsNotMacAddress, - NoSchemeProvided, -}; - -const mac_address_length = 17; - -fn isUriPathSeparator(c: u8) bool { - return switch (c) { - '?', '#' => true, - else => false, - }; -} - -fn isValidMacAddress(mac_address: []const u8) bool { - // A valid mac address has 6 two-character components with 5 colons, e.g. 12:34:56:ab:cd:ef. - if (mac_address.len != 17) { - return false; - } - - for (mac_address, 0..) |c, i| { - if ((i + 1) % 3 == 0) { - if (c != ':') { - return false; - } - } else if (!std.mem.containsAtLeastScalar(u8, "0123456789ABCDEFabcdef", 1, c)) { - return false; - } - } - - return true; -} - -/// Parses the provided url to a `std.Uri` struct. This is very specific to getting hostname and -/// path information for Ghostty's PWD reporting functionality. Takes into account that on macOS -/// the url passed to this function might have a mac address as its hostname and parses it -/// correctly. -pub fn parseUrl(url: []const u8) UrlParsingError!std.Uri { - return std.Uri.parse(url) catch |e| { - // The mac-address-as-hostname issue is specific to macOS so we just return an error if we - // hit it on other platforms. - if (comptime builtin.os.tag != .macos) return e; - - // It's possible this is a mac address on macOS where the last 2 characters in the - // address are non-digits, e.g. 'ff', and thus an invalid port. - // - // Example: file://12:34:56:78:90:12/path/to/file - if (e != error.InvalidPort) return e; - - const url_without_scheme_start = std.mem.indexOf(u8, url, "://") orelse { - return error.NoSchemeProvided; - }; - const scheme = url[0..url_without_scheme_start]; - const url_without_scheme = url[url_without_scheme_start + 3 ..]; - - // The first '/' after the scheme marks the end of the hostname. If the first '/' - // following the end of the scheme is not at the right position this is not a - // valid mac address. - if (url_without_scheme.len != mac_address_length and - std.mem.indexOfScalarPos(u8, url_without_scheme, 0, '/') != mac_address_length) - { - return error.HostnameIsNotMacAddress; - } - - // At this point we may have a mac address as the hostname. - const mac_address = url_without_scheme[0..mac_address_length]; - - if (!isValidMacAddress(mac_address)) { - return error.HostnameIsNotMacAddress; - } - - var uri_path_end_idx: usize = mac_address_length; - while (uri_path_end_idx < url_without_scheme.len and - !isUriPathSeparator(url_without_scheme[uri_path_end_idx])) - { - uri_path_end_idx += 1; - } - - // Same compliance factor as std.Uri.parse(), i.e. not at all compliant with the URI - // spec. - return .{ - .scheme = scheme, - .host = .{ .percent_encoded = mac_address }, - .path = .{ - .percent_encoded = url_without_scheme[mac_address_length..uri_path_end_idx], - }, - }; - }; -} - -/// Print the hostname from a file URI into a buffer. -pub fn bufPrintHostnameFromFileUri( - buf: []u8, - uri: std.Uri, -) HostnameParsingError![]const u8 { - // Get the raw string of the URI. Its unclear to me if the various - // tags of this enum guarantee no percent-encoding so we just - // check all of it. This isn't a performance critical path. - const host_component = uri.host orelse return error.NoHostnameInUri; - const host: []const u8 = switch (host_component) { - .raw => |v| v, - .percent_encoded => |v| v, - }; - - // When the "Private Wi-Fi address" setting is toggled on macOS the hostname - // is set to a random mac address, e.g. '12:34:56:78:90:ab'. - // The URI will be parsed as if the last set of digits is a port number, so - // we need to make sure that part is included when it's set. - - // We're only interested in special port handling when the current hostname is a - // partial MAC address that's potentially missing the last component. - // If that's not the case we just return the plain URI hostname directly. - // NOTE: This implementation is not sufficient to verify a valid mac address, but - // it's probably sufficient for this specific purpose. - if (host.len != 14 or std.mem.count(u8, host, ":") != 4) return host; - - // If we don't have a port then we can return the hostname as-is because - // it's not a partial MAC-address. - const port = uri.port orelse return host; - - // If the port is not a 1 or 2-digit number we're not looking at a partial - // MAC-address, and instead just a regular port so we return the plain - // URI hostname. - if (port > 99) return host; - - var fbs = std.io.fixedBufferStream(buf); - try std.fmt.format( - fbs.writer(), - // Make sure "port" is always 2 digits, prefixed with a 0 when "port" is a 1-digit number. - "{s}:{d:0>2}", - .{ host, port }, - ); - - return fbs.getWritten(); -} - pub const LocalHostnameValidationError = error{ PermissionDenied, Unexpected, }; +/// Validates a hostname according to [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123) +/// +/// std.net.isValidHostname is (currently) too generous. It considers strings like +/// ".example.com", "exa..mple.com", and "-example.com" to be valid hostnames, which +/// is incorrect. +pub fn isValid(hostname: []const u8) bool { + if (hostname.len == 0) return false; + if (hostname[0] == '.') return false; + + // Ignore trailing dot (FQDN). It doesn't count toward our length. + const end = if (hostname[hostname.len - 1] == '.') end: { + if (hostname.len == 1) return false; + break :end hostname.len - 1; + } else hostname.len; + + if (end > 253) return false; + + // Hostnames are divided into dot-separated "labels", which: + // + // - Start with a letter or digit + // - Can contain letters, digits, or hyphens + // - Must end with a letter or digit + // - Have a minimum of 1 character and a maximum of 63 + var label_start: usize = 0; + var label_len: usize = 0; + for (hostname[0..end], 0..) |c, i| { + switch (c) { + '.' => { + if (label_len == 0 or label_len > 63) return false; + if (!std.ascii.isAlphanumeric(hostname[label_start])) return false; + if (!std.ascii.isAlphanumeric(hostname[i - 1])) return false; + + label_start = i + 1; + label_len = 0; + }, + '-' => { + label_len += 1; + }, + else => { + if (!std.ascii.isAlphanumeric(c)) return false; + label_len += 1; + }, + } + } + + // Validate the final label + if (label_len == 0 or label_len > 63) return false; + if (!std.ascii.isAlphanumeric(hostname[label_start])) return false; + if (!std.ascii.isAlphanumeric(hostname[end - 1])) return false; + + return true; +} + +test isValid { + const testing = std.testing; + + // Valid hostnames + try testing.expect(isValid("example")); + try testing.expect(isValid("example.com")); + try testing.expect(isValid("www.example.com")); + try testing.expect(isValid("sub.domain.example.com")); + try testing.expect(isValid("example.com.")); + try testing.expect(isValid("host-name.example.com.")); + try testing.expect(isValid("123.example.com.")); + try testing.expect(isValid("a-b.com")); + try testing.expect(isValid("a.b.c.d.e.f.g")); + try testing.expect(isValid("127.0.0.1")); // Also a valid hostname + try testing.expect(isValid("a" ** 63 ++ ".com")); // Label exactly 63 chars (valid) + try testing.expect(isValid("a." ** 126 ++ "a")); // Total length 253 (valid) + + // Invalid hostnames + try testing.expect(!isValid("")); + try testing.expect(!isValid(".example.com")); + try testing.expect(!isValid("example.com..")); + try testing.expect(!isValid("host..domain")); + try testing.expect(!isValid("-hostname")); + try testing.expect(!isValid("hostname-")); + try testing.expect(!isValid("a.-.b")); + try testing.expect(!isValid("host_name.com")); + try testing.expect(!isValid(".")); + try testing.expect(!isValid("..")); + try testing.expect(!isValid("a" ** 64 ++ ".com")); // Label length 64 (too long) + try testing.expect(!isValid("a." ** 126 ++ "ab")); // Total length 254 (too long) +} + /// Checks if a hostname is local to the current machine. This matches /// both "localhost" and the current hostname of the machine (as returned /// by `gethostname`). -pub fn isLocalHostname(hostname: []const u8) LocalHostnameValidationError!bool { +pub fn isLocal(hostname: []const u8) LocalHostnameValidationError!bool { // A 'localhost' hostname is always considered local. if (std.mem.eql(u8, "localhost", hostname)) return true; @@ -161,185 +104,19 @@ pub fn isLocalHostname(hostname: []const u8) LocalHostnameValidationError!bool { return std.mem.eql(u8, hostname, ourHostname); } -test parseUrl { - // 1. Typical hostnames. - - var uri = try parseUrl("file://personal.computer/home/test/"); - - try std.testing.expectEqualStrings("file", uri.scheme); - try std.testing.expectEqualStrings("personal.computer", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); - try std.testing.expect(uri.port == null); - - uri = try parseUrl("kitty-shell-cwd://personal.computer/home/test/"); - - try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); - try std.testing.expectEqualStrings("personal.computer", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); - try std.testing.expect(uri.port == null); - - // 2. Hostnames that are mac addresses. - - // Numerical mac addresses. - - uri = try parseUrl("file://12:34:56:78:90:12/home/test/"); - - try std.testing.expectEqualStrings("file", uri.scheme); - try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); - try std.testing.expect(uri.port == 12); - - uri = try parseUrl("kitty-shell-cwd://12:34:56:78:90:12/home/test/"); - - try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); - try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); - try std.testing.expect(uri.port == 12); - - // Alphabetical mac addresses. - - uri = try parseUrl("file://ab:cd:ef:ab:cd:ef/home/test/"); - - try std.testing.expectEqualStrings("file", uri.scheme); - try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); - try std.testing.expect(uri.port == null); - - uri = try parseUrl("kitty-shell-cwd://ab:cd:ef:ab:cd:ef/home/test/"); - - try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); - try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); - try std.testing.expect(uri.port == null); - - // 3. Hostnames that are mac addresses with no path. - - // Numerical mac addresses. - - uri = try parseUrl("file://12:34:56:78:90:12"); - - try std.testing.expectEqualStrings("file", uri.scheme); - try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("", uri.path.percent_encoded); - try std.testing.expect(uri.port == 12); - - uri = try parseUrl("kitty-shell-cwd://12:34:56:78:90:12"); - - try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); - try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("", uri.path.percent_encoded); - try std.testing.expect(uri.port == 12); - - // Alphabetical mac addresses. - - uri = try parseUrl("file://ab:cd:ef:ab:cd:ef"); - - try std.testing.expectEqualStrings("file", uri.scheme); - try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("", uri.path.percent_encoded); - try std.testing.expect(uri.port == null); - - uri = try parseUrl("kitty-shell-cwd://ab:cd:ef:ab:cd:ef"); - - try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); - try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded); - try std.testing.expectEqualStrings("", uri.path.percent_encoded); - try std.testing.expect(uri.port == null); +test "isLocal returns true when provided hostname is localhost" { + try std.testing.expect(try isLocal("localhost")); } -test "parseUrl succeeds even if path component is missing" { - const uri = try parseUrl("file://12:34:56:78:90:ab"); - - try std.testing.expectEqualStrings("file", uri.scheme); - try std.testing.expectEqualStrings("12:34:56:78:90:ab", uri.host.?.percent_encoded); - try std.testing.expect(uri.path.isEmpty()); - try std.testing.expect(uri.port == null); -} - -test "bufPrintHostnameFromFileUri succeeds with ascii hostname" { - const uri = try std.Uri.parse("file://localhost/"); - - var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const actual = try bufPrintHostnameFromFileUri(&buf, uri); - try std.testing.expectEqualStrings("localhost", actual); -} - -test "bufPrintHostnameFromFileUri succeeds with hostname as mac address" { - const uri = try std.Uri.parse("file://12:34:56:78:90:12"); - - var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const actual = try bufPrintHostnameFromFileUri(&buf, uri); - try std.testing.expectEqualStrings("12:34:56:78:90:12", actual); -} - -test "bufPrintHostnameFromFileUri succeeds with hostname as mac address with the last component as ascii" { - const uri = try parseUrl("file://12:34:56:78:90:ab"); - - var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const actual = try bufPrintHostnameFromFileUri(&buf, uri); - try std.testing.expectEqualStrings("12:34:56:78:90:ab", actual); -} - -test "bufPrintHostnameFromFileUri succeeds with hostname as a mac address and the last section is < 10" { - const uri = try std.Uri.parse("file://12:34:56:78:90:05"); - - var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const actual = try bufPrintHostnameFromFileUri(&buf, uri); - try std.testing.expectEqualStrings("12:34:56:78:90:05", actual); -} - -test "bufPrintHostnameFromFileUri returns only hostname when there is a port component in the URI" { - // First: try with a non-2-digit port, to test general port handling. - const four_port_uri = try std.Uri.parse("file://has-a-port:1234"); - - var four_port_buf: [posix.HOST_NAME_MAX]u8 = undefined; - const four_port_actual = try bufPrintHostnameFromFileUri(&four_port_buf, four_port_uri); - try std.testing.expectEqualStrings("has-a-port", four_port_actual); - - // Second: try with a 2-digit port to test mac-address handling. - const two_port_uri = try std.Uri.parse("file://has-a-port:12"); - - var two_port_buf: [posix.HOST_NAME_MAX]u8 = undefined; - const two_port_actual = try bufPrintHostnameFromFileUri(&two_port_buf, two_port_uri); - try std.testing.expectEqualStrings("has-a-port", two_port_actual); - - // Third: try with a mac-address that has a port-component added to it to test mac-address handling. - const mac_with_port_uri = try std.Uri.parse("file://12:34:56:78:90:12:1234"); - - var mac_with_port_buf: [posix.HOST_NAME_MAX]u8 = undefined; - const mac_with_port_actual = try bufPrintHostnameFromFileUri(&mac_with_port_buf, mac_with_port_uri); - try std.testing.expectEqualStrings("12:34:56:78:90:12", mac_with_port_actual); -} - -test "bufPrintHostnameFromFileUri returns NoHostnameInUri error when hostname is missing from uri" { - const uri = try std.Uri.parse("file:///"); - - var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const actual = bufPrintHostnameFromFileUri(&buf, uri); - try std.testing.expectError(HostnameParsingError.NoHostnameInUri, actual); -} - -test "bufPrintHostnameFromFileUri returns NoSpaceLeft error when provided buffer has insufficient size" { - const uri = try std.Uri.parse("file://12:34:56:78:90:12/"); - - var buf: [5]u8 = undefined; - const actual = bufPrintHostnameFromFileUri(&buf, uri); - try std.testing.expectError(HostnameParsingError.NoSpaceLeft, actual); -} - -test "isLocalHostname returns true when provided hostname is localhost" { - try std.testing.expect(try isLocalHostname("localhost")); -} - -test "isLocalHostname returns true when hostname is local" { +test "isLocal returns true when hostname is local" { var buf: [posix.HOST_NAME_MAX]u8 = undefined; const localHostname = try posix.gethostname(&buf); - try std.testing.expect(try isLocalHostname(localHostname)); + try std.testing.expect(try isLocal(localHostname)); } -test "isLocalHostname returns false when hostname is not local" { +test "isLocal returns false when hostname is not local" { try std.testing.expectEqual( false, - try isLocalHostname("not-the-local-hostname"), + try isLocal("not-the-local-hostname"), ); } diff --git a/src/os/i18n_locales.zig b/src/os/i18n_locales.zig index 87eb4dec9..c1afa55ad 100644 --- a/src/os/i18n_locales.zig +++ b/src/os/i18n_locales.zig @@ -53,4 +53,5 @@ pub const locales = [_][:0]const u8{ "zh_TW.UTF-8", "hr_HR.UTF-8", "vi_VN.UTF-8", + "lt_LT.UTF-8", }; diff --git a/src/os/locale.zig b/src/os/locale.zig index b391d690f..92a63741f 100644 --- a/src/os/locale.zig +++ b/src/os/locale.zig @@ -83,6 +83,11 @@ fn setLangFromCocoa() void { const lang = locale.getProperty(objc.Object, "languageCode"); const country = locale.getProperty(objc.Object, "countryCode"); + if (lang.value == null or country.value == null) { + log.warn("languageCode or countryCode not found. Locale may be incorrect.", .{}); + return; + } + // Get our UTF8 string values const c_lang = lang.getProperty([*:0]const u8, "UTF8String"); const c_country = country.getProperty([*:0]const u8, "UTF8String"); diff --git a/src/os/main.zig b/src/os/main.zig index af851f673..2d269e412 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -29,6 +29,7 @@ pub const xdg = @import("xdg.zig"); pub const windows = @import("windows.zig"); pub const macos = @import("macos.zig"); pub const shell = @import("shell.zig"); +pub const uri = @import("uri.zig"); // Functions and types pub const CFReleaseThread = @import("cf_release_thread.zig"); @@ -67,6 +68,7 @@ pub const getKernelInfo = kernel_info.getKernelInfo; test { _ = i18n; _ = path; + _ = uri; if (comptime builtin.os.tag == .linux) { _ = kernel_info; diff --git a/src/os/shell.zig b/src/os/shell.zig index 3e57031dd..a6f23e843 100644 --- a/src/os/shell.zig +++ b/src/os/shell.zig @@ -1,110 +1,121 @@ const std = @import("std"); const testing = std.testing; +const Writer = std.Io.Writer; /// 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. /// -/// T should be a Zig type that follows the `std.io.Writer` interface. -pub fn ShellEscapeWriter(comptime T: type) type { - return struct { - child_writer: T, +/// T should be a Zig type that follows the `std.Io.Writer` interface. +pub const ShellEscapeWriter = struct { + writer: Writer, + child: *Writer, - fn write(self: *ShellEscapeWriter(T), data: []const u8) error{Error}!usize { - var count: usize = 0; - for (data) |byte| { - const buf = switch (byte) { - '\\', - '"', - '\'', - '$', - '`', - '*', - '?', - ' ', - '|', - '(', - ')', - => &[_]u8{ '\\', byte }, - else => &[_]u8{byte}, - }; - self.child_writer.writeAll(buf) catch return error.Error; - count += 1; - } - return count; + pub fn init(child: *Writer) ShellEscapeWriter { + return .{ + .writer = .{ + // TODO: Actually use a buffer here + .buffer = &.{}, + .vtable = &.{ .drain = ShellEscapeWriter.drain }, + }, + .child = child, + }; + } + + fn drain(w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize { + const self: *ShellEscapeWriter = @fieldParentPtr("writer", w); + + // TODO: This is a very naive implementation and does not really make + // full use of the post-Writergate API. However, since we know that + // this is going into an Allocating writer anyways, we can be a bit + // less strict here. + + var count: usize = 0; + for (data[0 .. data.len - 1]) |chunk| try self.writeEscaped(chunk, &count); + + for (0..splat) |_| try self.writeEscaped(data[data.len], &count); + return count; + } + + fn writeEscaped( + self: *ShellEscapeWriter, + s: []const u8, + count: *usize, + ) Writer.Error!void { + for (s) |byte| { + const buf = switch (byte) { + '\\', + '"', + '\'', + '$', + '`', + '*', + '?', + ' ', + '|', + '(', + ')', + => &[_]u8{ '\\', byte }, + else => &[_]u8{byte}, + }; + try self.child.writeAll(buf); + count.* += 1; } - - const Writer = std.io.Writer(*ShellEscapeWriter(T), error{Error}, write); - - pub fn init(child_writer: T) ShellEscapeWriter(T) { - return .{ .child_writer = child_writer }; - } - - pub fn writer(self: *ShellEscapeWriter(T)) Writer { - return .{ .context = self }; - } - }; -} + } +}; test "shell escape 1" { var buf: [128]u8 = undefined; - var fmt = std.io.fixedBufferStream(&buf); - var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; - const writer = shell.writer(); - try writer.writeAll("abc"); - try testing.expectEqualStrings("abc", fmt.getWritten()); + var writer: std.Io.Writer = .fixed(&buf); + var shell: ShellEscapeWriter = .{ .child_writer = &writer }; + try shell.writer.writeAll("abc"); + try testing.expectEqualStrings("abc", writer.buffered()); } test "shell escape 2" { var buf: [128]u8 = undefined; - var fmt = std.io.fixedBufferStream(&buf); - var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; - const writer = shell.writer(); - try writer.writeAll("a c"); - try testing.expectEqualStrings("a\\ c", fmt.getWritten()); + var writer: std.Io.Writer = .fixed(&buf); + var shell: ShellEscapeWriter = .{ .child_writer = &writer }; + try shell.writer.writeAll("a c"); + try testing.expectEqualStrings("a\\ c", writer.buffered()); } test "shell escape 3" { var buf: [128]u8 = undefined; - var fmt = std.io.fixedBufferStream(&buf); - var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; - const writer = shell.writer(); - try writer.writeAll("a?c"); - try testing.expectEqualStrings("a\\?c", fmt.getWritten()); + var writer: std.Io.Writer = .fixed(&buf); + var shell: ShellEscapeWriter = .{ .child_writer = &writer }; + try shell.writer.writeAll("a?c"); + try testing.expectEqualStrings("a\\?c", writer.buffered()); } test "shell escape 4" { var buf: [128]u8 = undefined; - var fmt = std.io.fixedBufferStream(&buf); - var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; - const writer = shell.writer(); - try writer.writeAll("a\\c"); - try testing.expectEqualStrings("a\\\\c", fmt.getWritten()); + var writer: std.Io.Writer = .fixed(&buf); + var shell: ShellEscapeWriter = .{ .child_writer = &writer }; + try shell.writer.writeAll("a\\c"); + try testing.expectEqualStrings("a\\\\c", writer.buffered()); } test "shell escape 5" { var buf: [128]u8 = undefined; - var fmt = std.io.fixedBufferStream(&buf); - var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; - const writer = shell.writer(); - try writer.writeAll("a|c"); - try testing.expectEqualStrings("a\\|c", fmt.getWritten()); + var writer: std.Io.Writer = .fixed(&buf); + var shell: ShellEscapeWriter = .{ .child_writer = &writer }; + try shell.writer.writeAll("a|c"); + try testing.expectEqualStrings("a\\|c", writer.buffered()); } test "shell escape 6" { var buf: [128]u8 = undefined; - var fmt = std.io.fixedBufferStream(&buf); - var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; - const writer = shell.writer(); - try writer.writeAll("a\"c"); - try testing.expectEqualStrings("a\\\"c", fmt.getWritten()); + var writer: std.Io.Writer = .fixed(&buf); + var shell: ShellEscapeWriter = .{ .child_writer = &writer }; + try shell.writer.writeAll("a\"c"); + try testing.expectEqualStrings("a\\\"c", writer.buffered()); } test "shell escape 7" { var buf: [128]u8 = undefined; - var fmt = std.io.fixedBufferStream(&buf); - var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() }; - const writer = shell.writer(); - try writer.writeAll("a(1)"); - try testing.expectEqualStrings("a\\(1\\)", fmt.getWritten()); + var writer: std.Io.Writer = .fixed(&buf); + var shell: ShellEscapeWriter = .{ .child_writer = &writer }; + try shell.writer.writeAll("a(1)"); + try testing.expectEqualStrings("a\\(1\\)", writer.buffered()); } diff --git a/src/os/string_encoding.zig b/src/os/string_encoding.zig new file mode 100644 index 000000000..162023ad2 --- /dev/null +++ b/src/os/string_encoding.zig @@ -0,0 +1,267 @@ +const std = @import("std"); + +/// Do an in-place decode of a string that has been encoded in the same way +/// that `bash`'s `printf %q` encodes a string. This is safe because a string +/// can only get shorter after decoding. This destructively modifies the buffer +/// given to it. If an error is returned the buffer may be in an unusable state. +pub fn printfQDecode(buf: [:0]u8) error{DecodeError}![:0]const u8 { + const data: [:0]u8 = data: { + // Strip off `$''` quoting. + if (std.mem.startsWith(u8, buf, "$'")) { + if (buf.len < 3 or !std.mem.endsWith(u8, buf, "'")) return error.DecodeError; + buf[buf.len - 1] = 0; + break :data buf[2 .. buf.len - 1 :0]; + } + // Strip off `''` quoting. + if (std.mem.startsWith(u8, buf, "'")) { + if (buf.len < 2 or !std.mem.endsWith(u8, buf, "'")) return error.DecodeError; + buf[buf.len - 1] = 0; + break :data buf[1 .. buf.len - 1 :0]; + } + break :data buf; + }; + + var src: usize = 0; + var dst: usize = 0; + + while (src < data.len) { + switch (data[src]) { + else => { + data[dst] = data[src]; + src += 1; + dst += 1; + }, + '\\' => { + if (src + 1 >= data.len) return error.DecodeError; + switch (data[src + 1]) { + ' ', + '\\', + '"', + '\'', + '$', + => |c| { + data[dst] = c; + src += 2; + dst += 1; + }, + 'e' => { + data[dst] = std.ascii.control_code.esc; + src += 2; + dst += 1; + }, + 'n' => { + data[dst] = std.ascii.control_code.lf; + src += 2; + dst += 1; + }, + 'r' => { + data[dst] = std.ascii.control_code.cr; + src += 2; + dst += 1; + }, + 't' => { + data[dst] = std.ascii.control_code.ht; + src += 2; + dst += 1; + }, + 'v' => { + data[dst] = std.ascii.control_code.vt; + src += 2; + dst += 1; + }, + else => return error.DecodeError, + } + }, + } + } + + data[dst] = 0; + return data[0..dst :0]; +} + +test "printf_q 1" { + const s: [:0]const u8 = "bobr\\ kurwa"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + const dst = try printfQDecode(&src); + try std.testing.expectEqualStrings("bobr kurwa", dst); +} + +test "printf_q 2" { + const s: [:0]const u8 = "bobr\\nkurwa"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + const dst = try printfQDecode(&src); + try std.testing.expectEqualStrings("bobr\nkurwa", dst); +} + +test "printf_q 3" { + const s: [:0]const u8 = "bobr\\dkurwa"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, printfQDecode(&src)); +} + +test "printf_q 4" { + const s: [:0]const u8 = "bobr kurwa\\"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, printfQDecode(&src)); +} + +test "printf_q 5" { + const s: [:0]const u8 = "$'bobr kurwa'"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + const dst = try printfQDecode(&src); + try std.testing.expectEqualStrings("bobr kurwa", dst); +} + +test "printf_q 6" { + const s: [:0]const u8 = "'bobr kurwa'"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + const dst = try printfQDecode(&src); + try std.testing.expectEqualStrings("bobr kurwa", dst); +} + +test "printf_q 7" { + const s: [:0]const u8 = "$'bobr kurwa"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, printfQDecode(&src)); +} + +test "printf_q 8" { + const s: [:0]const u8 = "$'"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, printfQDecode(&src)); +} + +test "printf_q 9" { + const s: [:0]const u8 = "'bobr kurwa"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, printfQDecode(&src)); +} + +test "printf_q 10" { + const s: [:0]const u8 = "'"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, printfQDecode(&src)); +} + +/// Do an in-place decode of a string that has been URL percent encoded. +/// This is safe because a string can only get shorter after decoding. This +/// destructively modifies the buffer given to it. If an error is returned the +/// buffer may be in an unusable state. +pub fn urlPercentDecode(buf: [:0]u8) error{DecodeError}![:0]const u8 { + var src: usize = 0; + var dst: usize = 0; + while (src < buf.len) { + switch (buf[src]) { + else => { + buf[dst] = buf[src]; + src += 1; + dst += 1; + }, + '%' => { + if (src + 2 >= buf.len) return error.DecodeError; + switch (buf[src + 1]) { + '0'...'9', 'a'...'f', 'A'...'F' => { + switch (buf[src + 2]) { + '0'...'9', 'a'...'f', 'A'...'F' => { + buf[dst] = std.math.shl(u8, hex(buf[src + 1]), 4) | hex(buf[src + 2]); + src += 3; + dst += 1; + }, + else => return error.DecodeError, + } + }, + else => return error.DecodeError, + } + }, + } + } + buf[dst] = 0; + return buf[0..dst :0]; +} + +inline fn hex(c: u8) u4 { + switch (c) { + '0'...'9' => return @truncate(c - '0'), + 'a'...'f' => return @truncate(c - 'a' + 10), + 'A'...'F' => return @truncate(c - 'A' + 10), + else => unreachable, + } +} + +test "singles percent" { + for (0..255) |c| { + var buf_: [4]u8 = undefined; + const buf = try std.fmt.bufPrintZ(&buf_, "%{x:0>2}", .{c}); + const decoded = try urlPercentDecode(buf); + try std.testing.expectEqual(1, decoded.len); + try std.testing.expectEqual(c, decoded[0]); + } + for (0..255) |c| { + var buf_: [4]u8 = undefined; + const buf = try std.fmt.bufPrintZ(&buf_, "%{X:0>2}", .{c}); + const decoded = try urlPercentDecode(buf); + try std.testing.expectEqual(1, decoded.len); + try std.testing.expectEqual(c, decoded[0]); + } +} + +test "percent 1" { + const s: [:0]const u8 = "bobr%20kurwa"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + const dst = try urlPercentDecode(&src); + try std.testing.expectEqualStrings("bobr kurwa", dst); +} + +test "percent 2" { + const s: [:0]const u8 = "bobr%2kurwa"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, urlPercentDecode(&src)); +} + +test "percent 3" { + const s: [:0]const u8 = "bobr%kurwa"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, urlPercentDecode(&src)); +} + +test "percent 4" { + const s: [:0]const u8 = "bobr%%kurwa"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, urlPercentDecode(&src)); +} + +test "percent 5" { + const s: [:0]const u8 = "bobr%20kurwa%20"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + const dst = try urlPercentDecode(&src); + try std.testing.expectEqualStrings("bobr kurwa ", dst); +} + +test "percent 6" { + const s: [:0]const u8 = "bobr%20kurwa%2"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, urlPercentDecode(&src)); +} + +test "percent 7" { + const s: [:0]const u8 = "bobr%20kurwa%"; + var src: [s.len:0]u8 = undefined; + @memcpy(&src, s); + try std.testing.expectError(error.DecodeError, urlPercentDecode(&src)); +} diff --git a/src/os/uri.zig b/src/os/uri.zig new file mode 100644 index 000000000..3d674870c --- /dev/null +++ b/src/os/uri.zig @@ -0,0 +1,204 @@ +const std = @import("std"); + +pub const ParseOptions = struct { + /// Parse MAC addresses in the host component. + /// + /// This is useful when the "Private Wi-Fi address" is enabled on macOS, + /// which sets the hostname to a rotating MAC address (12:34:56:ab:cd:ef). + mac_address: bool = false, + + /// Return the full, raw, unencoded path string. Any query and fragment + /// values will be return as part of the path instead of as distinct + /// fields. + raw_path: bool = false, +}; + +pub const ParseError = std.Uri.ParseError || error{InvalidMacAddress}; + +/// Parses a URI from the given string. +/// +/// This extends std.Uri.parse with some additional ParseOptions. +pub fn parse(text: []const u8, options: ParseOptions) ParseError!std.Uri { + var uri = std.Uri.parse(text) catch |err| uri: { + // We can attempt to re-parse the text as a URI that has a MAC address + // in its host field (which tripped up std.Uri.parse's port parsing): + // + // file://12:34:56:78:90:aa/path/to/file + // ^^ InvalidPort + // + if (err != error.InvalidPort or !options.mac_address) return err; + + // We can assume that the initial Uri.parse already validated the + // scheme, so we only need to find its bounds within the string. + const scheme_end = std.mem.indexOf(u8, text, "://") orelse { + return error.InvalidFormat; + }; + const scheme = text[0..scheme_end]; + + // We similarly find the bounds of the host component by looking + // for the first slash (/) after the scheme. This is all we need + // for this case because the resulting slice can be unambiguously + // determined to be a MAC address (or not). + const host_start = scheme_end + "://".len; + const host_end = std.mem.indexOfScalarPos(u8, text, host_start, '/') orelse text.len; + const mac_address = text[host_start..host_end]; + if (!isValidMacAddress(mac_address)) return error.InvalidMacAddress; + + // Parse the rest of the text (starting with the path component) as a + // partial URI and then add our MAC address as its host component. + var uri = try std.Uri.parseAfterScheme(scheme, text[host_end..]); + uri.host = .{ .percent_encoded = mac_address }; + break :uri uri; + }; + + // When MAC address parsing is enabled, we need to handle the case where + // std.Uri.parse parsed the address's last octet as a numeric port number. + // We use a few heuristics to identify this case (14 characters, 4 colons) + // and then "repair" the result by reassign the .host component to the full + // MAC address and clearing the .port component. + // + // 12:34:56:78:90:99 -> [12:34:56:78:90, 99] -> 12:34:56:78:90:99 + // (original host) (parsed host + port) (restored host) + // + if (options.mac_address and uri.host != null) mac: { + const host = uri.host.?.percent_encoded; + if (host.len != 14 or std.mem.count(u8, host, ":") != 4) break :mac; + + const port = uri.port orelse break :mac; + if (port > 99) break :mac; + + // std.Uri.parse returns slices pointing into the original text string. + const host_start = @intFromPtr(host.ptr) - @intFromPtr(text.ptr); + const path_start = @intFromPtr(uri.path.percent_encoded.ptr) - @intFromPtr(text.ptr); + const mac_address = text[host_start..path_start]; + if (!isValidMacAddress(mac_address)) return error.InvalidMacAddress; + + uri.host = .{ .percent_encoded = mac_address }; + uri.port = null; + } + + // When the raw_path option is active, return everything after the authority + // (host) in the .path component, including any query and fragment values. + if (options.raw_path) { + // std.Uri.parse returns slices pointing into the original text string. + const path_start = @intFromPtr(uri.path.percent_encoded.ptr) - @intFromPtr(text.ptr); + uri.path = .{ .raw = text[path_start..] }; + uri.query = null; + uri.fragment = null; + } + + return uri; +} + +test "parse: mac_address" { + const testing = @import("std").testing; + + // Numeric MAC address without a port + const uri1 = try parse("file://00:12:34:56:78:90/path", .{ .mac_address = true }); + try testing.expectEqualStrings("file", uri1.scheme); + try testing.expectEqualStrings("00:12:34:56:78:90", uri1.host.?.percent_encoded); + try testing.expectEqualStrings("/path", uri1.path.percent_encoded); + try testing.expectEqual(null, uri1.port); + + // Numeric MAC address with a port + const uri2 = try parse("file://00:12:34:56:78:90:999/path", .{ .mac_address = true }); + try testing.expectEqualStrings("file", uri2.scheme); + try testing.expectEqualStrings("00:12:34:56:78:90", uri2.host.?.percent_encoded); + try testing.expectEqualStrings("/path", uri2.path.percent_encoded); + try testing.expectEqual(999, uri2.port); + + // Alphabetic MAC address without a port + const uri3 = try parse("file://ab:cd:ef:ab:cd:ef/path", .{ .mac_address = true }); + try testing.expectEqualStrings("file", uri3.scheme); + try testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri3.host.?.percent_encoded); + try testing.expectEqualStrings("/path", uri3.path.percent_encoded); + try testing.expectEqual(null, uri3.port); + + // Alphabetic MAC address with a port + const uri4 = try parse("file://ab:cd:ef:ab:cd:ef:999/path", .{ .mac_address = true }); + try testing.expectEqualStrings("file", uri4.scheme); + try testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri4.host.?.percent_encoded); + try testing.expectEqualStrings("/path", uri4.path.percent_encoded); + try testing.expectEqual(999, uri4.port); + + // Numeric MAC address without a path component + const uri5 = try parse("file://00:12:34:56:78:90", .{ .mac_address = true }); + try testing.expectEqualStrings("file", uri5.scheme); + try testing.expectEqualStrings("00:12:34:56:78:90", uri5.host.?.percent_encoded); + try testing.expect(uri5.path.isEmpty()); + + // Alphabetic MAC address without a path component + const uri6 = try parse("file://ab:cd:ef:ab:cd:ef", .{ .mac_address = true }); + try testing.expectEqualStrings("file", uri6.scheme); + try testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri6.host.?.percent_encoded); + try testing.expect(uri6.path.isEmpty()); + + // Invalid MAC addresses + try testing.expectError(error.InvalidMacAddress, parse( + "file://zz:zz:zz:zz:zz:00/path", + .{ .mac_address = true }, + )); + try testing.expectError(error.InvalidMacAddress, parse( + "file://zz:zz:zz:zz:zz:zz/path", + .{ .mac_address = true }, + )); +} + +test "parse: raw_path" { + const testing = @import("std").testing; + + const text = "file://localhost/path??#fragment"; + var buf: [256]u8 = undefined; + + const uri1 = try parse(text, .{ .raw_path = false }); + try testing.expectEqualStrings("file", uri1.scheme); + try testing.expectEqualStrings("localhost", uri1.host.?.percent_encoded); + try testing.expectEqualStrings("/path", try uri1.path.toRaw(&buf)); + try testing.expectEqualStrings("?", uri1.query.?.percent_encoded); + try testing.expectEqualStrings("fragment", uri1.fragment.?.percent_encoded); + + const uri2 = try parse(text, .{ .raw_path = true }); + try testing.expectEqualStrings("file", uri2.scheme); + try testing.expectEqualStrings("localhost", uri2.host.?.percent_encoded); + try testing.expectEqualStrings("/path??#fragment", try uri2.path.toRaw(&buf)); + try testing.expectEqual(null, uri2.query); + try testing.expectEqual(null, uri2.fragment); + + const uri3 = try parse("file://localhost", .{ .raw_path = true }); + try testing.expectEqualStrings("file", uri3.scheme); + try testing.expectEqualStrings("localhost", uri3.host.?.percent_encoded); + try testing.expect(uri3.path.isEmpty()); + try testing.expectEqual(null, uri3.query); + try testing.expectEqual(null, uri3.fragment); +} + +/// Checks if a string represents a valid MAC address, e.g. 12:34:56:ab:cd:ef. +fn isValidMacAddress(s: []const u8) bool { + if (s.len != 17) return false; + + for (s, 0..) |c, i| { + if (i % 3 == 2) { + if (c != ':') return false; + } else { + switch (c) { + '0'...'9', 'A'...'F', 'a'...'f' => {}, + else => return false, + } + } + } + + return true; +} + +test isValidMacAddress { + const testing = @import("std").testing; + + try testing.expect(isValidMacAddress("01:23:45:67:89:Aa")); + try testing.expect(isValidMacAddress("Aa:Bb:Cc:Dd:Ee:Ff")); + + try testing.expect(!isValidMacAddress("")); + try testing.expect(!isValidMacAddress("00:23:45")); + try testing.expect(!isValidMacAddress("00:23:45:Xx:Yy:Zz")); + try testing.expect(!isValidMacAddress("01-23-45-67-89-Aa")); + try testing.expect(!isValidMacAddress("01:23:45:67:89:Aa:Bb")); +} diff --git a/src/os/wasm.zig b/src/os/wasm.zig index 73a5922cf..3d0b90e9a 100644 --- a/src/os/wasm.zig +++ b/src/os/wasm.zig @@ -23,93 +23,3 @@ pub const alloc = if (builtin.is_test) std.testing.allocator else std.heap.wasm_allocator; - -/// For host-owned allocations: -/// We need to keep track of our own pointer lengths because Zig -/// allocators usually don't do this and we need to be able to send -/// a direct pointer back to the host system. A more appropriate thing -/// to do would be to probably make a custom allocator that keeps track -/// of size. -var allocs: std.AutoHashMapUnmanaged([*]u8, usize) = .{}; - -/// Allocate len bytes and return a pointer to the memory in the host. -/// The data is not zeroed. -pub export fn malloc(len: usize) ?[*]u8 { - return alloc_(len) catch return null; -} - -fn alloc_(len: usize) ![*]u8 { - // Create the allocation - const slice = try alloc.alloc(u8, len); - errdefer alloc.free(slice); - - // Store the size so we can deallocate later - try allocs.putNoClobber(alloc, slice.ptr, slice.len); - errdefer _ = allocs.remove(slice.ptr); - - return slice.ptr; -} - -/// Free an allocation from malloc. -pub export fn free(ptr: ?[*]u8) void { - if (ptr) |v| { - if (allocs.get(v)) |len| { - const slice = v[0..len]; - alloc.free(slice); - _ = allocs.remove(v); - } - } -} - -/// Convert an allocated pointer of any type to a host-owned pointer. -/// This pushes the responsibility to free it to the host. The returned -/// pointer will match the pointer but is typed correctly for returning -/// to the host. -pub fn toHostOwned(ptr: anytype) ![*]u8 { - // Convert our pointer to a byte array - const info = @typeInfo(@TypeOf(ptr)).pointer; - const T = info.child; - const size = @sizeOf(T); - const casted = @as([*]u8, @ptrFromInt(@intFromPtr(ptr))); - - // Store the information about it - try allocs.putNoClobber(alloc, casted, size); - errdefer _ = allocs.remove(casted); - - return casted; -} - -/// Returns true if the value is host owned. -pub fn isHostOwned(ptr: anytype) bool { - const casted = @as([*]u8, @ptrFromInt(@intFromPtr(ptr))); - return allocs.contains(casted); -} - -/// Convert a pointer back to a module-owned value. The caller is expected -/// to cast or have the valid pointer for alloc calls. -pub fn toModuleOwned(ptr: anytype) void { - const casted = @as([*]u8, @ptrFromInt(@intFromPtr(ptr))); - _ = allocs.remove(casted); -} - -test "basics" { - const testing = std.testing; - const buf = malloc(32).?; - try testing.expect(allocs.size == 1); - free(buf); - try testing.expect(allocs.size == 0); -} - -test "toHostOwned" { - const testing = std.testing; - - const Point = struct { x: u32 = 0, y: u32 = 0 }; - const p = try alloc.create(Point); - errdefer alloc.destroy(p); - const ptr = try toHostOwned(p); - try testing.expect(allocs.size == 1); - try testing.expect(isHostOwned(p)); - try testing.expect(isHostOwned(ptr)); - free(ptr); - try testing.expect(allocs.size == 0); -} diff --git a/src/os/wasm/log.zig b/src/os/wasm/log.zig index d81571229..1aac8c4e7 100644 --- a/src/os/wasm/log.zig +++ b/src/os/wasm/log.zig @@ -1,15 +1,12 @@ const std = @import("std"); const builtin = @import("builtin"); const wasm = @import("../wasm.zig"); -const wasm_target = @import("target.zig"); // Use the correct implementation -pub const log = if (wasm_target.target) |target| switch (target) { - .browser => Browser.log, -} else @compileError("wasm target required"); +pub const log = Freestanding.log; -/// Browser implementation calls an extern "log" function. -pub const Browser = struct { +/// Freestanding implementation calls an extern "log" function. +pub const Freestanding = struct { // The function std.log will call. pub fn log( comptime level: std.log.Level, diff --git a/src/os/wasm/target.zig b/src/os/wasm/target.zig index cd8b2dd33..a6a29e208 100644 --- a/src/os/wasm/target.zig +++ b/src/os/wasm/target.zig @@ -10,7 +10,7 @@ pub const Target = enum { }; /// Our specific target platform. -pub const target: ?Target = if (!builtin.target.isWasm()) null else target: { +pub const target: ?Target = if (!builtin.target.cpu.arch.isWasm()) null else target: { const result = @as(Target, @enumFromInt(@intFromEnum(options.wasm_target))); // This maybe isn't necessary but I don't know if enums without a specific // tag type and value are guaranteed to be the same between build.zig diff --git a/src/pty.zig b/src/pty.zig index 02906b778..1ab88d40f 100644 --- a/src/pty.zig +++ b/src/pty.zig @@ -216,7 +216,7 @@ const PosixPty = struct { // Reset our signals var sa: posix.Sigaction = .{ .handler = .{ .handler = posix.SIG.DFL }, - .mask = posix.empty_sigset, + .mask = posix.sigemptyset(), .flags = 0, }; posix.sigaction(posix.SIG.ABRT, &sa, null); diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index 3cf306f91..d8427689b 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -236,8 +236,9 @@ pub fn isCovering(cp: u21) bool { } /// Returns true of the codepoint is a "symbol-like" character, which -/// for now we define as anything in a private use area and anything +/// for now we define as anything in a private use area, and anything /// in several unicode blocks: +/// - Arrows /// - Dingbats /// - Emoticons /// - Miscellaneous Symbols @@ -274,9 +275,9 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { // If we have a previous cell and it was a symbol then we need // to also constrain. This is so that multiple PUA glyphs align. - // As an exception, we ignore powerline glyphs since they are - // used for box drawing and we consider them whitespace. - if (cell_pin.x > 0) prev: { + // This does not apply if the previous symbol is a graphics + // element such as a block element or Powerline glyph. + if (cell_pin.x > 0) { const prev_cp = prev_cp: { var copy = cell_pin; copy.x -= 1; @@ -284,10 +285,7 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { break :prev_cp prev_cell.codepoint(); }; - // We consider powerline glyphs whitespace. - if (isPowerline(prev_cp)) break :prev; - - if (isSymbol(prev_cp)) { + if (isSymbol(prev_cp) and !isGraphicsElement(prev_cp)) { return 1; } } @@ -300,10 +298,7 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { const next_cell = copy.rowAndCell().cell; break :next_cp next_cell.codepoint(); }; - if (next_cp == 0 or - isSpace(next_cp) or - isPowerline(next_cp)) - { + if (next_cp == 0 or isSpace(next_cp)) { return 2; } @@ -311,10 +306,10 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 { return 1; } -/// Whether min contrast should be disabled for a given glyph. +/// Whether min contrast should be disabled for a given glyph. True +/// for graphics elements such as blocks and Powerline glyphs. pub fn noMinContrast(cp: u21) bool { - // TODO: We should disable for all box drawing type characters. - return isPowerline(cp); + return isGraphicsElement(cp); } // Some general spaces, others intentionally kept @@ -328,10 +323,42 @@ fn isSpace(char: u21) bool { }; } +/// Returns true if the codepoint is used for terminal graphics, such +/// as box drawing characters, block elements, and Powerline glyphs. +fn isGraphicsElement(char: u21) bool { + return isBoxDrawing(char) or isBlockElement(char) or isLegacyComputing(char) or isPowerline(char); +} + +// Returns true if the codepoint is a box drawing character. +fn isBoxDrawing(char: u21) bool { + return switch (char) { + 0x2500...0x257F => true, + else => false, + }; +} + +// Returns true if the codepoint is a block element. +fn isBlockElement(char: u21) bool { + return switch (char) { + 0x2580...0x259F => true, + else => false, + }; +} + +// Returns true if the codepoint is in a Symbols for Legacy +// Computing block, including supplements. +fn isLegacyComputing(char: u21) bool { + return switch (char) { + 0x1FB00...0x1FBFF => true, + 0x1CC00...0x1CEBF => true, // Supplement introduced in Unicode 16.0 + else => false, + }; +} + // Returns true if the codepoint is a part of the Powerline range. fn isPowerline(char: u21) bool { return switch (char) { - 0xE0B0...0xE0C8, 0xE0CA, 0xE0CC...0xE0D2, 0xE0D4 => true, + 0xE0B0...0xE0D7 => true, else => false, }; } @@ -492,3 +519,113 @@ test "Contents with zero-sized screen" { c.setCursor(null, null); try testing.expect(c.getCursorGlyph() == null); } + +test "Cell constraint widths" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try terminal.Screen.init(alloc, 4, 1, 0); + defer s.deinit(); + + // for each case, the numbers in the comment denote expected + // constraint widths for the symbol-containing cells + + // symbol->nothing: 2 + { + try s.testWriteString(""); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(2, constraintWidth(p0)); + s.reset(); + } + + // symbol->character: 1 + { + try s.testWriteString("z"); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(1, constraintWidth(p0)); + s.reset(); + } + + // symbol->space: 2 + { + try s.testWriteString(" z"); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(2, constraintWidth(p0)); + s.reset(); + } + // symbol->no-break space: 1 + { + try s.testWriteString("\u{00a0}z"); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(1, constraintWidth(p0)); + s.reset(); + } + + // symbol->end of row: 1 + { + try s.testWriteString(" "); + const p3 = s.pages.pin(.{ .screen = .{ .x = 3, .y = 0 } }).?; + try testing.expectEqual(1, constraintWidth(p3)); + s.reset(); + } + + // character->symbol: 2 + { + try s.testWriteString("z"); + const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; + try testing.expectEqual(2, constraintWidth(p1)); + s.reset(); + } + + // symbol->symbol: 1,1 + { + try s.testWriteString(""); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; + try testing.expectEqual(1, constraintWidth(p0)); + try testing.expectEqual(1, constraintWidth(p1)); + s.reset(); + } + + // symbol->space->symbol: 2,2 + { + try s.testWriteString(" "); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const p2 = s.pages.pin(.{ .screen = .{ .x = 2, .y = 0 } }).?; + try testing.expectEqual(2, constraintWidth(p0)); + try testing.expectEqual(2, constraintWidth(p2)); + s.reset(); + } + + // symbol->powerline: 1 (dedicated test because powerline is special-cased in cellpkg) + { + try s.testWriteString(""); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(1, constraintWidth(p0)); + s.reset(); + } + + // powerline->symbol: 2 (dedicated test because powerline is special-cased in cellpkg) + { + try s.testWriteString(""); + const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; + try testing.expectEqual(2, constraintWidth(p1)); + s.reset(); + } + + // powerline->nothing: 2 (dedicated test because powerline is special-cased in cellpkg) + { + try s.testWriteString(""); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(2, constraintWidth(p0)); + s.reset(); + } + + // powerline->space: 2 (dedicated test because powerline is special-cased in cellpkg) + { + try s.testWriteString(" z"); + const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; + try testing.expectEqual(2, constraintWidth(p0)); + s.reset(); + } +} diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index fbc8cab99..9e13d0b41 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -114,6 +114,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// True if the window is focused focused: bool, + /// 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. + scrollbar: terminal.Scrollbar, + scrollbar_dirty: bool, + /// The foreground color set by an OSC 10 sequence. If unset then /// default_foreground_color is used. foreground_color: ?terminal.color.RGB, @@ -184,7 +190,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// Background image, if we have one. bg_image: ?imagepkg.Image = null, - /// Set whenever the background image changes, singalling + /// Set whenever the background image changes, signalling /// that the new background image needs to be uploaded to /// the GPU. /// @@ -683,6 +689,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .grid_metrics = font_critical.metrics, .size = options.size, .focused = true, + .scrollbar = .zero, + .scrollbar_dirty = false, .foreground_color = null, .default_foreground_color = options.config.foreground, .background_color = null, @@ -1060,6 +1068,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Update relevant uniforms self.updateFontGridUniforms(); + + // Force a full rebuild, because cached rows may still reference + // an outdated atlas from the old grid and this can cause garbage + // to be rendered. + self.cells_viewport = null; } /// Update uniforms that are based on the font grid. @@ -1087,6 +1100,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { preedit: ?renderer.State.Preedit, cursor_style: ?renderer.CursorStyle, color_palette: terminal.color.Palette, + scrollbar: terminal.Scrollbar, /// If true, rebuild the full screen. full_rebuild: bool, @@ -1111,6 +1125,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type { return; } + // Get our scrollbar out of the terminal. We synchronize + // the scrollbar read with frame data updates because this + // naturally limits the number of calls to this method (it + // can be expensive) and also makes it so we don't need another + // cross-thread mailbox message within the IO path. + const scrollbar = state.terminal.screen.pages.scrollbar(); + // Swap bg/fg if the terminal is reversed const bg = self.background_color orelse self.default_background_color; const fg = self.foreground_color orelse self.default_foreground_color; @@ -1238,6 +1259,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .preedit = preedit, .cursor_style = cursor_style, .color_palette = state.terminal.color_palette.colors, + .scrollbar = scrollbar, .full_rebuild = full_rebuild, }; }; @@ -1266,6 +1288,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self.draw_mutex.lock(); defer self.draw_mutex.unlock(); + // The scrollbar is only emitted during draws so we also + // check the scrollbar cache here and update if needed. + // This is pretty fast. + if (!self.scrollbar.eql(critical.scrollbar)) { + self.scrollbar = critical.scrollbar; + self.scrollbar_dirty = true; + } + // Update our background color self.uniforms.bg_color = .{ critical.bg.r, @@ -1289,6 +1319,16 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self.draw_mutex.lock(); defer self.draw_mutex.unlock(); + // After the graphics API is complete (so we defer) we want to + // update our scrollbar state. + defer if (self.scrollbar_dirty) { + // Fail instantly if the surface mailbox if full, we'll just + // get it on the next frame. + if (self.surface_mailbox.push(.{ + .scrollbar = self.scrollbar, + }, .instant) > 0) self.scrollbar_dirty = false; + }; + // Let our graphics API do any bookkeeping, etc. // that it needs to do before / after `drawFrame`. self.api.drawFrameStart(); @@ -3093,8 +3133,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // its cell(s), we don't modify the alignment at all. .constraint = getConstraint(cp) orelse if (cellpkg.isSymbol(cp)) .{ - .size_horizontal = .fit, - .size_vertical = .fit, + .size = .fit, } else .none, .constraint_width = constraintWidth(cell_pin), }, diff --git a/src/renderer/link.zig b/src/renderer/link.zig index 410fb8632..9f489ed48 100644 --- a/src/renderer/link.zig +++ b/src/renderer/link.zig @@ -34,19 +34,19 @@ pub const Set = struct { alloc: Allocator, config: []const inputpkg.Link, ) !Set { - var links = std.ArrayList(Link).init(alloc); - defer links.deinit(); + var links: std.ArrayList(Link) = .empty; + defer links.deinit(alloc); for (config) |link| { var regex = try link.oniRegex(); errdefer regex.deinit(); - try links.append(.{ + try links.append(alloc, .{ .regex = regex, .highlight = link.highlight, }); } - return .{ .links = try links.toOwnedSlice() }; + return .{ .links = try links.toOwnedSlice(alloc) }; } pub fn deinit(self: *Set, alloc: Allocator) void { @@ -77,8 +77,8 @@ pub const Set = struct { // as selections which contain the start and end points of // the match. There is no way to map these back to the link // configuration right now because we don't need to. - var matches = std.ArrayList(terminal.Selection).init(alloc); - defer matches.deinit(); + var matches: std.ArrayList(terminal.Selection) = .empty; + defer matches.deinit(alloc); // If our mouse is over an OSC8 link, then we can skip the regex // matches below since OSC8 takes priority. @@ -101,7 +101,7 @@ pub const Set = struct { ); } - return .{ .matches = try matches.toOwnedSlice() }; + return .{ .matches = try matches.toOwnedSlice(alloc) }; } fn matchSetFromOSC8( @@ -112,8 +112,6 @@ pub const Set = struct { mouse_pin: terminal.Pin, mouse_mods: inputpkg.Mods, ) !void { - _ = alloc; - // If the right mods aren't pressed, then we can't match. if (!mouse_mods.equal(inputpkg.ctrlOrSuper(.{}))) return; @@ -135,6 +133,7 @@ pub const Set = struct { if (link.id == .implicit) { const uri = link.uri.offset.ptr(page.memory)[0..link.uri.len]; return try self.matchSetFromOSC8Implicit( + alloc, matches, mouse_pin, uri, @@ -154,7 +153,7 @@ pub const Set = struct { // building our matching selection. if (!row.hyperlink) { if (current) |sel| { - try matches.append(sel); + try matches.append(alloc, sel); current = null; } @@ -191,7 +190,7 @@ pub const Set = struct { // No match, if we have a current selection then complete it. if (current) |sel| { - try matches.append(sel); + try matches.append(alloc, sel); current = null; } } @@ -203,6 +202,7 @@ pub const Set = struct { /// around the mouse pin. fn matchSetFromOSC8Implicit( self: *const Set, + alloc: Allocator, matches: *std.ArrayList(terminal.Selection), mouse_pin: terminal.Pin, uri: []const u8, @@ -264,7 +264,7 @@ pub const Set = struct { sel.endPtr().* = cell_pin; } - try matches.append(sel); + try matches.append(alloc, sel); } /// Fills matches with the matches from regex link matches. @@ -334,7 +334,7 @@ pub const Set = struct { => if (!sel.contains(screen, mouse_pin)) continue, } - try matches.append(sel); + try matches.append(alloc, sel); } } } diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 576237587..b0a190a8b 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -38,8 +38,8 @@ pub fn loadFromFiles( paths: configpkg.RepeatablePath, target: Target, ) ![]const [:0]const u8 { - var list = std.ArrayList([:0]const u8).init(alloc_gpa); - defer list.deinit(); + var list: std.ArrayList([:0]const u8) = .empty; + defer list.deinit(alloc_gpa); errdefer for (list.items) |shader| alloc_gpa.free(shader); for (paths.value.items) |item| { @@ -56,10 +56,10 @@ pub fn loadFromFiles( return err; }; log.info("loaded custom shader path={s}", .{path}); - try list.append(shader); + try list.append(alloc_gpa, shader); } - return try list.toOwnedSlice(); + return try list.toOwnedSlice(alloc_gpa); } /// Load a single shader from a file and convert it to the target language @@ -73,34 +73,33 @@ pub fn loadFromFile( defer arena.deinit(); const alloc = arena.allocator(); - // Load the shader file - const cwd = std.fs.cwd(); - const file = try cwd.openFile(path, .{}); - defer file.close(); - // Read it all into memory -- we don't expect shaders to be large. - var buf_reader = std.io.bufferedReader(file.reader()); - const src = try buf_reader.reader().readAllAlloc( - alloc, - 4 * 1024 * 1024, // 4MB - ); + const src = src: { + // Load the shader file + const cwd = std.fs.cwd(); + const file = try cwd.openFile(path, .{}); + defer file.close(); + + break :src try file.readToEndAlloc( + alloc, + 4 * 1024 * 1024, // 4MB + ); + }; // Convert to full GLSL const glsl: [:0]const u8 = glsl: { - var list = std.ArrayList(u8).init(alloc); - try glslFromShader(list.writer(), src); - try list.append(0); - break :glsl list.items[0 .. list.items.len - 1 :0]; + var stream: std.Io.Writer.Allocating = .init(alloc); + try glslFromShader(&stream.writer, src); + try stream.writer.writeByte(0); + break :glsl stream.written()[0 .. stream.written().len - 1 :0]; }; // Convert to SPIR-V const spirv: []const u8 = spirv: { - // SpirV pointer must be aligned to 4 bytes since we expect - // a slice of words. - var list = std.ArrayListAligned(u8, @alignOf(u32)).init(alloc); + var stream: std.Io.Writer.Allocating = .init(alloc); var errlog: SpirvLog = .{ .alloc = alloc }; defer errlog.deinit(); - spirvFromGlsl(list.writer(), &errlog, glsl) catch |err| { + spirvFromGlsl(&stream.writer, &errlog, glsl) catch |err| { if (errlog.info.len > 0 or errlog.debug.len > 0) { log.warn("spirv error path={s} info={s} debug={s}", .{ path, @@ -111,6 +110,11 @@ pub fn loadFromFile( return err; }; + + // SpirV pointer must be aligned to 4 bytes since we expect + // a slice of words. + var list: std.ArrayListAligned(u8, .of(u32)) = .empty; + try list.appendSlice(alloc, stream.written()); break :spirv list.items; }; @@ -129,7 +133,7 @@ pub fn loadFromFile( /// mainImage function and don't define any of the uniforms. This function /// will convert the ShaderToy shader into a valid GLSL shader that can be /// compiled and linked. -pub fn glslFromShader(writer: anytype, src: []const u8) !void { +pub fn glslFromShader(writer: *std.Io.Writer, src: []const u8) !void { const prefix = @embedFile("shaders/shadertoy_prefix.glsl"); try writer.writeAll(prefix); try writer.writeAll("\n\n"); @@ -138,7 +142,7 @@ pub fn glslFromShader(writer: anytype, src: []const u8) !void { /// Convert a GLSL shader into SPIR-V assembly. pub fn spirvFromGlsl( - writer: anytype, + writer: *std.Io.Writer, errlog: ?*SpirvLog, src: [:0]const u8, ) !void { @@ -331,10 +335,10 @@ fn spvCross( /// Convert ShaderToy shader to null-terminated glsl for testing. fn testGlslZ(alloc: Allocator, src: []const u8) ![:0]const u8 { - var list = std.ArrayList(u8).init(alloc); - defer list.deinit(); - try glslFromShader(list.writer(), src); - return try list.toOwnedSliceSentinel(0); + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try glslFromShader(&buf.writer, src); + return try buf.toOwnedSliceSentinel(0); } test "spirv" { @@ -345,9 +349,8 @@ test "spirv" { defer alloc.free(src); var buf: [4096 * 4]u8 = undefined; - var buf_stream = std.io.fixedBufferStream(&buf); - const writer = buf_stream.writer(); - try spirvFromGlsl(writer, null, src); + var writer: std.Io.Writer = .fixed(&buf); + try spirvFromGlsl(&writer, null, src); } test "spirv invalid" { @@ -358,12 +361,11 @@ test "spirv invalid" { defer alloc.free(src); var buf: [4096 * 4]u8 = undefined; - var buf_stream = std.io.fixedBufferStream(&buf); - const writer = buf_stream.writer(); + var writer: std.Io.Writer = .fixed(&buf); var errlog: SpirvLog = .{ .alloc = alloc }; defer errlog.deinit(); - try testing.expectError(error.GlslangFailed, spirvFromGlsl(writer, &errlog, src)); + try testing.expectError(error.GlslangFailed, spirvFromGlsl(&writer, &errlog, src)); try testing.expect(errlog.info.len > 0); } @@ -374,9 +376,14 @@ test "shadertoy to msl" { const src = try testGlslZ(alloc, test_crt); defer alloc.free(src); - var spvlist = std.ArrayListAligned(u8, @alignOf(u32)).init(alloc); - defer spvlist.deinit(); - try spirvFromGlsl(spvlist.writer(), null, src); + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try spirvFromGlsl(&buf.writer, null, src); + + // TODO: Replace this with an aligned version of Writer.Allocating + var spvlist: std.ArrayListAligned(u8, .of(u32)) = .empty; + defer spvlist.deinit(alloc); + try spvlist.appendSlice(alloc, buf.written()); const msl = try mslFromSpv(alloc, spvlist.items); defer alloc.free(msl); @@ -389,9 +396,14 @@ test "shadertoy to glsl" { const src = try testGlslZ(alloc, test_crt); defer alloc.free(src); - var spvlist = std.ArrayListAligned(u8, @alignOf(u32)).init(alloc); - defer spvlist.deinit(); - try spirvFromGlsl(spvlist.writer(), null, src); + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try spirvFromGlsl(&buf.writer, null, src); + + // TODO: Replace this with an aligned version of Writer.Allocating + var spvlist: std.ArrayListAligned(u8, .of(u32)) = .empty; + defer spvlist.deinit(alloc); + try spvlist.appendSlice(alloc, buf.written()); const glsl = try glslFromSpv(alloc, spvlist.items); defer alloc.free(glsl); diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 2cf9d388f..e910a9885 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -73,6 +73,13 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then builtin unset GHOSTTY_BASH_RCFILE fi +# Add Ghostty binary to PATH if the path feature is enabled +if [[ "$GHOSTTY_SHELL_FEATURES" == *"path"* && -n "$GHOSTTY_BIN_DIR" ]]; then + if [[ ":$PATH:" != *":$GHOSTTY_BIN_DIR:"* ]]; then + export PATH="$PATH:$GHOSTTY_BIN_DIR" + fi +fi + # Sudo if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then # Wrap `sudo` command to ensure Ghostty terminfo is preserved. diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 6d0d19f4f..33473c8b0 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -196,6 +196,11 @@ set edit:before-readline = (conj $edit:before-readline $beam~) set edit:after-readline = (conj $edit:after-readline {|_| block }) } + if (and (has-value $features path) (has-env GHOSTTY_BIN_DIR)) { + if (not (has-value $paths $E:GHOSTTY_BIN_DIR)) { + set paths = [$@paths $E:GHOSTTY_BIN_DIR] + } + } if (and (has-value $features sudo) (not-eq "" $E:TERMINFO) (has-external sudo)) { edit:add-var sudo~ $sudo-with-terminfo~ } 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 daa4f1d4f..47af9be98 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 @@ -54,13 +54,22 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" if contains cursor $features # Change the cursor to a beam on prompt. function __ghostty_set_cursor_beam --on-event fish_prompt -d "Set cursor shape" - echo -en "\e[5 q" + if not functions -q fish_vi_cursor_handle + echo -en "\e[5 q" + end end function __ghostty_reset_cursor --on-event fish_preexec -d "Reset cursor shape" - echo -en "\e[0 q" + if not functions -q fish_vi_cursor_handle + echo -en "\e[0 q" + end end end + # Add Ghostty binary to PATH if the path feature is enabled + if contains path $features; and test -n "$GHOSTTY_BIN_DIR" + fish_add_path --global --path --append "$GHOSTTY_BIN_DIR" + end + # When using sudo shell integration feature, ensure $TERMINFO is set # and `sudo` is not already a function or alias if contains sudo $features; and test -n "$TERMINFO"; and test "file" = (type -t sudo 2> /dev/null; or echo "x") diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 8607664a2..27ef39bbc 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -220,6 +220,13 @@ _ghostty_deferred_init() { builtin print -rnu $_ghostty_fd \$'\\e[0 q'" fi + # Add Ghostty binary to PATH if the path feature is enabled + if [[ "$GHOSTTY_SHELL_FEATURES" == *"path"* ]] && [[ -n "$GHOSTTY_BIN_DIR" ]]; then + if [[ ":$PATH:" != *":$GHOSTTY_BIN_DIR:"* ]]; then + builtin export PATH="$PATH:$GHOSTTY_BIN_DIR" + fi + fi + # Sudo if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* ]] && [[ -n "$TERMINFO" ]]; then # Wrap `sudo` command to ensure Ghostty terminfo is preserved diff --git a/src/simd/codepoint_width.zig b/src/simd/codepoint_width.zig index e097dbd61..c1767bea4 100644 --- a/src/simd/codepoint_width.zig +++ b/src/simd/codepoint_width.zig @@ -6,7 +6,9 @@ extern "c" fn ghostty_simd_codepoint_width(u32) i8; pub fn codepointWidth(cp: u32) i8 { if (comptime options.simd) return ghostty_simd_codepoint_width(cp); - return @import("ziglyph").display_width.codePointWidth(@intCast(cp), .half); + const uucode = @import("uucode"); + if (cp > uucode.config.max_code_point) return 1; + return @import("uucode").get(.width, @intCast(cp)); } test "codepointWidth basic" { @@ -20,26 +22,30 @@ test "codepointWidth basic" { try testing.expectEqual(@as(i8, 2), codepointWidth(0xF900)); // 豈 try testing.expectEqual(@as(i8, 2), codepointWidth(0x20000)); // 𠀀 try testing.expectEqual(@as(i8, 2), codepointWidth(0x30000)); // 𠀀 - // try testing.expectEqual(@as(i8, 1), @import("ziglyph").display_width.codePointWidth(0x100, .half)); + // try testing.expectEqual(@as(i8, 1), @import("uucode").get(.width, 0x100)); } // This is not very fast in debug modes, so its commented by default. // IMPORTANT: UNCOMMENT THIS WHENEVER MAKING CODEPOINTWIDTH CHANGES. -// test "codepointWidth matches ziglyph" { +// test "codepointWidth matches uucode" { // const testing = std.testing; -// const ziglyph = @import("ziglyph"); +// const uucode = @import("uucode"); // // const min = 0xFF + 1; // start outside ascii -// for (min..std.math.maxInt(u21)) |cp| { +// const max = std.math.maxInt(u21) + 1; +// for (min..max) |cp| { // const simd = codepointWidth(@intCast(cp)); -// const zg = ziglyph.display_width.codePointWidth(@intCast(cp), .half); -// if (simd != zg) mismatch: { +// const uu = if (cp > uucode.config.max_code_point) +// 1 +// else +// uucode.get(.width, @intCast(cp)); +// if (simd != uu) mismatch: { // if (cp == 0x2E3B) { // try testing.expectEqual(@as(i8, 2), simd); // break :mismatch; // } // -// std.log.warn("mismatch cp=U+{x} simd={} zg={}", .{ cp, simd, zg }); +// std.log.warn("mismatch cp=U+{x} simd={} uucode={}", .{ cp, simd, uu }); // try testing.expect(false); // } // } diff --git a/src/stb/stb_image.h b/src/stb/stb_image.h index 3ae1815c1..ed7791dff 100644 --- a/src/stb/stb_image.h +++ b/src/stb/stb_image.h @@ -6831,7 +6831,7 @@ static stbi_uc *stbi__gif_load_next(stbi__context *s, stbi__gif *g, int *comp, i // 0: not specified. } - // background is what out is after the undoing of the previou frame; + // background is what out is after the undoing of the previous frame; memcpy( g->background, g->out, 4 * g->w * g->h ); } diff --git a/src/synthetic/Bytes.zig b/src/synthetic/Bytes.zig index 8a8207ba9..40a94e0e3 100644 --- a/src/synthetic/Bytes.zig +++ b/src/synthetic/Bytes.zig @@ -27,27 +27,35 @@ pub fn generator(self: *Bytes) Generator { return .init(self, next); } -pub fn next(self: *Bytes, buf: []u8) Generator.Error![]const u8 { +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), - buf.len, + max_len, ); - const result = buf[0..len]; - self.rand.bytes(result); - if (self.alphabet) |alphabet| { - for (result) |*byte| byte.* = alphabet[byte.* % alphabet.len]; + var buf: [8]u8 = undefined; + var remaining = len; + while (remaining > 0) { + const data = buf[0..@min(remaining, buf.len)]; + self.rand.bytes(data); + if (self.alphabet) |alphabet| { + for (data) |*byte| byte.* = alphabet[byte.* % alphabet.len]; + } + try writer.writeAll(data); + remaining -= data.len; } - - return result; } test "bytes" { const testing = std.testing; 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; const gen = v.generator(); - const result = try gen.next(&buf); - try testing.expect(result.len > 0); + try gen.next(&writer, buf.len); + try testing.expectEqual(buf.len, writer.buffered().len); } diff --git a/src/synthetic/Generator.zig b/src/synthetic/Generator.zig index 7478a54c3..28929ecbe 100644 --- a/src/synthetic/Generator.zig +++ b/src/synthetic/Generator.zig @@ -6,27 +6,27 @@ const assert = std.debug.assert; /// For generators, this is the only error that is allowed to be /// returned by the next function. -pub const Error = error{NoSpaceLeft}; +pub const Error = error{WriteFailed}; /// The vtable for the generator. ptr: *anyopaque, -nextFn: *const fn (ptr: *anyopaque, buf: []u8) Error![]const u8, +nextFn: *const fn (ptr: *anyopaque, *std.Io.Writer, usize) Error!void, /// Create a new generator from a pointer and a function pointer. /// This usually is only called by generator implementations, not /// generator users. pub fn init( pointer: anytype, - comptime nextFn: fn (ptr: @TypeOf(pointer), buf: []u8) Error![]const u8, + comptime nextFn: fn (ptr: @TypeOf(pointer), *std.Io.Writer, usize) Error!void, ) Generator { const Ptr = @TypeOf(pointer); assert(@typeInfo(Ptr) == .pointer); // Must be a pointer assert(@typeInfo(Ptr).pointer.size == .one); // Must be a single-item pointer assert(@typeInfo(@typeInfo(Ptr).pointer.child) == .@"struct"); // Must point to a struct const gen = struct { - fn next(ptr: *anyopaque, buf: []u8) Error![]const u8 { + fn next(ptr: *anyopaque, writer: *std.Io.Writer, max_len: usize) Error!void { const self: Ptr = @ptrCast(@alignCast(ptr)); - return try nextFn(self, buf); + try nextFn(self, writer, max_len); } }; @@ -37,6 +37,6 @@ pub fn init( } /// Get the next value from the generator. Returns the data written. -pub fn next(self: Generator, buf: []u8) Error![]const u8 { - return try self.nextFn(self.ptr, buf); +pub fn next(self: Generator, writer: *std.Io.Writer, max_size: usize) Error!void { + try self.nextFn(self.ptr, writer, max_size); } diff --git a/src/synthetic/Osc.zig b/src/synthetic/Osc.zig index 8d5d7d3a2..52940fee9 100644 --- a/src/synthetic/Osc.zig +++ b/src/synthetic/Osc.zig @@ -53,6 +53,9 @@ pub fn generator(self: *Osc) Generator { return .init(self, next); } +const osc = std.fmt.comptimePrint("{c}]", .{std.ascii.control_code.esc}); +const st = std.fmt.comptimePrint("{c}", .{std.ascii.control_code.bel}); + /// Get the next OSC request in bytes. The generated OSC request will /// have the prefix `ESC ]` and the terminator `BEL` (0x07). /// @@ -63,23 +66,22 @@ pub fn generator(self: *Osc) Generator { /// /// The buffer must be at least 3 bytes long to accommodate the /// prefix and terminator. -pub fn next(self: *Osc, buf: []u8) Generator.Error![]const u8 { - if (buf.len < 3) return error.NoSpaceLeft; - const unwrapped = try self.nextUnwrapped(buf[2 .. buf.len - 1]); - buf[0] = 0x1B; // ESC - buf[1] = ']'; - buf[unwrapped.len + 2] = 0x07; // BEL - return buf[0 .. unwrapped.len + 3]; +pub fn next(self: *Osc, writer: *std.Io.Writer, max_len: usize) Generator.Error!void { + assert(max_len >= 3); + try writer.writeAll(osc); + try self.nextUnwrapped(writer, max_len - (osc.len + st.len)); + try writer.writeAll(st); } -fn nextUnwrapped(self: *Osc, buf: []u8) Generator.Error![]const u8 { +fn nextUnwrapped(self: *Osc, writer: *std.Io.Writer, max_len: usize) Generator.Error!void { return switch (self.chooseValidity()) { .valid => valid: { const Indexer = @TypeOf(self.p_valid_kind).Indexer; const idx = self.rand.weightedIndex(f64, &self.p_valid_kind.values); break :valid try self.nextUnwrappedValidExact( - buf, + writer, Indexer.keyForIndex(idx), + max_len, ); }, @@ -87,70 +89,64 @@ fn nextUnwrapped(self: *Osc, buf: []u8) Generator.Error![]const u8 { const Indexer = @TypeOf(self.p_invalid_kind).Indexer; const idx = self.rand.weightedIndex(f64, &self.p_invalid_kind.values); break :invalid try self.nextUnwrappedInvalidExact( - buf, + writer, Indexer.keyForIndex(idx), + max_len, ); }, }; } -fn nextUnwrappedValidExact(self: *const Osc, buf: []u8, k: ValidKind) Generator.Error![]const u8 { - var fbs = std.io.fixedBufferStream(buf); +fn nextUnwrappedValidExact(self: *const Osc, writer: *std.Io.Writer, k: ValidKind, max_len: usize) Generator.Error!void { switch (k) { .change_window_title => { - try fbs.writer().writeAll("0;"); // Set window title + try writer.writeAll("0;"); // Set window title var bytes_gen = self.bytes(); - const title = try bytes_gen.next(fbs.buffer[fbs.pos..]); - try fbs.seekBy(@intCast(title.len)); + try bytes_gen.next(writer, max_len - 2); }, .prompt_start => { - try fbs.writer().writeAll("133;A"); // Start prompt + try writer.writeAll("133;A"); // Start prompt // aid if (self.rand.boolean()) { var bytes_gen = self.bytes(); bytes_gen.max_len = 16; - try fbs.writer().writeAll(";aid="); - const aid = try bytes_gen.next(fbs.buffer[fbs.pos..]); - try fbs.seekBy(@intCast(aid.len)); + try writer.writeAll(";aid="); + try bytes_gen.next(writer, max_len); } // redraw if (self.rand.boolean()) { - try fbs.writer().writeAll(";redraw="); + try writer.writeAll(";redraw="); if (self.rand.boolean()) { - try fbs.writer().writeAll("1"); + try writer.writeAll("1"); } else { - try fbs.writer().writeAll("0"); + try writer.writeAll("0"); } } }, - .prompt_end => try fbs.writer().writeAll("133;B"), // End prompt + .prompt_end => try writer.writeAll("133;B"), // End prompt } - - return fbs.getWritten(); } fn nextUnwrappedInvalidExact( self: *const Osc, - buf: []u8, + writer: *std.Io.Writer, k: InvalidKind, -) Generator.Error![]const u8 { + max_len: usize, +) Generator.Error!void { switch (k) { .random => { var bytes_gen = self.bytes(); - return try bytes_gen.next(buf); + try bytes_gen.next(writer, max_len); }, .good_prefix => { - var fbs = std.io.fixedBufferStream(buf); - try fbs.writer().writeAll("133;"); + try writer.writeAll("133;"); var bytes_gen = self.bytes(); - const data = try bytes_gen.next(fbs.buffer[fbs.pos..]); - try fbs.seekBy(@intCast(data.len)); - return fbs.getWritten(); + try bytes_gen.next(writer, max_len - 4); }, } } @@ -177,11 +173,21 @@ const Validity = enum { valid, invalid }; const test_seed = 0xC0FFEEEEEEEEEEEE; test "OSC generator" { + const testing = std.testing; var prng = std.Random.DefaultPrng.init(test_seed); - var buf: [4096]u8 = undefined; - var v: Osc = .{ .rand = prng.random() }; - const gen = v.generator(); - for (0..50) |_| _ = try gen.next(&buf); + var buf: [256]u8 = undefined; + { + var v: Osc = .{ + .rand = prng.random(), + }; + const gen = v.generator(); + for (0..50) |_| { + var writer: std.Io.Writer = .fixed(&buf); + try gen.next(&writer, buf.len); + const result = writer.buffered(); + try testing.expect(result.len > 0); + } + } } test "OSC generator valid" { @@ -195,8 +201,10 @@ test "OSC generator valid" { .p_valid = 1.0, }; for (0..50) |_| { - const seq = try gen.next(&buf); - var parser: terminal.osc.Parser = .init(); + var writer: std.Io.Writer = .fixed(&buf); + try gen.next(&writer, buf.len); + const seq = writer.buffered(); + var parser: terminal.osc.Parser = .init(null); for (seq[2 .. seq.len - 1]) |c| parser.next(c); try testing.expect(parser.end(null) != null); } @@ -213,8 +221,10 @@ test "OSC generator invalid" { .p_valid = 0.0, }; for (0..50) |_| { - const seq = try gen.next(&buf); - var parser: terminal.osc.Parser = .init(); + var writer: std.Io.Writer = .fixed(&buf); + try gen.next(&writer, buf.len); + const seq = writer.buffered(); + var parser: terminal.osc.Parser = .init(null); for (seq[2 .. seq.len - 1]) |c| parser.next(c); try testing.expect(parser.end(null) == null); } diff --git a/src/synthetic/Utf8.zig b/src/synthetic/Utf8.zig index c3ace6505..0d72a8bb2 100644 --- a/src/synthetic/Utf8.zig +++ b/src/synthetic/Utf8.zig @@ -41,13 +41,12 @@ pub fn generator(self: *Utf8) Generator { return .init(self, next); } -pub fn next(self: *Utf8, buf: []u8) Generator.Error![]const u8 { +pub fn next(self: *Utf8, writer: *std.Io.Writer, max_len: usize) Generator.Error!void { const len = @min( self.rand.intRangeAtMostBiased(usize, self.min_len, self.max_len), - buf.len, + max_len, ); - const result = buf[0..len]; var rem: usize = len; while (rem > 0) { // Pick a utf8 byte count to generate. @@ -75,9 +74,11 @@ pub fn next(self: *Utf8, buf: []u8) Generator.Error![]const u8 { assert(std.unicode.utf8CodepointSequenceLength( cp, ) catch unreachable == @intFromEnum(utf8_len)); - rem -= std.unicode.utf8Encode( + + var buf: [4]u8 = undefined; + const l = std.unicode.utf8Encode( cp, - result[result.len - rem ..], + &buf, ) catch |err| switch (err) { // Impossible because our generation above is hardcoded to // produce a valid range. If not, a bug. @@ -86,18 +87,22 @@ pub fn next(self: *Utf8, buf: []u8) Generator.Error![]const u8 { // Possible, in which case we redo the loop and encode nothing. error.Utf8CannotEncodeSurrogateHalf => continue, }; + try writer.writeAll(buf[0..l]); + rem -= l; } - - return result; } test "utf8" { const testing = std.testing; var prng = std.Random.DefaultPrng.init(0); var buf: [256]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); var v: Utf8 = .{ .rand = prng.random() }; + v.min_len = buf.len; + v.max_len = buf.len; const gen = v.generator(); - const result = try gen.next(&buf); - try testing.expect(result.len > 0); + try gen.next(&writer, buf.len); + const result = writer.buffered(); + try testing.expectEqual(256, result.len); try testing.expect(std.unicode.utf8ValidateSlice(result)); } diff --git a/src/synthetic/cli.zig b/src/synthetic/cli.zig index 36832587c..d9b6a659d 100644 --- a/src/synthetic/cli.zig +++ b/src/synthetic/cli.zig @@ -90,12 +90,19 @@ fn mainActionImpl( const rand = prng.random(); // Our output always goes to stdout. - const writer = std.io.getStdOut().writer(); + var buffer: [2048]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&buffer); + const writer = &stdout_writer.interface; // Create our implementation const impl = try Impl.create(alloc, opts); defer impl.destroy(alloc); try impl.run(writer, rand); + + // Always flush + writer.flush() catch |err| switch (err) { + error.WriteFailed => return, + }; } test { diff --git a/src/synthetic/cli/Ascii.zig b/src/synthetic/cli/Ascii.zig index 25e5bb00b..b2d57fa88 100644 --- a/src/synthetic/cli/Ascii.zig +++ b/src/synthetic/cli/Ascii.zig @@ -23,7 +23,7 @@ pub fn destroy(self: *Ascii, alloc: Allocator) void { alloc.destroy(self); } -pub fn run(self: *Ascii, writer: anytype, rand: std.Random) !void { +pub fn run(self: *Ascii, writer: *std.Io.Writer, rand: std.Random) !void { _ = self; var gen: synthetic.Bytes = .{ @@ -31,14 +31,12 @@ pub fn run(self: *Ascii, writer: anytype, rand: std.Random) !void { .alphabet = synthetic.Bytes.Alphabet.ascii, }; - var buf: [1024]u8 = undefined; while (true) { - const data = try gen.next(&buf); - writer.writeAll(data) catch |err| { - const Error = error{ NoSpaceLeft, BrokenPipe } || @TypeOf(err); + gen.next(writer, 1024) catch |err| { + const Error = error{ WriteFailed, BrokenPipe } || @TypeOf(err); switch (@as(Error, err)) { error.BrokenPipe => return, // stdout closed - error.NoSpaceLeft => return, // fixed buffer full + error.WriteFailed => return, // fixed buffer full else => return err, } }; @@ -56,8 +54,6 @@ test Ascii { const rand = prng.random(); var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - const writer = fbs.writer(); - - try impl.run(writer, rand); + var writer: std.Io.Writer = .fixed(&buf); + try impl.run(&writer, rand); } diff --git a/src/synthetic/cli/Osc.zig b/src/synthetic/cli/Osc.zig index 4792cda6b..8250b81de 100644 --- a/src/synthetic/cli/Osc.zig +++ b/src/synthetic/cli/Osc.zig @@ -29,7 +29,7 @@ pub fn destroy(self: *Osc, alloc: Allocator) void { alloc.destroy(self); } -pub fn run(self: *Osc, writer: anytype, rand: std.Random) !void { +pub fn run(self: *Osc, writer: *std.Io.Writer, rand: std.Random) !void { var gen: synthetic.Osc = .{ .rand = rand, .p_valid = self.opts.@"p-valid", @@ -37,14 +37,11 @@ pub fn run(self: *Osc, writer: anytype, rand: std.Random) !void { var buf: [1024]u8 = undefined; while (true) { - const data = try gen.next(&buf); - writer.writeAll(data) catch |err| { - const Error = error{ NoSpaceLeft, BrokenPipe } || @TypeOf(err); - switch (@as(Error, err)) { - error.BrokenPipe => return, // stdout closed - error.NoSpaceLeft => return, // fixed buffer full - else => return err, - } + 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, }; } } @@ -60,8 +57,6 @@ test Osc { const rand = prng.random(); var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - const writer = fbs.writer(); - - try impl.run(writer, rand); + var writer: std.Io.Writer = .fixed(&buf); + try impl.run(&writer, rand); } diff --git a/src/synthetic/cli/Utf8.zig b/src/synthetic/cli/Utf8.zig index 28a11f891..635704755 100644 --- a/src/synthetic/cli/Utf8.zig +++ b/src/synthetic/cli/Utf8.zig @@ -23,21 +23,19 @@ pub fn destroy(self: *Utf8, alloc: Allocator) void { alloc.destroy(self); } -pub fn run(self: *Utf8, writer: anytype, rand: std.Random) !void { +pub fn run(self: *Utf8, writer: *std.Io.Writer, rand: std.Random) !void { _ = self; var gen: synthetic.Utf8 = .{ .rand = rand, }; - var buf: [1024]u8 = undefined; while (true) { - const data = try gen.next(&buf); - writer.writeAll(data) catch |err| { - const Error = error{ NoSpaceLeft, BrokenPipe } || @TypeOf(err); + gen.next(writer, 1024) catch |err| { + const Error = error{ WriteFailed, BrokenPipe } || @TypeOf(err); switch (@as(Error, err)) { error.BrokenPipe => return, // stdout closed - error.NoSpaceLeft => return, // fixed buffer full + error.WriteFailed => return, // fixed buffer full else => return err, } }; @@ -55,8 +53,6 @@ test Utf8 { const rand = prng.random(); var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - const writer = fbs.writer(); - - try impl.run(writer, rand); + var writer: std.Io.Writer = .fixed(&buf); + try impl.run(&writer, rand); } diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index b8e16dbf7..3aba29128 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -56,7 +56,7 @@ const std_size = Page.layout(std_capacity).total_size; /// allocator because we need memory that is zero-initialized and page-aligned. const PagePool = std.heap.MemoryPoolAligned( [std_size]u8, - std.heap.page_size_min, + .fromByteUnits(std.heap.page_size_min), ); /// List of pins, known as "tracked" pins. These are pins that are kept @@ -128,6 +128,10 @@ explicit_max_size: usize, /// and at least two pages for our algorithms. min_max_size: usize, +/// The total number of rows represented by this PageList. This is used +/// specifically for scrollbar information so we can have the total size. +total_rows: usize, + /// The list of tracked pins. These are kept up to date automatically. tracked_pins: PinSet, @@ -145,12 +149,35 @@ viewport: Viewport, /// never be access directly; use `viewport`. viewport_pin: *Pin, +/// The row offset from the top that the viewport pin is at. We +/// store the offset from the top because it doesn't change while more +/// data is printed to the terminal. +/// +/// This is null when it isn't calculated. It is calculated on demand +/// when the viewportRowOffset function is called, because it is only +/// required for certain operations such as rendering the scrollbar. +/// +/// In order to make this more efficient, in many places where the value +/// would be invalidated, we update it in-place instead. This is key to +/// keeping our performance decent in normal cases since recalculating +/// this from scratch, depending on the size of the scrollback and position +/// of the pin, can be very expensive. +/// +/// This is only valid if viewport is `pin`. Every other offset is +/// self-evident or quick to calculate. +viewport_pin_row_offset: ?usize, + /// The current desired screen dimensions. I say "desired" because individual /// pages may still be a different size and not yet reflowed since we lazily /// reflow text. cols: size.CellCountInt, rows: size.CellCountInt, +/// If this is true then verifyIntegrity will do nothing. This is +/// only present with runtime safety enabled. +pause_integrity_checks: if (build_options.slow_runtime_safety) usize else void = + if (build_options.slow_runtime_safety) 0 else {}, + /// The viewport location. pub const Viewport = union(enum) { /// The viewport is pinned to the active area. By using a specific marker @@ -249,7 +276,7 @@ pub fn init( errdefer tracked_pins.deinit(pool.alloc); try tracked_pins.putNoClobber(pool.alloc, viewport_pin, {}); - return .{ + const result: PageList = .{ .cols = cols, .rows = rows, .pool = pool, @@ -258,10 +285,14 @@ pub fn init( .page_size = page_size, .explicit_max_size = max_size orelse std.math.maxInt(usize), .min_max_size = min_max_size, + .total_rows = rows, .tracked_pins = tracked_pins, .viewport = .{ .active = {} }, .viewport_pin = viewport_pin, + .viewport_pin_row_offset = null, }; + result.assertIntegrity(); + return result; } fn initPages( @@ -306,9 +337,87 @@ fn initPages( return .{ page_list, page_size }; } +/// Assert that the PageList is in a valid state. This is a no-op in +/// release builds. +pub inline fn assertIntegrity(self: *const PageList) void { + if (comptime !build_options.slow_runtime_safety) return; + + self.verifyIntegrity() catch |err| { + log.err("PageList integrity check failed: {}", .{err}); + @panic("PageList integrity check failed"); + }; +} + +/// Pause or resume integrity checks. This is useful when you're doing +/// a multi-step operation that temporarily leaves the PageList in an +/// inconsistent state. +pub inline fn pauseIntegrityChecks(self: *PageList, pause: bool) void { + if (comptime !build_options.slow_runtime_safety) return; + if (pause) { + self.pause_integrity_checks += 1; + } else { + self.pause_integrity_checks -= 1; + } +} + +const IntegrityError = error{ + TotalRowsMismatch, + ViewportPinOffsetMismatch, +}; + +/// Verify the integrity of the PageList. This is expensive and should +/// only be called in debug/test builds. +fn verifyIntegrity(self: *const PageList) IntegrityError!void { + if (comptime !build_options.slow_runtime_safety) return; + if (self.pause_integrity_checks > 0) return; + + // Verify that our cached total_rows matches the actual row count + const actual_total = self.totalRows(); + if (actual_total != self.total_rows) { + log.warn( + "PageList integrity violation: total_rows mismatch cached={} actual={}", + .{ self.total_rows, actual_total }, + ); + 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; + const actual_offset: usize = offset: { + var offset: usize = 0; + var node = self.pages.last; + while (node) |n| : (node = n.prev) { + offset += n.data.size.rows; + if (n == self.viewport_pin.node) { + offset -= self.viewport_pin.y; + break :offset self.total_rows - offset; + } + } + + log.warn( + "PageList integrity violation: viewport pin not in list", + .{}, + ); + return error.ViewportPinOffsetMismatch; + }; + + if (cached_offset != actual_offset) { + log.warn( + "PageList integrity violation: viewport pin offset mismatch cached={} actual={}", + .{ cached_offset, actual_offset }, + ); + return error.ViewportPinOffsetMismatch; + } + } +} + /// Deinit the pagelist. If you own the memory pool (used clonePool) then /// this will reset the pool and retain capacity. pub fn deinit(self: *PageList) void { + // Verify integrity before cleanup + self.assertIntegrity(); + // Always deallocate our hashmap. self.tracked_pins.deinit(self.pool.alloc); @@ -339,6 +448,8 @@ pub fn deinit(self: *PageList) void { /// This can't fail because we always retain at least enough allocated /// memory to fit the active area. pub fn reset(self: *PageList) void { + defer self.assertIntegrity(); + // 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. @@ -388,11 +499,18 @@ pub fn reset(self: *PageList) void { const page_arena = &self.pool.pages.arena; var it = page_arena.state.buffer_list.first; while (it) |node| : (it = node.next) { - // The fully allocated buffer - const alloc_buf = @as([*]u8, @ptrCast(node))[0..node.data]; + // WARN: Since HeapAllocator's BufNode is not public API, + // we have to hardcode its layout here. We do a comptime assert + // on Zig version to verify we check it on every bump. + const BufNode = struct { + data: usize, + node: std.SinglyLinkedList.Node, + }; + const buf_node: *BufNode = @fieldParentPtr("node", node); + // The fully allocated buffer + const alloc_buf = @as([*]u8, @ptrCast(buf_node))[0..buf_node.data]; // The buffer minus our header - const BufNode = @TypeOf(page_arena.state.buffer_list).Node; const data_buf = alloc_buf[@sizeOf(BufNode)..]; @memset(data_buf, 0); } @@ -406,6 +524,9 @@ pub fn reset(self: *PageList) void { self.rows, ) catch @panic("initPages failed"); + // Our total rows always goes back to the default + self.total_rows = self.rows; + // Update all our tracked pins to point to our first page top-left { var it = self.tracked_pins.iterator(); @@ -563,9 +684,11 @@ pub fn clone( .min_max_size = self.min_max_size, .cols = self.cols, .rows = self.rows, + .total_rows = total_rows, .tracked_pins = tracked_pins, .viewport = .{ .active = {} }, .viewport_pin = viewport_pin, + .viewport_pin_row_offset = null, }; // We always need to have enough rows for our viewport because this is @@ -582,8 +705,12 @@ pub fn clone( const row = &last.data.rows.ptr(last.data.memory)[last.data.size.rows - 1]; last.data.clearCells(row, 0, result.cols); } + + // Update our total rows to be our row size. + result.total_rows = result.rows; } + result.assertIntegrity(); return result; } @@ -610,6 +737,8 @@ pub const Resize = struct { /// Resize /// TODO: docs pub fn resize(self: *PageList, opts: Resize) !void { + defer self.assertIntegrity(); + if (comptime std.debug.runtime_safety) { // Resize does not work with 0 values, this should be protected // upstream @@ -617,6 +746,12 @@ pub fn resize(self: *PageList, opts: Resize) !void { if (opts.rows) |v| assert(v > 0); } + // Resizing (especially with reflow) can cause our row offset to + // become invalid. Rather than do something fancy like we do other + // places and try to update it in place, we just invalidate it because + // its too easy to get the logic wrong in here. + self.viewport_pin_row_offset = null; + if (!opts.reflow) return try self.resizeWithoutReflow(opts); // Recalculate our minimum max size. This allows grow to work properly @@ -651,7 +786,6 @@ pub fn resize(self: *PageList, opts: Resize) !void { copy.cols = self.cols; break :opts copy; }); - try self.resizeCols(cols, opts.cursor); }, } @@ -721,16 +855,21 @@ fn resizeCols( self.pages.first = dst_node; self.pages.last = dst_node; - var dst_cursor = ReflowCursor.init(dst_node); - // Reflow all our rows. - while (it.next()) |row| { - try dst_cursor.reflowRow(self, row); + { + var dst_cursor = ReflowCursor.init(dst_node); + while (it.next()) |row| { + try dst_cursor.reflowRow(self, row); - // Once we're done reflowing a page, destroy it. - if (row.y == row.node.data.size.rows - 1) { - self.destroyNode(row.node); + // Once we're done reflowing a page, destroy it. + 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; } // If our total rows is less than our active rows, we need to grow. @@ -797,6 +936,9 @@ const ReflowCursor = struct { page_cell: *pagepkg.Cell, new_rows: usize, + /// This is the final row count of the reflowed pages. + total_rows: usize, + fn init(node: *List.Node) ReflowCursor { const page = &node.data; const rows = page.rows.ptr(page.memory); @@ -809,6 +951,9 @@ const ReflowCursor = struct { .page_row = &rows[0], .page_cell = &rows[0].cells.ptr(page.memory)[0], .new_rows = 0, + + // Initially whatever size our input node is. + .total_rows = node.data.size.rows, }; } @@ -1222,12 +1367,21 @@ const ReflowCursor = struct { ) !void { const old_x = self.x; const old_y = self.y; + const old_total_rows = self.total_rows; + + self.* = .init(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( + self.node, + adjustment, + ); + }); - self.* = .init(try list.adjustCapacity( - self.node, - adjustment, - )); self.cursorAbsolute(old_x, old_y); + self.total_rows = old_total_rows; } /// True if this cursor is at the bottom of the page by capacity, @@ -1246,11 +1400,6 @@ const ReflowCursor = struct { } } - fn cursorDown(self: *ReflowCursor) void { - assert(self.y + 1 < self.page.size.rows); - self.cursorAbsolute(self.x, self.y + 1); - } - /// Create a new row and move the cursor down. /// /// Asserts that the cursor is on the bottom row of the @@ -1302,6 +1451,12 @@ const ReflowCursor = struct { list: *PageList, cap: Capacity, ) !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. + const new_total_rows: usize = self.total_rows + 1; + defer self.total_rows = new_total_rows; + if (self.bottom()) { try self.cursorNewPage(list, cap); } else { @@ -1367,6 +1522,11 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { // destroy pages if we're increasing cols which will free up page_size // so that when we call grow() in the row mods, we won't prune. if (opts.cols) |cols| { + // Any column change without reflow should not result in row counts + // changing. + const old_total_rows = self.total_rows; + defer assert(self.total_rows == old_total_rows); + switch (std.math.order(cols, self.cols)) { .eq => {}, @@ -1435,7 +1595,10 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { // behavior because it seemed fine in an ocean of differing behavior // between terminal apps. I'm completely open to changing it as long // as resize behavior isn't regressed in a user-hostile way. - _ = self.trimTrailingBlankRows(self.rows - rows); + const trimmed = self.trimTrailingBlankRows(self.rows - rows); + + // Account for our trimmed rows in the total row cache + self.total_rows -= trimmed; // If we didn't trim enough, just modify our row count and this // will create additional history. @@ -1495,6 +1658,7 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { } if (build_options.slow_runtime_safety) { + // We never have less rows than our active screen has. assert(self.totalRows() >= self.rows); } } @@ -1666,6 +1830,10 @@ fn trailingBlankLines( /// Trims up to max trailing blank rows from the pagelist and returns the /// number of rows trimmed. A blank row is any row with no text (but may /// have styling). +/// +/// IMPORTANT: This function does NOT update `total_rows`. It returns the +/// number of rows trimmed, and the caller is responsible for decrementing +/// `total_rows` by this amount. fn trimTrailingBlankRows( self: *PageList, max: size.CellCountInt, @@ -1718,6 +1886,11 @@ pub const Scroll = union(enum) { /// the scrollback history. top, + /// Scroll to the given absolute row from the top. A value of zero + /// is the top row. This row will be the first visible row in the viewport. + /// Scrolling into or below the active area will clamp to the active area. + row: usize, + /// Scroll up (negative) or down (positive) by the given number of /// rows. This is clamped to the "top" and "active" top left. delta_row: isize, @@ -1736,22 +1909,171 @@ pub const Scroll = union(enum) { /// pages, etc. This can only be used to move the viewport within the /// previously allocated pages. pub fn scroll(self: *PageList, behavior: Scroll) void { + defer self.assertIntegrity(); + switch (behavior) { - .active => self.viewport = .{ .active = {} }, - .top => self.viewport = .{ .top = {} }, + .active => self.viewport = .active, + .top => self.viewport = .top, .pin => |p| { if (self.pinIsActive(p)) { - self.viewport = .{ .active = {} }; + self.viewport = .active; + return; + } else if (self.pinIsTop(p)) { + self.viewport = .top; return; } self.viewport_pin.* = p; - self.viewport = .{ .pin = {} }; + self.viewport = .pin; + self.viewport_pin_row_offset = null; // invalidate cache + }, + .row => |n| row: { + // If we're at the top, pin the top. + if (n == 0) { + self.viewport = .top; + break :row; + } + + // If we're below the top of the active area, pin the active area. + if (n >= self.total_rows - self.rows) { + self.viewport = .active; + break :row; + } + + // See if there are any other faster paths we can take. + switch (self.viewport) { + .top, .active => {}, + .pin => if (self.viewport_pin_row_offset) |*v| { + // If we have a pin and we already calculated a row offset, + // then we can efficiently calculate the delta and move + // that much from that pin. + const delta: isize = delta: { + const n_isize: isize = @intCast(n); + const v_isize: isize = @intCast(v.*); + break :delta n_isize - v_isize; + }; + self.scroll(.{ .delta_row = delta }); + return; + }, + } + + // We have an accurate row offset so store it to prevent + // calculating this again. + self.viewport_pin_row_offset = n; + self.viewport = .pin; + + // Slow path, we've just got to traverse the linked list and + // get to our row. As a slight speedup, let's pick the traversal + // that's likely faster based on our absolute row and total rows. + const midpoint = self.total_rows / 2; + if (n < midpoint) { + // Iterate forward from the first node. + var node_it = self.pages.first; + var rem: size.CellCountInt = std.math.cast( + size.CellCountInt, + n, + ) orelse { + self.viewport = .active; + break :row; + }; + while (node_it) |node| : (node_it = node.next) { + if (rem < node.data.size.rows) { + self.viewport_pin.* = .{ + .node = node, + .y = rem, + }; + break :row; + } + + rem -= node.data.size.rows; + } + } else { + // Iterate backwards from the last node. + var node_it = self.pages.last; + var rem: size.CellCountInt = std.math.cast( + size.CellCountInt, + self.total_rows - n, + ) orelse { + self.viewport = .active; + break :row; + }; + while (node_it) |node| : (node_it = node.prev) { + if (rem <= node.data.size.rows) { + self.viewport_pin.* = .{ + .node = node, + .y = node.data.size.rows - rem, + }; + break :row; + } + + rem -= node.data.size.rows; + } + } + + // If we reached here, then we couldn't find the offset. + // This feels impossible? Just clamp to active, screw it lol. + self.viewport = .active; }, .delta_prompt => |n| self.scrollPrompt(n), - .delta_row => |n| { - if (n == 0) return; + .delta_row => |n| delta_row: { + switch (self.viewport) { + // If we're at the top and we're scrolling backwards, + // we don't have to do anything, because there's nowhere to go. + .top => if (n <= 0) break :delta_row, + // If we're at active and we're scrolling forwards, we don't + // have to do anything because it'll result in staying in + // the active. + .active => if (n >= 0) break :delta_row, + + // If we're already a pin type, then we can fast-path our + // delta by simply moving the pin. This has the added benefit + // that we can update our row offset cache efficiently, too. + .pin => switch (std.math.order(n, 0)) { + .eq => break :delta_row, + + .lt => switch (self.viewport_pin.upOverflow(@intCast(-n))) { + .offset => |new_pin| { + self.viewport_pin.* = new_pin; + if (self.viewport_pin_row_offset) |*v| { + v.* -= @as(usize, @intCast(-n)); + } + break :delta_row; + }, + + // If we overflow up we're at the top. + .overflow => { + self.viewport = .top; + break :delta_row; + }, + }, + + .gt => switch (self.viewport_pin.downOverflow(@intCast(n))) { + // If we offset its a valid pin but we still have to + // check if we're in the active area. + .offset => |new_pin| { + if (self.pinIsActive(new_pin)) { + self.viewport = .active; + } else { + self.viewport_pin.* = new_pin; + if (self.viewport_pin_row_offset) |*v| { + v.* += @intCast(n); + } + } + break :delta_row; + }, + + // If we overflow down we're at active. + .overflow => { + self.viewport = .active; + break :delta_row; + }, + }, + }, + } + + // Slow path: we have to calculate the new pin by moving + // from our viewport. const top = self.getTopLeft(.viewport); const p: Pin = if (n < 0) switch (top.upOverflow(@intCast(-n))) { .offset => |v| v, @@ -1769,13 +2091,22 @@ pub fn scroll(self: *PageList, behavior: Scroll) void { // active area, you usually expect that the viewport will now // follow the active area. if (self.pinIsActive(p)) { - self.viewport = .{ .active = {} }; + self.viewport = .active; + return; + } + + // If we're at the top, then just set the top. This is a lot + // more efficient everywhere. We must check this after the + // active check above because we prefer active if they overlap. + if (self.pinIsTop(p)) { + self.viewport = .top; return; } // Pin is not active so we need to track it. self.viewport_pin.* = p; - self.viewport = .{ .pin = {} }; + self.viewport = .pin; + self.viewport_pin_row_offset = null; // invalidate cache }, } } @@ -1811,10 +2142,11 @@ fn scrollPrompt(self: *PageList, delta: isize) void { // into the active area. Otherwise, we scroll up to the pin. if (prompt_pin) |p| { if (self.pinIsActive(p)) { - self.viewport = .{ .active = {} }; + self.viewport = .active; } else { self.viewport_pin.* = p; - self.viewport = .{ .pin = {} }; + self.viewport = .pin; + self.viewport_pin_row_offset = null; // invalidate cache } } } @@ -1822,6 +2154,8 @@ 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 { + defer self.assertIntegrity(); + // Go through the active area backwards to find the first non-empty // row. We use this to determine how many rows to scroll up. const non_empty: usize = non_empty: { @@ -1849,6 +2183,146 @@ pub fn scrollClear(self: *PageList) !void { for (0..non_empty) |_| _ = try self.grow(); } +/// 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 { + /// Total size of the scrollable area. + total: usize, + + /// The offset into the total area that the viewport is at. This is + /// guaranteed to be less than or equal to total. This includes the + /// visible row. + offset: usize, + + /// The length of the visible area. This is including the offset row. + len: usize, + + /// A zero-sized scrollable region. + pub const zero: Scrollbar = .{ + .total = 0, + .offset = 0, + .len = 0, + }; + + // Sync with: ghostty_action_scrollbar_s + pub const C = extern struct { + total: u64, + offset: u64, + len: u64, + }; + + pub fn cval(self: Scrollbar) C { + return .{ + .total = @intCast(self.total), + .offset = @intCast(self.offset), + .len = @intCast(self.len), + }; + } + + /// Comparison for scrollbars. + pub fn eql(self: Scrollbar, other: Scrollbar) bool { + return self.total == other.total and + self.offset == other.offset and + self.len == other.len; + } +}; + +/// Return the scrollbar state for this PageList. +/// +/// This may be expensive to calculate depending on where the viewport +/// 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 { + return .{ + .total = self.total_rows, + .offset = self.viewportRowOffset(), + .len = self.rows, // Length is always rows + }; +} + +/// Returns the offset of the current viewport from the top of the +/// screen. +/// +/// This is potentially expensive to calculate because if the viewport +/// is a pin and the pin is near the beginning of the scrollback, we +/// will traverse a lot of linked list nodes. +/// +/// The result is cached so repeated calls are cheap. +fn viewportRowOffset(self: *PageList) usize { + return switch (self.viewport) { + .top => 0, + .active => self.total_rows - self.rows, + .pin => pin: { + // We assert integrity on this code path because it verifies + // that the cached value is correct. + defer self.assertIntegrity(); + + // Return cached value if available + if (self.viewport_pin_row_offset) |cached| break :pin cached; + + // Traverse from the end and count rows until we reach the + // viewport pin. We count backwards because most of the time + // a user is scrolling near the active area. + const top_offset: usize = offset: { + var offset: usize = 0; + var node = self.pages.last; + while (node) |n| : (node = n.prev) { + offset += n.data.size.rows; + if (n == self.viewport_pin.node) { + assert(n.data.size.rows > self.viewport_pin.y); + offset -= self.viewport_pin.y; + break :offset self.total_rows - offset; + } + } + + // Invalid pins are not possible. + unreachable; + }; + + // The offset is from the bottom and our cached value and this + // function returns from the top, so we need to invert it. + self.viewport_pin_row_offset = top_offset; + break :pin top_offset; + }, + }; +} + +/// This fixes up the viewport data when rows are removed from the +/// PageList. This will update a viewport to `active` if row removal +/// puts the viewport into the active area, to `top` if the viewport +/// is now at row 0, and updates any row offset caches as necessary. +/// +/// This is unit tested transitively through other tests such as +/// eraseRows. +fn fixupViewport( + self: *PageList, + removed: usize, +) void { + switch (self.viewport) { + .active => {}, + + // For pin, we check if our pin is now in the active area and if so + // we move our viewport back to the active area. + .pin => if (self.pinIsActive(self.viewport_pin.*)) { + self.viewport = .active; + } else if (self.viewport_pin_row_offset) |*v| { + // If we have a cached row offset, we need to update it + // to account for the erased rows. + if (v.* < removed) { + self.viewport = .top; + } else { + v.* -= removed; + } + }, + + // For top, we move back to active if our erasing moved our + // top page into the active area. + .top => if (self.pinIsActive(.{ .node = self.pages.first.? })) { + self.viewport = .active; + }, + } +} + /// Returns the actual max size. This may be greater than the explicit /// value if the explicit value is less than the min_max_size. /// @@ -1861,7 +2335,7 @@ pub fn maxSize(self: *const PageList) usize { } /// Returns true if we need to grow into our active area. -fn growRequiredForActive(self: *const PageList) bool { +inline fn growRequiredForActive(self: *const PageList) bool { var rows: usize = 0; var page = self.pages.last; while (page) |p| : (page = p.prev) { @@ -1880,11 +2354,17 @@ fn growRequiredForActive(self: *const PageList) bool { /// /// This returns the newly allocated page node if there is one. pub fn grow(self: *PageList) !?*List.Node { + defer self.assertIntegrity(); + const last = self.pages.last.?; if (last.data.capacity.rows > last.data.size.rows) { // Fast path: we have capacity in the last page. last.data.size.rows += 1; last.data.assertIntegrity(); + + // Increase our total rows by one + self.total_rows += 1; + return null; } @@ -1914,6 +2394,27 @@ pub fn grow(self: *PageList) !?*List.Node { 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. + self.total_rows -= first.data.size.rows; + self.total_rows += 1; + + // If we have a pin viewport cache then we need to update it. + if (self.viewport == .pin) viewport: { + if (self.viewport_pin_row_offset) |*v| { + // If our offset is less than the number of rows in the + // pruned page, then we are now at the top. + if (v.* < first.data.size.rows) { + self.viewport = .top; + break :viewport; + } + + // Otherwise, our viewport pin is below what we pruned + // so we just decrement our offset. + v.* -= first.data.size.rows; + } + } + // Initialize our new page and reinsert it as the last first.data = .initBuf(.init(buf), layout); first.data.size.rows = 1; @@ -1947,6 +2448,9 @@ pub fn grow(self: *PageList) !?*List.Node { // verified the case above. next_node.data.assertIntegrity(); + // Record the increased row count + self.total_rows += 1; + return next_node; } @@ -1969,7 +2473,7 @@ pub const AdjustCapacity = struct { pub const AdjustCapacityError = Allocator.Error || Page.CloneFromError; -/// Adjust the capcaity of the given page in the list. This should +/// 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. /// @@ -1990,6 +2494,7 @@ pub fn adjustCapacity( node: *List.Node, adjustment: AdjustCapacity, ) AdjustCapacityError!*List.Node { + defer self.assertIntegrity(); const page: *Page = &node.data; // We always start with the base capacity of the existing page. This @@ -2047,7 +2552,7 @@ pub fn adjustCapacity( /// Create a new page node. This does not add it to the list and this /// does not do any memory size accounting with max_size/page_size. -fn createPage( +inline fn createPage( self: *PageList, cap: Capacity, ) Allocator.Error!*List.Node { @@ -2055,7 +2560,7 @@ fn createPage( return try createPageExt(&self.pool, cap, &self.page_size); } -fn createPageExt( +inline fn createPageExt( pool: *MemoryPool, cap: Capacity, total_size: ?*usize, @@ -2075,7 +2580,7 @@ fn createPageExt( else try page_alloc.alignedAlloc( u8, - std.heap.page_size_min, + .fromByteUnits(std.heap.page_size_min), layout.total_size, ); errdefer if (pooled) @@ -2103,6 +2608,10 @@ fn createPageExt( /// Destroy the memory of the given node in the PageList linked list /// and return it to the pool. The node is assumed to already be removed /// from the linked list. +/// +/// IMPORTANT: This function does NOT update `total_rows`. The caller is +/// responsible for accounting for the removed rows. This function only +/// updates `page_size` (byte accounting), not row accounting. fn destroyNode(self: *PageList, node: *List.Node) void { destroyNodeExt(&self.pool, node, &self.page_size); } @@ -2140,6 +2649,7 @@ pub fn eraseRow( self: *PageList, pt: point.Point, ) !void { + defer self.assertIntegrity(); const pn = self.pin(pt).?; var node = pn.node; @@ -2159,6 +2669,9 @@ pub fn eraseRow( } } + // If we have a pinned viewport, we need to adjust for active area. + self.fixupViewport(1); + { // Set all the rows as dirty in this page var dirty = node.data.dirtyBitSet(); @@ -2229,6 +2742,8 @@ pub fn eraseRowBounded( pt: point.Point, limit: usize, ) !void { + defer self.assertIntegrity(); + // This function has a lot of repeated code in it because it is a hot path. // // To get a better idea of what's happening, read eraseRow first for more @@ -2251,6 +2766,21 @@ pub fn eraseRowBounded( var dirty = node.data.dirtyBitSet(); dirty.setRangeValue(.{ .start = pn.y, .end = pn.y + limit }, true); + // If our viewport is a pin and our pin is within the erased + // region we need to maybe shift our cache up. We do this here instead + // of in the pin loop below because its unlikely to be true and we + // don't want to run the conditional N times. + if (self.viewport == .pin) viewport: { + if (self.viewport_pin_row_offset) |*v| { + const p = self.viewport_pin; + if (p.node != node or + p.y < pn.y or + p.y > pn.y + limit or + p.y == 0) break :viewport; + v.* -= 1; + } + } + // Update pins in the shifted region. const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { @@ -2284,6 +2814,18 @@ pub fn eraseRowBounded( // Update tracked pins. { + // See the other places we do something similar in this function + // for a detailed explanation. + if (self.viewport == .pin) viewport: { + if (self.viewport_pin_row_offset) |*v| { + const p = self.viewport_pin; + if (p.node != node or + p.y < pn.y or + p.y == 0) break :viewport; + v.* -= 1; + } + } + const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { if (p.node == node and p.y >= pn.y) { @@ -2322,6 +2864,17 @@ pub fn eraseRowBounded( var dirty = node.data.dirtyBitSet(); dirty.setRangeValue(.{ .start = 0, .end = shifted_limit }, true); + // See the other places we do something similar in this function + // for a detailed explanation. + if (self.viewport == .pin) viewport: { + if (self.viewport_pin_row_offset) |*v| { + const p = self.viewport_pin; + if (p.node != node or + p.y > shifted_limit) break :viewport; + v.* -= 1; + } + } + // Update pins in the shifted region. const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { @@ -2346,6 +2899,16 @@ pub fn eraseRowBounded( // Account for the rows shifted in this node. shifted += node.data.size.rows; + // See the other places we do something similar in this function + // for a detailed explanation. + if (self.viewport == .pin) viewport: { + if (self.viewport_pin_row_offset) |*v| { + const p = self.viewport_pin; + if (p.node != node) break :viewport; + v.* -= 1; + } + } + // Update tracked pins. const pin_keys = self.tracked_pins.keys(); for (pin_keys) |p| { @@ -2374,6 +2937,8 @@ pub fn eraseRows( tl_pt: point.Point, bl_pt: ?point.Point, ) void { + defer self.assertIntegrity(); + // The count of rows that was erased. var erased: usize = 0; @@ -2452,6 +3017,9 @@ pub fn eraseRows( dirty.setRangeValue(.{ .start = 0, .end = chunk.node.data.size.rows }, true); } + // Update our total row count + self.total_rows -= erased; + // If we deleted active, we need to regrow because one of our invariants // is that we always have full active space. if (tl_pt == .active) { @@ -2466,26 +3034,16 @@ pub fn eraseRows( } // If we have a pinned viewport, we need to adjust for active area. - switch (self.viewport) { - .active => {}, - - // For pin, we check if our pin is now in the active area and if so - // we move our viewport back to the active area. - .pin => if (self.pinIsActive(self.viewport_pin.*)) { - self.viewport = .{ .active = {} }; - }, - - // For top, we move back to active if our erasing moved our - // top page into the active area. - .top => if (self.pinIsActive(.{ .node = self.pages.first.? })) { - self.viewport = .{ .active = {} }; - }, - } + self.fixupViewport(erased); } /// Erase a single page, freeing all its resources. The page can be /// anywhere in the linked list but must NOT be the final page in the /// entire list (i.e. must not make the list empty). +/// +/// IMPORTANT: This function does NOT update `total_rows`. The caller is +/// responsible for accounting for the removed rows before or after calling +/// this function. fn erasePage(self: *PageList, node: *List.Node) void { assert(node.next != null or node.prev != null); @@ -2594,6 +3152,11 @@ fn pinIsActive(self: *const PageList, p: Pin) bool { return false; } +/// Returns true if the pin is at the top of the scrollback area. +fn pinIsTop(self: *const PageList, p: Pin) bool { + return p.y == 0 and p.node == self.pages.first.?; +} + /// Convert a pin to a point in the given context. If the pin can't fit /// within the given tag (i.e. its in the history but you requested active), /// then this will return null. @@ -2676,7 +3239,7 @@ pub const EncodeUtf8Options = struct { /// predates this and is a thin wrapper around it so the tests all live there. pub fn encodeUtf8( self: *const PageList, - writer: anytype, + writer: *std.Io.Writer, opts: EncodeUtf8Options, ) anyerror!void { // We don't currently use self at all. There is an argument that this @@ -2716,7 +3279,7 @@ pub fn encodeUtf8( /// 1 | etc.| | 4 /// +-----+ : /// +--------+ -pub fn diagram(self: *const PageList, writer: anytype) !void { +pub fn diagram(self: *const PageList, writer: *std.Io.Writer) !void { const active_pin = self.getTopLeft(.active); var active = false; @@ -3334,23 +3897,9 @@ 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. +/// This is only used for testing so it isn't optimized in any way. fn growRows(self: *PageList, n: usize) !void { - var page = self.pages.last.?; - var n_rem: usize = n; - if (page.data.size.rows < page.data.capacity.rows) { - const add = @min(n_rem, page.data.capacity.rows - page.data.size.rows); - page.data.size.rows += add; - if (n_rem == add) return; - n_rem -= add; - } - - while (n_rem > 0) { - page = (try self.grow()).?; - const add = @min(n_rem, page.data.capacity.rows); - page.data.size.rows = add; - n_rem -= add; - } + for (0..n) |_| _ = try self.grow(); } /// Clear all dirty bits on all pages. This is not efficient since it @@ -3394,7 +3943,7 @@ pub const Pin = struct { y: size.CellCountInt = 0, x: size.CellCountInt = 0, - pub fn rowAndCell(self: Pin) struct { + pub inline fn rowAndCell(self: Pin) struct { row: *pagepkg.Row, cell: *pagepkg.Cell, } { @@ -3407,7 +3956,7 @@ pub const Pin = struct { /// Returns the cells for the row that this pin is on. The subset determines /// what subset of the cells are returned. The "left/right" subsets are /// inclusive of the x coordinate of the pin. - pub fn cells(self: Pin, subset: CellSubset) []pagepkg.Cell { + pub inline fn cells(self: Pin, subset: CellSubset) []pagepkg.Cell { const rac = self.rowAndCell(); const all = self.node.data.getCells(rac.row); return switch (subset) { @@ -3419,12 +3968,12 @@ pub const Pin = struct { /// Returns the grapheme codepoints for the given cell. These are only /// the EXTRA codepoints and not the first codepoint. - pub fn grapheme(self: Pin, cell: *const pagepkg.Cell) ?[]u21 { + pub inline fn grapheme(self: Pin, cell: *const pagepkg.Cell) ?[]u21 { return self.node.data.lookupGrapheme(cell); } /// Returns the style for the given cell in this pin. - pub fn style(self: Pin, cell: *const pagepkg.Cell) stylepkg.Style { + pub inline fn style(self: Pin, cell: *const pagepkg.Cell) stylepkg.Style { if (cell.style_id == stylepkg.default_id) return .{}; return self.node.data.styles.get( self.node.data.memory, @@ -3433,12 +3982,12 @@ pub const Pin = struct { } /// Check if this pin is dirty. - pub fn isDirty(self: Pin) bool { + pub inline fn isDirty(self: Pin) bool { return self.node.data.isRowDirty(self.y); } /// Mark this pin location as dirty. - pub fn markDirty(self: Pin) void { + pub inline fn markDirty(self: Pin) void { var set = self.node.data.dirtyBitSet(); set.set(self.y); } @@ -3507,7 +4056,7 @@ pub const Pin = struct { /// pointFromPin and building up the iterator from points. /// /// The limit pin is inclusive. - pub fn pageIterator( + pub inline fn pageIterator( self: Pin, direction: Direction, limit: ?Pin, @@ -3529,7 +4078,7 @@ pub const Pin = struct { }; } - pub fn rowIterator( + pub inline fn rowIterator( self: Pin, direction: Direction, limit: ?Pin, @@ -3546,7 +4095,7 @@ pub const Pin = struct { }; } - pub fn cellIterator( + pub inline fn cellIterator( self: Pin, direction: Direction, limit: ?Pin, @@ -3647,14 +4196,14 @@ pub const Pin = struct { return false; } - pub fn eql(self: Pin, other: Pin) bool { + pub inline fn eql(self: Pin, other: Pin) bool { return self.node == other.node and self.y == other.y and self.x == other.x; } /// Move the pin left n columns. n must fit within the size. - pub fn left(self: Pin, n: usize) Pin { + pub inline fn left(self: Pin, n: usize) Pin { assert(n <= self.x); var result = self; result.x -= std.math.cast(size.CellCountInt, n) orelse result.x; @@ -3662,7 +4211,7 @@ pub const Pin = struct { } /// Move the pin right n columns. n must fit within the size. - pub fn right(self: Pin, n: usize) Pin { + pub inline fn right(self: Pin, n: usize) Pin { assert(self.x + n < self.node.data.size.cols); var result = self; result.x +|= std.math.cast(size.CellCountInt, n) orelse @@ -3671,14 +4220,14 @@ pub const Pin = struct { } /// Move the pin left n columns, stopping at the start of the row. - pub fn leftClamp(self: Pin, n: size.CellCountInt) Pin { + pub inline fn leftClamp(self: Pin, n: size.CellCountInt) Pin { var result = self; result.x -|= n; return result; } /// Move the pin right n columns, stopping at the end of the row. - pub fn rightClamp(self: Pin, n: size.CellCountInt) Pin { + pub inline fn rightClamp(self: Pin, n: size.CellCountInt) Pin { var result = self; result.x = @min(self.x +| n, self.node.data.size.cols - 1); return result; @@ -3740,7 +4289,7 @@ pub const Pin = struct { /// Move the pin down a certain number of rows, or return null if /// the pin goes beyond the end of the screen. - pub fn down(self: Pin, n: usize) ?Pin { + pub inline fn down(self: Pin, n: usize) ?Pin { return switch (self.downOverflow(n)) { .offset => |v| v, .overflow => null, @@ -3749,7 +4298,7 @@ pub const Pin = struct { /// Move the pin up a certain number of rows, or return null if /// the pin goes beyond the start of the screen. - pub fn up(self: Pin, n: usize) ?Pin { + pub inline fn up(self: Pin, n: usize) ?Pin { return switch (self.upOverflow(n)) { .offset => |v| v, .overflow => null, @@ -3889,6 +4438,9 @@ test "PageList" { try testing.expect(s.pages.first != null); try testing.expectEqual(@as(usize, s.rows), s.totalRows()); + // Initial total rows should be our row count + try testing.expectEqual(s.rows, s.total_rows); + // Our viewport pin must be defined. It isn't used until the // viewport is a pin but it prevents undefined access on clone. try testing.expect(s.viewport_pin.node == s.pages.first.?); @@ -3899,6 +4451,13 @@ test "PageList" { .y = 0, .x = 0, }, s.getTopLeft(.active)); + + // Scrollbar should be where we expect it + try testing.expectEqual(Scrollbar{ + .total = s.rows, + .offset = 0, + .len = s.rows, + }, s.scrollbar()); } test "PageList init rows across two pages" { @@ -3922,6 +4481,16 @@ test "PageList init rows across two pages" { try testing.expect(s.viewport == .active); try testing.expect(s.pages.first != null); try testing.expectEqual(@as(usize, s.rows), s.totalRows()); + + // 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" { @@ -4109,6 +4678,13 @@ test "PageList active after grow" { .y = 10, } }, pt); } + + // Scrollbar should be in the active area + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 10, + .len = s.rows, + }, s.scrollbar()); } test "PageList grow allows exceeding max size for active area" { @@ -4134,6 +4710,9 @@ test "PageList grow allows exceeding max size for active area" { page.data.size.rows = 1; page.data.capacity.rows = 1; } + + // Avoid integrity check failures + s.total_rows = s.totalRows(); } // Grow our row and ensure we don't prune pages because we need @@ -4185,6 +4764,13 @@ test "PageList grow prune required with a single page" { const new = try s.grow(); try testing.expect(new != null); try testing.expect(new != s.pages.first); + + // Scrollbar should be in the active area + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); } test "PageList scroll top" { @@ -4213,6 +4799,12 @@ test "PageList scroll top" { } }, pt); } + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 0, + .len = s.rows, + }, s.scrollbar()); + try s.growRows(10); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); @@ -4222,6 +4814,12 @@ test "PageList scroll top" { } }, pt); } + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 0, + .len = s.rows, + }, s.scrollbar()); + s.scroll(.{ .active = {} }); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); @@ -4230,6 +4828,12 @@ test "PageList scroll top" { .y = 20, } }, pt); } + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); } test "PageList scroll delta row back" { @@ -4250,6 +4854,12 @@ test "PageList scroll delta row back" { s.scroll(.{ .delta_row = -1 }); + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = s.total_rows - s.rows - 1, + .len = s.rows, + }, s.scrollbar()); + { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ @@ -4266,6 +4876,20 @@ test "PageList scroll delta row back" { .y = 9, } }, pt); } + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = s.total_rows - s.rows - 11, + .len = s.rows, + }, s.scrollbar()); + + s.scroll(.{ .delta_row = -1 }); + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = s.total_rows - s.rows - 12, + .len = s.rows, + }, s.scrollbar()); } test "PageList scroll delta row back overflow" { @@ -4294,6 +4918,12 @@ test "PageList scroll delta row back overflow" { } }, pt); } + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 0, + .len = s.rows, + }, s.scrollbar()); + try s.growRows(10); { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); @@ -4302,6 +4932,12 @@ test "PageList scroll delta row back overflow" { .y = 0, } }, pt); } + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 0, + .len = s.rows, + }, s.scrollbar()); } test "PageList scroll delta row forward" { @@ -4323,6 +4959,12 @@ test "PageList scroll delta row forward" { s.scroll(.{ .top = {} }); s.scroll(.{ .delta_row = 2 }); + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 2, + .len = s.rows, + }, s.scrollbar()); + { const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); try testing.expectEqual(point.Point{ .screen = .{ @@ -4339,6 +4981,12 @@ test "PageList scroll delta row forward" { .y = 2, } }, pt); } + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 2, + .len = s.rows, + }, s.scrollbar()); } test "PageList scroll delta row forward into active" { @@ -4357,6 +5005,12 @@ test "PageList scroll delta row forward into active" { .y = 0, } }, pt); } + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); } test "PageList scroll delta row back without space preserves active" { @@ -4376,6 +5030,538 @@ test "PageList scroll delta row back without space preserves active" { } try testing.expect(s.viewport == .active); + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList scroll to pin" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(10); + + s.scroll(.{ .pin = s.pin(.{ .screen = .{ + .y = 4, + .x = 2, + } }).? }); + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 4, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 4, + } }, pt); + } + + s.scroll(.{ .pin = s.pin(.{ .screen = .{ + .y = 5, + .x = 2, + } }).? }); + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 5, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 5, + } }, pt); + } +} + +test "PageList scroll to pin in active" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(10); + + s.scroll(.{ .pin = s.pin(.{ .screen = .{ + .y = 30, + .x = 2, + } }).? }); + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 10, + } }, pt); + } +} + +test "PageList scroll to pin at top" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(10); + + s.scroll(.{ .pin = s.pin(.{ .screen = .{ + .y = 0, + .x = 2, + } }).? }); + + try testing.expect(s.viewport == .top); + + try testing.expectEqual(Scrollbar{ + .total = s.totalRows(), + .offset = 0, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 0, + } }, pt); + } +} + +test "PageList scroll to row 0" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(10); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 10, + } }, pt); + } + + s.scroll(.{ .row = 0 }); + try testing.expect(s.viewport == .top); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 0, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 0, + .len = s.rows, + }, s.scrollbar()); + + try s.growRows(10); + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 0, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 0, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList scroll to row in scrollback" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(20); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 20, + } }, pt); + } + + s.scroll(.{ .row = 5 }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 5, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 5, + } }, pt); + } + + try s.growRows(10); + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 5, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 5, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList scroll to row in middle" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(50); + + const total = s.total_rows; + const midpoint = total / 2; + s.scroll(.{ .row = midpoint }); + + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = midpoint, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = @as(size.CellCountInt, @intCast(midpoint)), + } }, pt); + } + + try s.growRows(10); + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = @as(size.CellCountInt, @intCast(midpoint)), + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = midpoint, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList scroll to row at active boundary" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(20); + + const active_start = s.total_rows - s.rows; + + s.scroll(.{ .row = active_start }); + + try testing.expect(s.viewport == .active); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = @as(size.CellCountInt, @intCast(active_start)), + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); + + try s.growRows(10); + + try testing.expect(s.viewport == .active); + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList scroll to row beyond active" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(10); + + s.scroll(.{ .row = 1000 }); + + try testing.expect(s.viewport == .active); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 10, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList scroll to row without scrollback" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + s.scroll(.{ .row = 5 }); + + try testing.expect(s.viewport == .active); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 0, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList scroll to row then delta" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(30); + + s.scroll(.{ .row = 10 }); + + try testing.expect(s.viewport == .pin); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 10, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 10, + .len = s.rows, + }, s.scrollbar()); + + s.scroll(.{ .delta_row = 5 }); + + try testing.expect(s.viewport == .pin); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 15, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 15, + .len = s.rows, + }, s.scrollbar()); + + s.scroll(.{ .delta_row = -3 }); + + try testing.expect(s.viewport == .pin); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 12, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 12, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList scroll to row with cache fast path down" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(50); + + s.scroll(.{ .row = 10 }); + + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 10, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 10, + } }, pt); + } + + // Verify cache is populated + try testing.expect(s.viewport_pin_row_offset != null); + try testing.expectEqual(@as(usize, 10), s.viewport_pin_row_offset.?); + + // Now scroll to a different row - this should use the fast path + s.scroll(.{ .row = 20 }); + + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 20, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 20, + } }, pt); + } + + try s.growRows(10); + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 20, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 20, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList scroll to row with cache fast path up" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try s.growRows(50); + + s.scroll(.{ .row = 30 }); + + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 30, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 30, + } }, pt); + } + + // Verify cache is populated + try testing.expect(s.viewport_pin_row_offset != null); + try testing.expectEqual(@as(usize, 30), s.viewport_pin_row_offset.?); + + // Now scroll up to a different row - this should use the fast path + s.scroll(.{ .row = 15 }); + + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 15, + .len = s.rows, + }, s.scrollbar()); + + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 15, + } }, pt); + } + + try s.growRows(10); + { + const pt = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 15, + } }, pt); + } + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 15, + .len = s.rows, + }, s.scrollbar()); } test "PageList scroll clear" { @@ -4411,7 +5597,7 @@ test "PageList scroll clear" { } } -test "PageList: jump zero" { +test "PageList: jump zero prompts" { const testing = std.testing; const alloc = testing.allocator; @@ -4431,9 +5617,15 @@ test "PageList: jump zero" { s.scroll(.{ .delta_prompt = 0 }); try testing.expect(s.viewport == .active); + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); } -test "Screen: jump to prompt" { +test "Screen: jump back one prompt" { const testing = std.testing; const alloc = testing.allocator; @@ -4459,6 +5651,12 @@ test "Screen: jump to prompt" { .x = 0, .y = 1, } }, s.pointFromPin(.screen, s.pin(.{ .viewport = .{} }).?).?); + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 1, + .len = s.rows, + }, s.scrollbar()); } { s.scroll(.{ .delta_prompt = -1 }); @@ -4467,16 +5665,32 @@ test "Screen: jump to prompt" { .x = 0, .y = 1, } }, s.pointFromPin(.screen, s.pin(.{ .viewport = .{} }).?).?); + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 1, + .len = s.rows, + }, s.scrollbar()); } // Jump forward { s.scroll(.{ .delta_prompt = 1 }); try testing.expect(s.viewport == .active); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); } { s.scroll(.{ .delta_prompt = 1 }); try testing.expect(s.viewport == .active); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = s.total_rows - s.rows, + .len = s.rows, + }, s.scrollbar()); } } @@ -4560,6 +5774,15 @@ test "PageList grow prune scrollback" { defer s.untrackPin(p); try testing.expect(p.node == s.pages.first.?); + // Scroll back to create a pinned viewport (not active) + const pin_y = page1.capacity.rows / 2; + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = pin_y } }).? }); + try testing.expect(s.viewport == .pin); + + // Get the scrollbar state to populate the cache + const scrollbar_before = s.scrollbar(); + try testing.expectEqual(pin_y, scrollbar_before.offset); + // Next should create a new page, but it should reuse our first // page since we're at max size. const new = (try s.grow()).?; @@ -4574,6 +5797,330 @@ test "PageList grow prune scrollback" { try testing.expect(p.node == s.pages.first.?); try testing.expect(p.x == 0); try testing.expect(p.y == 0); + + // Verify the viewport offset cache was invalidated. After pruning, + // the offset should have changed because we removed rows from + // the beginning. + { + const scrollbar_after = s.scrollbar(); + const rows_pruned = page1.capacity.rows; + const expected_offset = if (pin_y >= rows_pruned) + pin_y - rows_pruned + else + 0; + try testing.expectEqual(expected_offset, scrollbar_after.offset); + } +} + +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); + defer s.deinit(); + + // Grow to capacity of first page + const page1_node = s.pages.last.?; + const page1 = page1_node.data; + for (0..page1.capacity.rows - page1.size.rows) |_| { + try testing.expect(try s.grow() == null); + } + + // Grow and allocate second page, then fill it up + const page2_node = (try s.grow()).?; + const page2 = page2_node.data; + for (0..page2.capacity.rows - page2.size.rows) |_| { + try testing.expect(try s.grow() == null); + } + + // Get our page size + const old_page_size = s.page_size; + + // Scroll back to create a pinned viewport in page2 (NOT page1) + // This is the key difference from the previous test - the viewport + // pin is NOT in the page that will be pruned. + const pin_y = page1.capacity.rows + 5; + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = pin_y } }).? }); + try testing.expect(s.viewport == .pin); + try testing.expect(s.viewport_pin.node == page2_node); + + // Get the scrollbar state to populate the cache + const scrollbar_before = s.scrollbar(); + try testing.expectEqual(pin_y, scrollbar_before.offset); + + // Next grow will trigger pruning of the first page. + // The viewport_pin.node is page2, not page1, so it won't be moved + // by the pin update loop, but the cached offset still needs to be + // invalidated because rows were removed from the beginning. + const new = (try s.grow()).?; + try testing.expect(s.pages.last.? == new); + try testing.expectEqual(s.page_size, old_page_size); + + // Our first should now be page2 (page1 was pruned) + try testing.expectEqual(page2_node, s.pages.first.?); + + // The viewport pin should still be on page2, unchanged + try testing.expect(s.viewport_pin.node == page2_node); + + // Verify the viewport offset cache was invalidated/updated. + // After pruning, the offset should have decreased by the number + // of rows that were pruned. + const scrollbar_after = s.scrollbar(); + const rows_pruned = page1.capacity.rows; + const expected_offset = pin_y - rows_pruned; + try testing.expectEqual(expected_offset, scrollbar_after.offset); +} + +test "PageList eraseRows invalidates viewport offset cache" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + // Grow so we take up several pages worth of history + const page = &s.pages.last.?.data; + { + var cur_page = s.pages.last.?; + for (0..page.capacity.rows * 3) |_| { + if (try s.grow()) |new_page| cur_page = new_page; + } + } + + // Scroll back to create a pinned viewport somewhere in the middle + // of the scrollback + const pin_y = page.capacity.rows; + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = pin_y } }).? }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y, + .len = s.rows, + }, s.scrollbar()); + + // Erase some history rows BEFORE the viewport pin. + // This removes rows from before our pin, which changes its absolute + // offset from the top, but the cache is not invalidated. + const rows_to_erase = page.capacity.rows / 2; + s.eraseRows( + .{ .history = .{} }, + .{ .history = .{ .y = rows_to_erase - 1 } }, + ); + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y - rows_to_erase, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList eraseRow invalidates viewport offset cache" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + // Grow so we take up several pages worth of history + const page = &s.pages.last.?.data; + { + var cur_page = s.pages.last.?; + for (0..page.capacity.rows * 3) |_| { + if (try s.grow()) |new_page| cur_page = new_page; + } + } + + // Scroll back to create a pinned viewport somewhere in the middle + // of the scrollback + const pin_y = page.capacity.rows; + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = pin_y } }).? }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y, + .len = s.rows, + }, s.scrollbar()); + + // Erase a single row from the history BEFORE the viewport pin. + // This removes one row from before our pin, which changes its absolute + // offset from the top by 1, but the cache is not invalidated. + try s.eraseRow(.{ .history = .{ .y = 0 } }); + + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y - 1, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList eraseRowBounded invalidates viewport offset cache" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + // Grow so we take up several pages worth of history + const page = &s.pages.last.?.data; + { + var cur_page = s.pages.last.?; + for (0..page.capacity.rows * 3) |_| { + if (try s.grow()) |new_page| cur_page = new_page; + } + } + + // Scroll back to create a pinned viewport somewhere in the middle + // of the scrollback + const pin_y: u16 = 4; + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = pin_y } }).? }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y, + .len = s.rows, + }, s.scrollbar()); + + // Erase a row from the history BEFORE the viewport pin with a bounded + // shift. This removes one row from before our pin, which changes its + // absolute offset from the top by 1, but the cache is not invalidated. + try s.eraseRowBounded(.{ .history = .{ .y = 0 } }, 10); + + // Verify the scrollbar reflects the change (offset decreased by 1) + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y - 1, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList eraseRowBounded multi-page invalidates viewport offset cache" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + // Grow so we take up several pages worth of history + const page = &s.pages.last.?.data; + { + var cur_page = s.pages.last.?; + for (0..page.capacity.rows * 3) |_| { + if (try s.grow()) |new_page| cur_page = new_page; + } + } + + // Scroll back to create a pinned viewport somewhere in the middle + // of the scrollback, after the first page + const pin_y = page.capacity.rows + 1; + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = pin_y } }).? }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y, + .len = s.rows, + }, s.scrollbar()); + + // Erase a row from the beginning of history with a limit that spans + // across multiple pages. This ensures we hit the code path where + // eraseRowBounded finds the limit boundary in a subsequent page. + const limit = page.capacity.rows + 10; + try s.eraseRowBounded(.{ .history = .{ .y = 0 } }, limit); + + // Verify the scrollbar reflects the change (offset decreased by 1) + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y - 1, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList eraseRowBounded full page shift invalidates viewport offset cache" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + // Grow so we take up several pages worth of history + const page = &s.pages.last.?.data; + { + var cur_page = s.pages.last.?; + for (0..page.capacity.rows * 4) |_| { + if (try s.grow()) |new_page| cur_page = new_page; + } + } + + // Scroll back to create a pinned viewport somewhere well beyond + // the first two pages + const pin_y = 5; + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = pin_y } }).? }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y, + .len = s.rows, + }, s.scrollbar()); + + // Erase a row from the beginning of history with a limit that is + // larger than multiple full pages. This ensures we hit the code path + // where eraseRowBounded continues looping through entire pages, + // rotating all rows in each page until it reaches the limit or + // runs out of pages. + const limit = page.capacity.rows * 2 + 10; + try s.eraseRowBounded(.{ .history = .{ .y = 0 } }, limit); + + // Verify the scrollbar reflects the change (offset decreased by 1) + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y - 1, + .len = s.rows, + }, s.scrollbar()); +} + +test "PageList eraseRowBounded exhausts pages invalidates viewport offset cache" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + // Grow so we take up several pages worth of history + const page = &s.pages.last.?.data; + { + var cur_page = s.pages.last.?; + for (0..page.capacity.rows * 3) |_| { + if (try s.grow()) |new_page| cur_page = new_page; + } + } + + // Our total rows should include history + const total_rows_before = s.totalRows(); + try testing.expect(total_rows_before > s.rows); + + // Scroll back to create a pinned viewport somewhere in the history, + // well after the erase will complete + const pin_y = page.capacity.rows * 2 + 10; + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = pin_y } }).? }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y, + .len = s.rows, + }, s.scrollbar()); + + // Erase a row from the beginning of history with a limit that is + // LARGER than all remaining pages combined. This ensures we exhaust + // all pages in the while loop and reach the cleanup code after the loop. + const limit = total_rows_before * 2; + try s.eraseRowBounded(.{ .history = .{ .y = 0 } }, limit); + + // Verify the scrollbar reflects the change (offset decreased by 1) + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y - 1, + .len = s.rows, + }, s.scrollbar()); } test "PageList adjustCapacity to increase styles" { @@ -5010,11 +6557,11 @@ test "PageList erase" { try testing.expectEqual(@as(usize, 6), s.totalPages()); // Our total rows should be large - try testing.expect(s.totalRows() > s.rows); + try testing.expect(s.total_rows > s.rows); // Erase the entire history, we should be back to just our active set. s.eraseRows(.{ .history = .{} }, null); - try testing.expectEqual(s.rows, s.totalRows()); + try testing.expectEqual(s.rows, s.total_rows); // We should be back to just one page try testing.expectEqual(@as(usize, 1), s.totalPages()); @@ -5069,7 +6616,7 @@ test "PageList erase row with tracked pin resets to top-left" { cur_page.data.pauseIntegrityChecks(false); // Our total rows should be large - try testing.expect(s.totalRows() > s.rows); + try testing.expect(s.total_rows > s.rows); // Put a tracked pin in the history const p = try s.trackPin(s.pin(.{ .history = .{} }).?); @@ -5077,7 +6624,7 @@ test "PageList erase row with tracked pin resets to top-left" { // Erase the entire history, we should be back to just our active set. s.eraseRows(.{ .history = .{} }, null); - try testing.expectEqual(s.rows, s.totalRows()); + try testing.expectEqual(s.rows, s.total_rows); // Our pin should move to the first page try testing.expectEqual(s.pages.first.?, p.node); @@ -5098,7 +6645,7 @@ test "PageList erase row with tracked pin shifts" { // Erase only a few rows in our active s.eraseRows(.{ .active = .{} }, .{ .active = .{ .y = 3 } }); - try testing.expectEqual(s.rows, s.totalRows()); + try testing.expectEqual(s.rows, s.total_rows); // Our pin should move to the first page try testing.expectEqual(s.pages.first.?, p.node); @@ -5119,7 +6666,7 @@ test "PageList erase row with tracked pin is erased" { // Erase the entire history, we should be back to just our active set. s.eraseRows(.{ .active = .{} }, .{ .active = .{ .y = 3 } }); - try testing.expectEqual(s.rows, s.totalRows()); + try testing.expectEqual(s.rows, s.total_rows); // Our pin should move to the first page try testing.expectEqual(s.pages.first.?, p.node); @@ -5148,9 +6695,8 @@ test "PageList erase resets viewport to active if moves within active" { cur_page.data.pauseIntegrityChecks(false); // Move our viewport to the top - s.scroll(.{ .delta_row = -@as(isize, @intCast(s.totalRows())) }); - try testing.expect(s.viewport == .pin); - try testing.expect(s.viewport_pin.node == s.pages.first.?); + s.scroll(.{ .delta_row = -@as(isize, @intCast(s.total_rows)) }); + try testing.expect(s.viewport == .top); // Erase the entire history, we should be back to just our active set. s.eraseRows(.{ .history = .{} }, null); @@ -5178,14 +6724,12 @@ test "PageList erase resets viewport if inside erased page but not active" { cur_page.data.pauseIntegrityChecks(false); // Move our viewport to the top - s.scroll(.{ .delta_row = -@as(isize, @intCast(s.totalRows())) }); - try testing.expect(s.viewport == .pin); - try testing.expect(s.viewport_pin.node == s.pages.first.?); + s.scroll(.{ .delta_row = -@as(isize, @intCast(s.total_rows)) }); + try testing.expect(s.viewport == .top); // Erase the entire history, we should be back to just our active set. s.eraseRows(.{ .history = .{} }, .{ .history = .{ .y = 2 } }); - try testing.expect(s.viewport == .pin); - try testing.expect(s.viewport_pin.node == s.pages.first.?); + try testing.expect(s.viewport == .top); } test "PageList erase resets viewport to active if top is inside active" { @@ -5246,7 +6790,7 @@ test "PageList erase a one-row active" { } s.eraseRows(.{ .active = .{} }, .{ .active = .{} }); - try testing.expectEqual(s.rows, s.totalRows()); + try testing.expectEqual(s.rows, s.total_rows); // The row should be empty { @@ -6089,14 +7633,16 @@ test "PageList resize (no reflow) more rows contains viewport" { // Set viewport above active by scrolling up one. s.scroll(.{ .delta_row = -1 }); // The viewport should be a pin now. - try testing.expectEqual(Viewport.pin, s.viewport); + try testing.expectEqual(Viewport.top, s.viewport); // Resize try s.resize(.{ .rows = 7, .reflow = false }); try testing.expectEqual(@as(usize, 7), s.rows); try testing.expectEqual(@as(usize, 7), s.totalRows()); - // The viewport should now be active, not a pin. - try testing.expectEqual(Viewport.active, s.viewport); + + // Question: maybe the viewport should actually be in the active + // here and not pinned to the top. + try testing.expectEqual(Viewport.top, s.viewport); } test "PageList resize (no reflow) less cols" { @@ -6650,6 +8196,55 @@ test "PageList resize reflow more cols wrapped rows" { } } +test "PageList resize reflow invalidates viewport offset cache" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 2, 4, null); + defer s.deinit(); + try s.growRows(20); + + const page = &s.pages.last.?.data; + for (0..s.rows) |y| { + if (y % 2 == 0) { + const rac = page.getRowAndCell(0, y); + rac.row.wrap = true; + } else { + const rac = page.getRowAndCell(0, y); + rac.row.wrap_continuation = true; + } + + for (0..s.cols) |x| { + const rac = page.getRowAndCell(x, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 'A' }, + }; + } + } + + // Scroll to a pinned viewport in history + const pin_y = 10; + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = pin_y } }).? }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = pin_y, + .len = s.rows, + }, s.scrollbar()); + + // Resize with reflow - unwrapping rows changes total_rows + try s.resize(.{ .cols = 4, .reflow = true }); + try testing.expectEqual(@as(usize, 4), s.cols); + + // Verify scrollbar cache was invalidated during reflow + try testing.expectEqual(Scrollbar{ + .total = s.total_rows, + .offset = 8, + .len = s.rows, + }, s.scrollbar()); +} + test "PageList resize reflow more cols creates multiple pages" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 1f2e814f6..625591d3f 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -97,13 +97,9 @@ pub const Action = union(enum) { // Implement formatter for logging pub fn format( self: CSI, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { - _ = layout; - _ = opts; - try std.fmt.format(writer, "ESC [ {s} {any} {c}", .{ + try writer.print("ESC [ {s} {any} {c}", .{ self.intermediates, self.params, self.final, @@ -118,13 +114,9 @@ pub const Action = union(enum) { // Implement formatter for logging pub fn format( self: ESC, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { - _ = layout; - _ = opts; - try std.fmt.format(writer, "ESC {s} {c}", .{ + try writer.print("ESC {s} {c}", .{ self.intermediates, self.final, }); @@ -142,11 +134,8 @@ pub const Action = union(enum) { // print out custom formats for some of our primitives. pub fn format( self: Action, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { - _ = layout; const T = Action; const info = @typeInfo(T).@"union"; @@ -162,21 +151,20 @@ pub const Action = union(enum) { const value = @field(self, u_field.name); switch (@TypeOf(value)) { // Unicode - u21 => try std.fmt.format(writer, "'{u}' (U+{X})", .{ value, value }), + u21 => try writer.print("'{u}' (U+{X})", .{ value, value }), // Byte - u8 => try std.fmt.format(writer, "0x{x}", .{value}), + u8 => try writer.print("0x{x}", .{value}), // Note: we don't do ASCII (u8) because there are a lot // of invisible characters we don't want to handle right // now. // All others do the default behavior - else => try std.fmt.formatType( - @field(self, u_field.name), + else => try writer.printValue( "any", - opts, - writer, + .{}, + @field(self, u_field.name), 3, ), } @@ -233,7 +221,7 @@ pub fn init() Parser { .params_idx = 0, .param_acc = 0, .param_acc_idx = 0, - .osc_parser = .init(), + .osc_parser = .init(null), .intermediates = undefined, .params = undefined, @@ -274,7 +262,7 @@ pub fn next(self: *Parser, c: u8) [3]?Action { // Exit depends on current state if (self.state == next_state) null else switch (self.state) { .osc_string => if (self.osc_parser.end(c)) |cmd| - Action{ .osc_dispatch = cmd } + Action{ .osc_dispatch = cmd.* } else null, .dcs_passthrough => Action{ .dcs_unhook = {} }, @@ -314,7 +302,7 @@ pub fn next(self: *Parser, c: u8) [3]?Action { }; } -pub fn collect(self: *Parser, c: u8) void { +pub inline fn collect(self: *Parser, c: u8) void { if (self.intermediates_idx >= MAX_INTERMEDIATE) { log.warn("invalid intermediates count", .{}); return; @@ -324,7 +312,7 @@ pub fn collect(self: *Parser, c: u8) void { self.intermediates_idx += 1; } -fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action { +inline fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action { return switch (action) { .none, .ignore => null, .print => Action{ .print = c }, @@ -391,7 +379,7 @@ fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action { // We only allow colon or mixed separators for the 'm' command. if (c != 'm' and self.params_sep.count() > 0) { log.warn( - "CSI colon or mixed separators only allowed for 'm' command, got: {}", + "CSI colon or mixed separators only allowed for 'm' command, got: {f}", .{result}, ); break :csi_dispatch null; @@ -410,7 +398,7 @@ fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action { }; } -pub fn clear(self: *Parser) void { +pub inline fn clear(self: *Parser) void { self.intermediates_idx = 0; self.params_idx = 0; self.params_sep = .initEmpty(); diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 7be4d7c12..81d6d4ab6 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -533,13 +533,13 @@ pub fn adjustCapacity( return new_node; } -pub fn cursorCellRight(self: *Screen, n: size.CellCountInt) *pagepkg.Cell { +pub inline fn cursorCellRight(self: *Screen, n: size.CellCountInt) *pagepkg.Cell { assert(self.cursor.x + n < self.pages.cols); const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell); return @ptrCast(cell + n); } -pub fn cursorCellLeft(self: *Screen, n: size.CellCountInt) *pagepkg.Cell { +pub inline fn cursorCellLeft(self: *Screen, n: size.CellCountInt) *pagepkg.Cell { assert(self.cursor.x >= n); const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell); return @ptrCast(cell - n); @@ -959,7 +959,7 @@ fn cursorScrollAboveRotate(self: *Screen) !void { /// Move the cursor down if we're not at the bottom of the screen. Otherwise /// scroll. Currently only used for testing. -fn cursorDownOrScroll(self: *Screen) !void { +inline fn cursorDownOrScroll(self: *Screen) !void { if (self.cursor.y + 1 < self.pages.rows) { self.cursorDown(1); } else { @@ -1034,7 +1034,7 @@ 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. -fn cursorChangePin(self: *Screen, new: Pin) void { +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 // as the pins are not equal @@ -1108,7 +1108,7 @@ fn cursorChangePin(self: *Screen, new: Pin) void { /// Mark the cursor position as dirty. /// TODO: test -pub fn cursorMarkDirty(self: *Screen) void { +pub inline fn cursorMarkDirty(self: *Screen) void { self.cursor.page_pin.markDirty(); } @@ -1155,12 +1155,13 @@ pub const Scroll = union(enum) { active, top, pin: Pin, + row: usize, delta_row: isize, delta_prompt: isize, }; /// Scroll the viewport of the terminal grid. -pub fn scroll(self: *Screen, behavior: Scroll) void { +pub inline fn scroll(self: *Screen, behavior: Scroll) void { defer self.assertIntegrity(); if (comptime build_options.kitty_graphics) { @@ -1174,6 +1175,7 @@ pub fn scroll(self: *Screen, behavior: Scroll) void { .active => self.pages.scroll(.{ .active = {} }), .top => self.pages.scroll(.{ .top = {} }), .pin => |p| self.pages.scroll(.{ .pin = p }), + .row => |v| self.pages.scroll(.{ .row = v }), .delta_row => |v| self.pages.scroll(.{ .delta_row = v }), .delta_prompt => |v| self.pages.scroll(.{ .delta_prompt = v }), } @@ -1181,7 +1183,7 @@ pub fn scroll(self: *Screen, behavior: Scroll) void { /// See PageList.scrollClear. In addition to that, we reset the cursor /// to be on top. -pub fn scrollClear(self: *Screen) !void { +pub inline fn scrollClear(self: *Screen) !void { defer self.assertIntegrity(); try self.pages.scrollClear(); @@ -1196,14 +1198,14 @@ pub fn scrollClear(self: *Screen) !void { } /// Returns true if the viewport is scrolled to the bottom of the screen. -pub fn viewportIsBottom(self: Screen) bool { +pub inline fn viewportIsBottom(self: Screen) bool { return self.pages.viewport == .active; } /// Erase the region specified by tl and br, inclusive. This will physically /// erase the rows meaning the memory will be reclaimed (if the underlying /// page is empty) and other rows will be shifted up. -pub fn eraseRows( +pub inline fn eraseRows( self: *Screen, tl: point.Point, bl: ?point.Point, @@ -1539,7 +1541,7 @@ pub fn splitCellBoundary( /// Returns the blank cell to use when doing terminal operations that /// require preserving the bg color. -pub fn blankCell(self: *const Screen) Cell { +pub inline fn blankCell(self: *const Screen) Cell { if (self.cursor.style_id == style.default_id) return .{}; return self.cursor.style.bgCell() orelse .{}; } @@ -1557,7 +1559,7 @@ pub fn blankCell(self: *const Screen) Cell { /// probably means the system is in trouble anyways. I'd like to improve this /// in the future but it is not a priority particularly because this scenario /// (resize) is difficult. -pub fn resize( +pub inline fn resize( self: *Screen, cols: size.CellCountInt, rows: size.CellCountInt, @@ -1568,7 +1570,7 @@ pub fn resize( /// Resize the screen without any reflow. In this mode, columns/rows will /// be truncated as they are shrunk. If they are grown, the new space is filled /// with zeros. -pub fn resizeWithoutReflow( +pub inline fn resizeWithoutReflow( self: *Screen, cols: size.CellCountInt, rows: size.CellCountInt, @@ -2168,17 +2170,21 @@ pub const SelectionString = struct { /// 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). -pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ![:0]const u8 { +pub fn selectionString( + self: *Screen, + alloc: Allocator, + opts: SelectionString, +) ![:0]const u8 { // Use an ArrayList so that we can grow the array as we go. We // build an initial capacity of just our rows in our selection times // columns. It can be more or less based on graphemes, newlines, etc. - var strbuilder = std.ArrayList(u8).init(alloc); - defer strbuilder.deinit(); + var strbuilder: std.ArrayList(u8) = .empty; + defer strbuilder.deinit(alloc); // If we're building a stringmap, create our builder for the pins. const MapBuilder = std.ArrayList(Pin); - var mapbuilder: ?MapBuilder = if (opts.map != null) MapBuilder.init(alloc) else null; - defer if (mapbuilder) |*b| b.deinit(); + var mapbuilder: ?MapBuilder = if (opts.map != null) .empty else null; + defer if (mapbuilder) |*b| b.deinit(alloc); const sel_ordered = opts.sel.ordered(self, .forward); const sel_start: Pin = start: { @@ -2235,9 +2241,9 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! const raw: u21 = if (cell.hasText()) cell.content.codepoint else 0; const char = if (raw > 0) raw else ' '; const encode_len = try std.unicode.utf8Encode(char, &buf); - try strbuilder.appendSlice(buf[0..encode_len]); + try strbuilder.appendSlice(alloc, buf[0..encode_len]); if (mapbuilder) |*b| { - for (0..encode_len) |_| try b.append(.{ + for (0..encode_len) |_| try b.append(alloc, .{ .node = chunk.node, .y = @intCast(y), .x = @intCast(x), @@ -2248,9 +2254,9 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! const cps = chunk.node.data.lookupGrapheme(cell).?; for (cps) |cp| { const encode_len = try std.unicode.utf8Encode(cp, &buf); - try strbuilder.appendSlice(buf[0..encode_len]); + try strbuilder.appendSlice(alloc, buf[0..encode_len]); if (mapbuilder) |*b| { - for (0..encode_len) |_| try b.append(.{ + for (0..encode_len) |_| try b.append(alloc, .{ .node = chunk.node, .y = @intCast(y), .x = @intCast(x), @@ -2265,8 +2271,8 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! if (!is_final_row and (!row.wrap or sel_ordered.rectangle)) { - try strbuilder.append('\n'); - if (mapbuilder) |*b| try b.append(.{ + try strbuilder.append(alloc, '\n'); + if (mapbuilder) |*b| try b.append(alloc, .{ .node = chunk.node, .y = @intCast(y), .x = chunk.node.data.size.cols - 1, @@ -2281,11 +2287,11 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! // If we have a mapbuilder, we need to setup our string map. if (mapbuilder) |*b| { - var strclone = try strbuilder.clone(); - defer strclone.deinit(); - const str = try strclone.toOwnedSliceSentinel(0); + var strclone = try strbuilder.clone(alloc); + defer strclone.deinit(alloc); + const str = try strclone.toOwnedSliceSentinel(alloc, 0); errdefer alloc.free(str); - const map = try b.toOwnedSlice(); + const map = try b.toOwnedSlice(alloc); errdefer alloc.free(map); opts.map.?.* = .{ .string = str, .map = map }; } @@ -2306,7 +2312,7 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! const i = strbuilder.items.len; strbuilder.items.len += trimmed.len; std.mem.copyForwards(u8, strbuilder.items[i..], trimmed); - try strbuilder.append('\n'); + try strbuilder.append(alloc, '\n'); } // Remove all trailing newlines @@ -2317,7 +2323,7 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! } // Get our final string - const string = try strbuilder.toOwnedSliceSentinel(0); + const string = try strbuilder.toOwnedSliceSentinel(alloc, 0); errdefer alloc.free(string); return string; @@ -2561,6 +2567,7 @@ pub fn selectWord(self: *Screen, pin: Pin) ?Selection { '`', '|', ':', + ';', ',', '(', ')', @@ -2902,7 +2909,7 @@ pub fn promptPath( /// one byte at a time. pub fn dumpString( self: *const Screen, - writer: anytype, + writer: *std.Io.Writer, opts: PageList.EncodeUtf8Options, ) anyerror!void { try self.pages.encodeUtf8(writer, opts); @@ -2915,10 +2922,10 @@ pub fn dumpStringAlloc( alloc: Allocator, tl: point.Point, ) ![]const u8 { - var builder = std.ArrayList(u8).init(alloc); + var builder: std.Io.Writer.Allocating = .init(alloc); defer builder.deinit(); - try self.dumpString(builder.writer(), .{ + try self.dumpString(&builder.writer, .{ .tl = self.pages.getTopLeft(tl), .br = self.pages.getBottomRight(tl) orelse return error.UnknownPoint, .unwrap = false, @@ -2934,10 +2941,10 @@ pub fn dumpStringAllocUnwrapped( alloc: Allocator, tl: point.Point, ) ![]const u8 { - var builder = std.ArrayList(u8).init(alloc); + var builder: std.Io.Writer.Allocating = .init(alloc); defer builder.deinit(); - try self.dumpString(builder.writer(), .{ + try self.dumpString(&builder.writer, .{ .tl = self.pages.getTopLeft(tl), .br = self.pages.getBottomRight(tl) orelse return error.UnknownPoint, .unwrap = true, @@ -7815,6 +7822,7 @@ test "Screen: selectWord with character boundary" { " `abc` \n123", " |abc| \n123", " :abc: \n123", + " ;abc; \n123", " ,abc, \n123", " (abc( \n123", " )abc) \n123", @@ -9030,33 +9038,33 @@ test "Screen UTF8 cell map with newlines" { var cell_map = Page.CellMap.init(alloc); defer cell_map.deinit(); - var builder = std.ArrayList(u8).init(alloc); + var builder: std.Io.Writer.Allocating = .init(alloc); defer builder.deinit(); - try s.dumpString(builder.writer(), .{ + try s.dumpString(&builder.writer, .{ .tl = s.pages.getTopLeft(.screen), .br = s.pages.getBottomRight(.screen), .cell_map = &cell_map, }); - try testing.expectEqual(7, builder.items.len); - try testing.expectEqualStrings("A\n\nB\n\nC", builder.items); - try testing.expectEqual(builder.items.len, cell_map.items.len); + try testing.expectEqual(7, builder.written().len); + try testing.expectEqualStrings("A\n\nB\n\nC", builder.written()); + try testing.expectEqual(builder.written().len, cell_map.map.items.len); try testing.expectEqual(Page.CellMapEntry{ .x = 0, .y = 0, - }, cell_map.items[0]); + }, cell_map.map.items[0]); try testing.expectEqual(Page.CellMapEntry{ .x = 1, .y = 0, - }, cell_map.items[1]); + }, cell_map.map.items[1]); try testing.expectEqual(Page.CellMapEntry{ .x = 0, .y = 1, - }, cell_map.items[2]); + }, cell_map.map.items[2]); try testing.expectEqual(Page.CellMapEntry{ .x = 0, .y = 2, - }, cell_map.items[3]); + }, cell_map.map.items[3]); } test "Screen UTF8 cell map with blank prefix" { @@ -9068,32 +9076,32 @@ test "Screen UTF8 cell map with blank prefix" { s.cursorAbsolute(2, 1); try s.testWriteString("B"); - var cell_map = Page.CellMap.init(alloc); + var cell_map: Page.CellMap = .init(alloc); defer cell_map.deinit(); - var builder = std.ArrayList(u8).init(alloc); + var builder: std.Io.Writer.Allocating = .init(alloc); defer builder.deinit(); - try s.dumpString(builder.writer(), .{ + try s.dumpString(&builder.writer, .{ .tl = s.pages.getTopLeft(.screen), .br = s.pages.getBottomRight(.screen), .cell_map = &cell_map, }); - try testing.expectEqualStrings("\n B", builder.items); - try testing.expectEqual(builder.items.len, cell_map.items.len); + try testing.expectEqualStrings("\n B", builder.written()); + try testing.expectEqual(builder.written().len, cell_map.map.items.len); try testing.expectEqual(Page.CellMapEntry{ .x = 0, .y = 0, - }, cell_map.items[0]); + }, cell_map.map.items[0]); try testing.expectEqual(Page.CellMapEntry{ .x = 0, .y = 1, - }, cell_map.items[1]); + }, cell_map.map.items[1]); try testing.expectEqual(Page.CellMapEntry{ .x = 1, .y = 1, - }, cell_map.items[2]); + }, cell_map.map.items[2]); try testing.expectEqual(Page.CellMapEntry{ .x = 2, .y = 1, - }, cell_map.items[3]); + }, cell_map.map.items[3]); } diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index c9a1f1d1d..69bcbcb84 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -223,7 +223,7 @@ pub fn init( .left = 0, .right = cols - 1, }, - .pwd = std.ArrayList(u8).init(alloc), + .pwd = .empty, .modes = .{ .values = opts.default_modes, .default = opts.default_modes, @@ -235,10 +235,15 @@ pub fn deinit(self: *Terminal, alloc: Allocator) void { self.tabstops.deinit(alloc); self.screen.deinit(); self.secondary_screen.deinit(); - self.pwd.deinit(); + self.pwd.deinit(alloc); self.* = undefined; } +/// The general allocator we should use for this terminal. +fn gpa(self: *Terminal) Allocator { + return self.screen.alloc; +} + /// Print UTF-8 encoded string to the terminal. pub fn printString(self: *Terminal, str: []const u8) !void { const view = try std.unicode.Utf8View.init(str); @@ -434,8 +439,8 @@ pub fn print(self: *Terminal, c: u21) !void { // control characters because they're always filtered prior. const width: usize = if (c <= 0xFF) 1 else @intCast(unicode.table.get(c).width); - // Note: it is possible to have a width of "3" and a width of "-1" - // from ziglyph. We should look into those cases and handle them + // Note: it is possible to have a width of "3" and a width of "-1" from + // uucode.x's wcwidth. We should look into those cases and handle them // appropriately. assert(width <= 2); // log.debug("c={x} width={}", .{ c, width }); @@ -2531,7 +2536,7 @@ pub fn resize( /// Set the pwd for the terminal. pub fn setPwd(self: *Terminal, pwd: []const u8) !void { self.pwd.clearRetainingCapacity(); - try self.pwd.appendSlice(pwd); + try self.pwd.appendSlice(self.gpa(), pwd); } /// Returns the pwd for the terminal, if any. The memory is owned by the diff --git a/src/terminal/apc.zig b/src/terminal/apc.zig index a168da4a1..704c3fbe3 100644 --- a/src/terminal/apc.zig +++ b/src/terminal/apc.zig @@ -65,7 +65,9 @@ pub const Handler = struct { .kitty => |*p| kitty: { if (comptime !build_options.kitty_graphics) unreachable; - const command = p.complete() catch |err| { + // Use the same allocator that was used to create the parser. + const alloc = p.arena.child_allocator; + const command = p.complete(alloc) catch |err| { log.warn("kitty graphics protocol error: {}", .{err}); break :kitty null; }; diff --git a/src/terminal/bitmap_allocator.zig b/src/terminal/bitmap_allocator.zig index 724c71be5..894172b4c 100644 --- a/src/terminal/bitmap_allocator.zig +++ b/src/terminal/bitmap_allocator.zig @@ -34,7 +34,7 @@ pub fn BitmapAllocator(comptime chunk_size: comptime_int) type { assert(std.math.isPowerOfTwo(chunk_size)); } - pub const base_align = @alignOf(u64); + pub const base_align: std.mem.Alignment = .fromByteUnits(@alignOf(u64)); pub const bitmap_bit_size = @bitSizeOf(u64); /// The bitmap of available chunks. Each bit represents a chunk. A @@ -49,7 +49,7 @@ pub fn BitmapAllocator(comptime chunk_size: comptime_int) type { /// Initialize the allocator map with a given buf and memory layout. pub fn init(buf: OffsetBuf, l: Layout) Self { - assert(@intFromPtr(buf.start()) % base_align == 0); + assert(base_align.check(@intFromPtr(buf.start()))); // Initialize our bitmaps to all 1s to note that all chunks are free. const bitmap = buf.member(u64, l.bitmap_start); @@ -92,7 +92,7 @@ pub fn BitmapAllocator(comptime chunk_size: comptime_int) type { return error.OutOfMemory; const chunks = self.chunks.ptr(base); - const ptr: [*]T = @alignCast(@ptrCast(&chunks[idx * chunk_size])); + const ptr: [*]T = @ptrCast(@alignCast(&chunks[idx * chunk_size])); return ptr[0..n]; } diff --git a/src/terminal/build_options.zig b/src/terminal/build_options.zig index 1b0449bbf..e209a56fa 100644 --- a/src/terminal/build_options.zig +++ b/src/terminal/build_options.zig @@ -1,5 +1,6 @@ const std = @import("std"); +/// Options set by Zig build.zig and exposed via `terminal_options`. pub const Options = struct { /// The target artifact to build. This will gate some functionality. artifact: Artifact, @@ -23,6 +24,10 @@ pub const Options = struct { /// generally be disabled in production builds. slow_runtime_safety: bool, + /// Force C ABI mode on or off. If not set, then it will be set based on + /// Options. + c_abi: bool, + /// Add the required build options for the terminal module. pub fn add( self: Options, @@ -31,6 +36,7 @@ pub const Options = struct { ) void { const opts = b.addOptions(); opts.addOption(Artifact, "artifact", self.artifact); + opts.addOption(bool, "c_abi", self.c_abi); opts.addOption(bool, "oniguruma", self.oniguruma); opts.addOption(bool, "simd", self.simd); opts.addOption(bool, "slow_runtime_safety", self.slow_runtime_safety); diff --git a/src/terminal/c/key_encode.zig b/src/terminal/c/key_encode.zig new file mode 100644 index 000000000..f5f6ff054 --- /dev/null +++ b/src/terminal/c/key_encode.zig @@ -0,0 +1,271 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const lib_alloc = @import("../../lib/allocator.zig"); +const CAllocator = lib_alloc.Allocator; +const key_encode = @import("../../input/key_encode.zig"); +const key_event = @import("key_event.zig"); +const KittyFlags = @import("../../terminal/kitty/key.zig").Flags; +const OptionAsAlt = @import("../../input/config.zig").OptionAsAlt; +const Result = @import("result.zig").Result; +const KeyEvent = @import("key_event.zig").Event; + +/// Wrapper around key encoding options that tracks the allocator for C API usage. +const KeyEncoderWrapper = struct { + opts: key_encode.Options, + alloc: Allocator, +}; + +/// C: GhosttyKeyEncoder +pub const Encoder = ?*KeyEncoderWrapper; + +pub fn new( + alloc_: ?*const CAllocator, + result: *Encoder, +) callconv(.c) Result { + const alloc = lib_alloc.default(alloc_); + const ptr = alloc.create(KeyEncoderWrapper) catch + return .out_of_memory; + ptr.* = .{ + .opts = .{}, + .alloc = alloc, + }; + result.* = ptr; + return .success; +} + +pub fn free(encoder_: Encoder) callconv(.c) void { + const wrapper = encoder_ orelse return; + const alloc = wrapper.alloc; + alloc.destroy(wrapper); +} + +/// C: GhosttyKeyEncoderOption +pub const Option = enum(c_int) { + cursor_key_application = 0, + keypad_key_application = 1, + ignore_keypad_with_numlock = 2, + alt_esc_prefix = 3, + modify_other_keys_state_2 = 4, + kitty_flags = 5, + macos_option_as_alt = 6, + + /// Input type expected for setting the option. + pub fn InType(comptime self: Option) type { + return switch (self) { + .cursor_key_application, + .keypad_key_application, + .ignore_keypad_with_numlock, + .alt_esc_prefix, + .modify_other_keys_state_2, + => bool, + .kitty_flags => u8, + .macos_option_as_alt => OptionAsAlt, + }; + } +}; + +pub fn setopt( + encoder_: Encoder, + option: Option, + value: ?*const anyopaque, +) callconv(.c) void { + return switch (option) { + inline else => |comptime_option| setoptTyped( + encoder_, + comptime_option, + @ptrCast(@alignCast(value orelse return)), + ), + }; +} + +fn setoptTyped( + encoder_: Encoder, + comptime option: Option, + value: *const option.InType(), +) void { + const opts = &encoder_.?.opts; + switch (option) { + .cursor_key_application => opts.cursor_key_application = value.*, + .keypad_key_application => opts.keypad_key_application = value.*, + .ignore_keypad_with_numlock => opts.ignore_keypad_with_numlock = value.*, + .alt_esc_prefix => opts.alt_esc_prefix = value.*, + .modify_other_keys_state_2 => opts.modify_other_keys_state_2 = value.*, + .kitty_flags => opts.kitty_flags = flags: { + const bits: u5 = @truncate(value.*); + break :flags @bitCast(bits); + }, + .macos_option_as_alt => opts.macos_option_as_alt = value.*, + } +} + +pub fn encode( + encoder_: Encoder, + event_: KeyEvent, + out_: ?[*]u8, + out_len: usize, + out_written: *usize, +) callconv(.c) Result { + // Attempt to write to this buffer + var writer: std.Io.Writer = .fixed(if (out_) |out| out[0..out_len] else &.{}); + key_encode.encode( + &writer, + event_.?.event, + encoder_.?.opts, + ) catch |err| switch (err) { + error.WriteFailed => { + // If we don't have space, use a discarding writer to count + // how much space we would have needed. + var discarding: std.Io.Writer.Discarding = .init(&.{}); + key_encode.encode( + &discarding.writer, + event_.?.event, + encoder_.?.opts, + ) catch unreachable; + + // Discarding always uses a u64. If we're on 32-bit systems + // we cast down. We should make this safer in the future. + out_written.* = @intCast(discarding.count); + return .out_of_memory; + }, + }; + + out_written.* = writer.end; + return .success; +} + +test "alloc" { + const testing = std.testing; + var e: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + free(e); +} + +test "setopt bool" { + const testing = std.testing; + var e: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Test setting bool options + const val_true: bool = true; + setopt(e, .cursor_key_application, &val_true); + try testing.expect(e.?.opts.cursor_key_application); + + const val_false: bool = false; + setopt(e, .cursor_key_application, &val_false); + try testing.expect(!e.?.opts.cursor_key_application); + + setopt(e, .keypad_key_application, &val_true); + try testing.expect(e.?.opts.keypad_key_application); +} + +test "setopt kitty flags" { + const testing = std.testing; + var e: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Test setting kitty flags + const flags: KittyFlags = .{ + .disambiguate = true, + .report_events = true, + }; + const flags_int: u8 = @intCast(flags.int()); + setopt(e, .kitty_flags, &flags_int); + try testing.expect(e.?.opts.kitty_flags.disambiguate); + try testing.expect(e.?.opts.kitty_flags.report_events); + try testing.expect(!e.?.opts.kitty_flags.report_alternates); +} + +test "setopt macos option as alt" { + const testing = std.testing; + var e: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Test setting option as alt + const opt_left: OptionAsAlt = .left; + setopt(e, .macos_option_as_alt, &opt_left); + try testing.expectEqual(OptionAsAlt.left, e.?.opts.macos_option_as_alt); + + const opt_true: OptionAsAlt = .true; + setopt(e, .macos_option_as_alt, &opt_true); + try testing.expectEqual(OptionAsAlt.true, e.?.opts.macos_option_as_alt); +} + +test "encode: kitty ctrl release with ctrl mod set" { + const testing = std.testing; + + // Create encoder + var encoder: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &encoder, + )); + defer free(encoder); + + // Set kitty flags with all features enabled + { + const flags: KittyFlags = .{ + .disambiguate = true, + .report_events = true, + .report_alternates = true, + .report_all = true, + .report_associated = true, + }; + const flags_int: u8 = @intCast(flags.int()); + setopt(encoder, .kitty_flags, &flags_int); + } + + // Create key event + var event: key_event.Event = undefined; + try testing.expectEqual(Result.success, key_event.new( + &lib_alloc.test_allocator, + &event, + )); + defer key_event.free(event); + + // Set event properties: release action, ctrl key, ctrl modifier + key_event.set_action(event, .release); + key_event.set_key(event, .control_left); + key_event.set_mods(event, .{ .ctrl = true }); + + // Encode null should give us the length required + var required: usize = 0; + try testing.expectEqual(Result.out_of_memory, encode( + encoder, + event, + null, + 0, + &required, + )); + + // Encode the key event + var buf: [128]u8 = undefined; + var written: usize = 0; + try testing.expectEqual(Result.success, encode( + encoder, + event, + &buf, + buf.len, + &written, + )); + try testing.expectEqual(required, written); + + // Expected: ESC[57442;5:3u (ctrl key code with mods and release event) + const actual = buf[0..written]; + try testing.expectEqualStrings("\x1b[57442;5:3u", actual); +} diff --git a/src/terminal/c/key_event.zig b/src/terminal/c/key_event.zig new file mode 100644 index 000000000..5befe4384 --- /dev/null +++ b/src/terminal/c/key_event.zig @@ -0,0 +1,253 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const lib_alloc = @import("../../lib/allocator.zig"); +const CAllocator = lib_alloc.Allocator; +const key = @import("../../input/key.zig"); +const Result = @import("result.zig").Result; + +/// Wrapper around KeyEvent that tracks the allocator for C API usage. +/// The UTF-8 text is not owned by this wrapper - the caller is responsible +/// for ensuring the lifetime of any UTF-8 text set via set_utf8. +const KeyEventWrapper = struct { + event: key.KeyEvent = .{}, + alloc: Allocator, +}; + +/// C: GhosttyKeyEvent +pub const Event = ?*KeyEventWrapper; + +pub fn new( + alloc_: ?*const CAllocator, + result: *Event, +) callconv(.c) Result { + const alloc = lib_alloc.default(alloc_); + const ptr = alloc.create(KeyEventWrapper) catch + return .out_of_memory; + ptr.* = .{ .alloc = alloc }; + result.* = ptr; + return .success; +} + +pub fn free(event_: Event) callconv(.c) void { + const wrapper = event_ orelse return; + const alloc = wrapper.alloc; + alloc.destroy(wrapper); +} + +pub fn set_action(event_: Event, action: key.Action) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.action = action; +} + +pub fn get_action(event_: Event) callconv(.c) key.Action { + const event: *key.KeyEvent = &event_.?.event; + return event.action; +} + +pub fn set_key(event_: Event, k: key.Key) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.key = k; +} + +pub fn get_key(event_: Event) callconv(.c) key.Key { + const event: *key.KeyEvent = &event_.?.event; + return event.key; +} + +pub fn set_mods(event_: Event, mods: key.Mods) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.mods = mods; +} + +pub fn get_mods(event_: Event) callconv(.c) key.Mods { + const event: *key.KeyEvent = &event_.?.event; + return event.mods; +} + +pub fn set_consumed_mods(event_: Event, consumed_mods: key.Mods) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.consumed_mods = consumed_mods; +} + +pub fn get_consumed_mods(event_: Event) callconv(.c) key.Mods { + const event: *key.KeyEvent = &event_.?.event; + return event.consumed_mods; +} + +pub fn set_composing(event_: Event, composing: bool) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.composing = composing; +} + +pub fn get_composing(event_: Event) callconv(.c) bool { + const event: *key.KeyEvent = &event_.?.event; + return event.composing; +} + +pub fn set_utf8(event_: Event, utf8: ?[*]const u8, len: usize) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.utf8 = if (utf8) |ptr| ptr[0..len] else ""; +} + +pub fn get_utf8(event_: Event, len: ?*usize) callconv(.c) ?[*]const u8 { + const event: *key.KeyEvent = &event_.?.event; + if (len) |l| l.* = event.utf8.len; + return if (event.utf8.len == 0) null else event.utf8.ptr; +} + +pub fn set_unshifted_codepoint(event_: Event, codepoint: u32) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.unshifted_codepoint = @truncate(codepoint); +} + +pub fn get_unshifted_codepoint(event_: Event) callconv(.c) u32 { + const event: *key.KeyEvent = &event_.?.event; + return event.unshifted_codepoint; +} + +test "alloc" { + const testing = std.testing; + var e: Event = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + free(e); +} + +test "set" { + const testing = std.testing; + var e: Event = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Test action + set_action(e, .press); + try testing.expectEqual(key.Action.press, e.?.event.action); + + // Test key + set_key(e, .key_a); + try testing.expectEqual(key.Key.key_a, e.?.event.key); + + // Test mods + const mods: key.Mods = .{ .shift = true, .ctrl = true }; + set_mods(e, mods); + try testing.expect(e.?.event.mods.shift); + try testing.expect(e.?.event.mods.ctrl); + + // Test consumed mods + const consumed: key.Mods = .{ .shift = true }; + set_consumed_mods(e, consumed); + try testing.expect(e.?.event.consumed_mods.shift); + try testing.expect(!e.?.event.consumed_mods.ctrl); + + // Test composing + set_composing(e, true); + try testing.expect(e.?.event.composing); + + // Test UTF-8 + const text = "hello"; + set_utf8(e, text.ptr, text.len); + try testing.expectEqualStrings(text, e.?.event.utf8); + + // Test UTF-8 null + set_utf8(e, null, 0); + try testing.expectEqualStrings("", e.?.event.utf8); + + // Test unshifted codepoint + set_unshifted_codepoint(e, 'a'); + try testing.expectEqual(@as(u21, 'a'), e.?.event.unshifted_codepoint); +} + +test "get" { + const testing = std.testing; + var e: Event = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Set some values + set_action(e, .repeat); + set_key(e, .key_z); + + const mods: key.Mods = .{ .alt = true, .super = true }; + set_mods(e, mods); + + const consumed: key.Mods = .{ .alt = true }; + set_consumed_mods(e, consumed); + + set_composing(e, true); + + const text = "test"; + set_utf8(e, text.ptr, text.len); + + set_unshifted_codepoint(e, 'z'); + + // Get them back + try testing.expectEqual(key.Action.repeat, get_action(e)); + try testing.expectEqual(key.Key.key_z, get_key(e)); + + const got_mods = get_mods(e); + try testing.expect(got_mods.alt); + try testing.expect(got_mods.super); + + const got_consumed = get_consumed_mods(e); + try testing.expect(got_consumed.alt); + try testing.expect(!got_consumed.super); + + try testing.expect(get_composing(e)); + + var utf8_len: usize = undefined; + const got_utf8 = get_utf8(e, &utf8_len); + try testing.expect(got_utf8 != null); + try testing.expectEqual(@as(usize, 4), utf8_len); + try testing.expectEqualStrings("test", got_utf8.?[0..utf8_len]); + + try testing.expectEqual(@as(u32, 'z'), get_unshifted_codepoint(e)); +} + +test "complete key event" { + const testing = std.testing; + var e: Event = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Build a complete key event for shift+a + set_action(e, .press); + set_key(e, .key_a); + + const mods: key.Mods = .{ .shift = true }; + set_mods(e, mods); + + const consumed: key.Mods = .{ .shift = true }; + set_consumed_mods(e, consumed); + + const text = "A"; + set_utf8(e, text.ptr, text.len); + + set_unshifted_codepoint(e, 'a'); + + // Verify all fields + try testing.expectEqual(key.Action.press, e.?.event.action); + try testing.expectEqual(key.Key.key_a, e.?.event.key); + try testing.expect(e.?.event.mods.shift); + try testing.expect(e.?.event.consumed_mods.shift); + try testing.expectEqualStrings("A", e.?.event.utf8); + try testing.expectEqual(@as(u21, 'a'), e.?.event.unshifted_codepoint); + + // Also test the getter + var utf8_len: usize = undefined; + const got_utf8 = get_utf8(e, &utf8_len); + try testing.expect(got_utf8 != null); + try testing.expectEqual(@as(usize, 1), utf8_len); + try testing.expectEqualStrings("A", got_utf8.?[0..utf8_len]); +} diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig new file mode 100644 index 000000000..f68333d9b --- /dev/null +++ b/src/terminal/c/main.zig @@ -0,0 +1,47 @@ +pub const osc = @import("osc.zig"); +pub const key_event = @import("key_event.zig"); +pub const key_encode = @import("key_encode.zig"); +pub const paste = @import("paste.zig"); + +// The full C API, unexported. +pub const osc_new = osc.new; +pub const osc_free = osc.free; +pub const osc_reset = osc.reset; +pub const osc_next = osc.next; +pub const osc_end = osc.end; +pub const osc_command_type = osc.commandType; +pub const osc_command_data = osc.commandData; + +pub const key_event_new = key_event.new; +pub const key_event_free = key_event.free; +pub const key_event_set_action = key_event.set_action; +pub const key_event_get_action = key_event.get_action; +pub const key_event_set_key = key_event.set_key; +pub const key_event_get_key = key_event.get_key; +pub const key_event_set_mods = key_event.set_mods; +pub const key_event_get_mods = key_event.get_mods; +pub const key_event_set_consumed_mods = key_event.set_consumed_mods; +pub const key_event_get_consumed_mods = key_event.get_consumed_mods; +pub const key_event_set_composing = key_event.set_composing; +pub const key_event_get_composing = key_event.get_composing; +pub const key_event_set_utf8 = key_event.set_utf8; +pub const key_event_get_utf8 = key_event.get_utf8; +pub const key_event_set_unshifted_codepoint = key_event.set_unshifted_codepoint; +pub const key_event_get_unshifted_codepoint = key_event.get_unshifted_codepoint; + +pub const key_encoder_new = key_encode.new; +pub const key_encoder_free = key_encode.free; +pub const key_encoder_setopt = key_encode.setopt; +pub const key_encoder_encode = key_encode.encode; + +pub const paste_is_safe = paste.is_safe; + +test { + _ = osc; + _ = key_event; + _ = key_encode; + _ = paste; + + // We want to make sure we run the tests for the C allocator interface. + _ = @import("../../lib/allocator.zig"); +} diff --git a/src/terminal/c/osc.zig b/src/terminal/c/osc.zig new file mode 100644 index 000000000..1311eaff8 --- /dev/null +++ b/src/terminal/c/osc.zig @@ -0,0 +1,132 @@ +const std = @import("std"); +const assert = std.debug.assert; +const builtin = @import("builtin"); +const lib_alloc = @import("../../lib/allocator.zig"); +const CAllocator = lib_alloc.Allocator; +const osc = @import("../osc.zig"); +const Result = @import("result.zig").Result; + +/// C: GhosttyOscParser +pub const Parser = ?*osc.Parser; + +/// C: GhosttyOscCommand +pub const Command = ?*osc.Command; + +pub fn new( + alloc_: ?*const CAllocator, + result: *Parser, +) callconv(.c) Result { + const alloc = lib_alloc.default(alloc_); + const ptr = alloc.create(osc.Parser) catch + return .out_of_memory; + ptr.* = .init(alloc); + result.* = ptr; + return .success; +} + +pub fn free(parser_: Parser) callconv(.c) void { + // C-built parsers always have an associated allocator. + const parser = parser_ orelse return; + const alloc = parser.alloc.?; + parser.deinit(); + alloc.destroy(parser); +} + +pub fn reset(parser_: Parser) callconv(.c) void { + parser_.?.reset(); +} + +pub fn next(parser_: Parser, byte: u8) callconv(.c) void { + parser_.?.next(byte); +} + +pub fn end(parser_: Parser, terminator: u8) callconv(.c) Command { + return parser_.?.end(terminator); +} + +pub fn commandType(command_: Command) callconv(.c) osc.Command.Key { + const command = command_ orelse return .invalid; + return command.*; +} + +/// C: GhosttyOscCommandData +pub const CommandData = enum(c_int) { + invalid = 0, + change_window_title_str = 1, + + /// Output type expected for querying the data of the given kind. + pub fn OutType(comptime self: CommandData) type { + return switch (self) { + .invalid => void, + .change_window_title_str => [*:0]const u8, + }; + } +}; + +pub fn commandData( + command_: Command, + data: CommandData, + out: ?*anyopaque, +) callconv(.c) bool { + return switch (data) { + inline else => |comptime_data| commandDataTyped( + command_, + comptime_data, + @ptrCast(@alignCast(out)), + ), + }; +} + +fn commandDataTyped( + command_: Command, + comptime data: CommandData, + out: *data.OutType(), +) bool { + const command = command_.?; + switch (data) { + .invalid => return false, + .change_window_title_str => switch (command.*) { + .change_window_title => |v| out.* = v.ptr, + else => return false, + }, + } + + return true; +} + +test "alloc" { + const testing = std.testing; + var p: Parser = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &p, + )); + free(p); +} + +test "command type null" { + const testing = std.testing; + try testing.expectEqual(.invalid, commandType(null)); +} + +test "change window title" { + const testing = std.testing; + var p: Parser = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &p, + )); + defer free(p); + + // Parse it + next(p, '0'); + next(p, ';'); + next(p, 'a'); + const cmd = end(p, 0); + try testing.expectEqual(.change_window_title, commandType(cmd)); + + // Extract the title + var title: [*:0]const u8 = undefined; + try testing.expect(commandData(cmd, .change_window_title_str, @ptrCast(&title))); + try testing.expectEqualStrings("a", std.mem.span(title)); +} diff --git a/src/terminal/c/paste.zig b/src/terminal/c/paste.zig new file mode 100644 index 000000000..eb4117a70 --- /dev/null +++ b/src/terminal/c/paste.zig @@ -0,0 +1,36 @@ +const std = @import("std"); +const paste = @import("../../input/paste.zig"); + +pub fn is_safe(data: ?[*]const u8, len: usize) callconv(.c) bool { + const slice: []const u8 = if (data) |v| v[0..len] else &.{}; + return paste.isSafe(slice); +} + +test "is_safe with safe data" { + const testing = std.testing; + const safe = "hello world"; + try testing.expect(is_safe(safe.ptr, safe.len)); +} + +test "is_safe with newline" { + const testing = std.testing; + const unsafe = "hello\nworld"; + try testing.expect(!is_safe(unsafe.ptr, unsafe.len)); +} + +test "is_safe with bracketed paste end" { + const testing = std.testing; + const unsafe = "hello\x1b[201~world"; + try testing.expect(!is_safe(unsafe.ptr, unsafe.len)); +} + +test "is_safe with empty data" { + const testing = std.testing; + const empty = ""; + try testing.expect(is_safe(empty.ptr, 0)); +} + +test "is_safe with null empty data" { + const testing = std.testing; + try testing.expect(is_safe(null, 0)); +} diff --git a/src/terminal/c/result.zig b/src/terminal/c/result.zig new file mode 100644 index 000000000..a2ebc9b69 --- /dev/null +++ b/src/terminal/c/result.zig @@ -0,0 +1,5 @@ +/// C: GhosttyResult +pub const Result = enum(c_int) { + success = 0, + out_of_memory = -1, +}; diff --git a/src/terminal/c_api.zig b/src/terminal/c_api.zig deleted file mode 100644 index 194a91d6d..000000000 --- a/src/terminal/c_api.zig +++ /dev/null @@ -1,49 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; -const builtin = @import("builtin"); -const lib_alloc = @import("../lib/allocator.zig"); -const CAllocator = lib_alloc.Allocator; -const osc = @import("osc.zig"); - -/// C: GhosttyOscParser -pub const OscParser = ?*osc.Parser; - -/// C: GhosttyResult -pub const Result = enum(c_int) { - success = 0, - out_of_memory = -1, -}; - -pub fn osc_new( - alloc_: ?*const CAllocator, - result: *OscParser, -) callconv(.c) Result { - const alloc = lib_alloc.default(alloc_); - const ptr = alloc.create(osc.Parser) catch - return .out_of_memory; - ptr.* = .initAlloc(alloc); - result.* = ptr; - return .success; -} - -pub fn osc_free(parser_: OscParser) callconv(.c) void { - // C-built parsers always have an associated allocator. - const parser = parser_ orelse return; - const alloc = parser.alloc.?; - parser.deinit(); - alloc.destroy(parser); -} - -test { - _ = lib_alloc; -} - -test "osc" { - const testing = std.testing; - var p: OscParser = undefined; - try testing.expectEqual(Result.success, osc_new( - &lib_alloc.test_allocator, - &p, - )); - osc_free(p); -} diff --git a/src/terminal/dcs.zig b/src/terminal/dcs.zig index e4d0f3de2..971ea13a0 100644 --- a/src/terminal/dcs.zig +++ b/src/terminal/dcs.zig @@ -64,7 +64,7 @@ pub const Handler = struct { .state = .{ .tmux = .{ .max_bytes = self.max_bytes, - .buffer = try std.ArrayList(u8).initCapacity( + .buffer = try .initCapacity( alloc, 128, // Arbitrary choice to limit initial reallocs ), @@ -83,7 +83,7 @@ pub const Handler = struct { // https://github.com/mitchellh/ghostty/issues/517 'q' => .{ .state = .{ - .xtgettcap = try std.ArrayList(u8).initCapacity( + .xtgettcap = try .initCapacity( alloc, 128, // Arbitrary choice ), @@ -134,11 +134,11 @@ pub const Handler = struct { } else unreachable, .xtgettcap => |*list| { - if (list.items.len >= self.max_bytes) { + if (list.written().len >= self.max_bytes) { return error.OutOfMemory; } - try list.append(byte); + try list.writer.writeByte(byte); }, .decrqss => |*buffer| { @@ -170,11 +170,12 @@ pub const Handler = struct { break :tmux .{ .tmux = .{ .exit = {} } }; } else unreachable, - .xtgettcap => |list| xtgettcap: { - for (list.items, 0..) |b, i| { - list.items[i] = std.ascii.toUpper(b); - } - break :xtgettcap .{ .xtgettcap = .{ .data = list } }; + .xtgettcap => |*list| xtgettcap: { + // Note: purposely do not deinit our state here because + // we copy it into the resulting command. + const items = list.written(); + for (items, 0..) |b, i| items[i] = std.ascii.toUpper(b); + break :xtgettcap .{ .xtgettcap = .{ .data = list.* } }; }, .decrqss => |buffer| .{ .decrqss = switch (buffer.len) { @@ -216,8 +217,8 @@ pub const Command = union(enum) { else void, - pub fn deinit(self: Command) void { - switch (self) { + pub fn deinit(self: *Command) void { + switch (self.*) { .xtgettcap => |*v| v.data.deinit(), .decrqss => {}, .tmux => {}, @@ -225,16 +226,16 @@ pub const Command = union(enum) { } pub const XTGETTCAP = struct { - data: std.ArrayList(u8), + data: std.Io.Writer.Allocating, i: usize = 0, /// Returns the next terminfo key being requested and null /// when there are no more keys. The returned value is NOT hex-decoded /// because we expect to use a comptime lookup table. pub fn next(self: *XTGETTCAP) ?[]const u8 { - if (self.i >= self.data.items.len) return null; - - var rem = self.data.items[self.i..]; + const items = self.data.written(); + if (self.i >= items.len) return null; + var rem = items[self.i..]; const idx = std.mem.indexOf(u8, rem, ";") orelse rem.len; // Note that if we're at the end, idx + 1 is len + 1 so we're over @@ -271,7 +272,7 @@ const State = union(enum) { ignore: void, /// XTGETTCAP - xtgettcap: std.ArrayList(u8), + xtgettcap: std.Io.Writer.Allocating, /// DECRQSS decrqss: struct { diff --git a/src/terminal/hash_map.zig b/src/terminal/hash_map.zig index 9a16be3b2..23b10950e 100644 --- a/src/terminal/hash_map.zig +++ b/src/terminal/hash_map.zig @@ -88,7 +88,7 @@ pub fn OffsetHashMap( /// Initialize a new HashMap with the given capacity and backing /// memory. The backing memory must be aligned to base_align. pub fn init(buf: OffsetBuf, l: Layout) Self { - assert(@intFromPtr(buf.start()) % base_align == 0); + assert(base_align.check(@intFromPtr(buf.start()))); const m = Unmanaged.init(buf, l); return .{ .metadata = getOffset( @@ -124,7 +124,11 @@ fn HashMapUnmanaged( const header_align = @alignOf(Header); const key_align = if (@sizeOf(K) == 0) 1 else @alignOf(K); const val_align = if (@sizeOf(V) == 0) 1 else @alignOf(V); - const base_align = @max(header_align, key_align, val_align); + const base_align: mem.Alignment = .fromByteUnits(@max( + header_align, + key_align, + val_align, + )); // This is actually a midway pointer to the single buffer containing // a `Header` field, the `Metadata`s and `Entry`s. @@ -287,7 +291,7 @@ fn HashMapUnmanaged( /// Initialize a hash map with a given capacity and a buffer. The /// buffer must fit within the size defined by `layoutForCapacity`. pub fn init(buf: OffsetBuf, layout: Layout) Self { - assert(@intFromPtr(buf.start()) % base_align == 0); + assert(base_align.check(@intFromPtr(buf.start()))); // Get all our main pointers const metadata_buf = buf.rebase(@sizeOf(Header)); @@ -862,7 +866,11 @@ fn HashMapUnmanaged( // Our total memory size required is the end of our values // aligned to the base required alignment. - const total_size = std.mem.alignForward(usize, vals_end, base_align); + const total_size = std.mem.alignForward( + usize, + vals_end, + base_align.toByteUnits(), + ); // The offsets we actually store in the map are from the // metadata pointer so that we can use self.metadata as @@ -1126,15 +1134,15 @@ test "HashMap put and remove loop in random order" { defer alloc.free(buf); var map = Map.init(.init(buf), layout); - var keys = std.ArrayList(u32).init(alloc); - defer keys.deinit(); + var keys: std.ArrayList(u32) = .empty; + defer keys.deinit(alloc); const size = 32; const iterations = 100; var i: u32 = 0; while (i < size) : (i += 1) { - try keys.append(i); + try keys.append(alloc, i); } var prng = std.Random.DefaultPrng.init(0); const random = prng.random(); diff --git a/src/terminal/kitty/color.zig b/src/terminal/kitty/color.zig index b23e30ad8..099002f39 100644 --- a/src/terminal/kitty/color.zig +++ b/src/terminal/kitty/color.zig @@ -42,13 +42,8 @@ pub const Kind = union(enum) { pub fn format( self: Kind, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { - _ = layout; - _ = opts; - switch (self) { .palette => |p| try writer.print("{d}", .{p}), .special => |s| try writer.print("{s}", .{@tagName(s)}), @@ -61,11 +56,11 @@ test "OSC: kitty color protocol kind string" { var buf: [256]u8 = undefined; { - const actual = try std.fmt.bufPrint(&buf, "{}", .{Kind{ .special = .foreground }}); + const actual = try std.fmt.bufPrint(&buf, "{f}", .{Kind{ .special = .foreground }}); try testing.expectEqualStrings("foreground", actual); } { - const actual = try std.fmt.bufPrint(&buf, "{}", .{Kind{ .palette = 42 }}); + const actual = try std.fmt.bufPrint(&buf, "{f}", .{Kind{ .palette = 42 }}); try testing.expectEqualStrings("42", actual); } } diff --git a/src/terminal/kitty/graphics_command.zig b/src/terminal/kitty/graphics_command.zig index dcb4850c9..99a7cdaac 100644 --- a/src/terminal/kitty/graphics_command.zig +++ b/src/terminal/kitty/graphics_command.zig @@ -59,7 +59,7 @@ pub const Parser = struct { errdefer arena.deinit(); var result: Parser = .{ .arena = arena, - .data = std.ArrayList(u8).init(alloc), + .data = .empty, .kv = .{}, .kv_temp_len = 0, .kv_current = 0, @@ -77,8 +77,8 @@ pub const Parser = struct { pub fn deinit(self: *Parser) void { // We don't free the hash map because its in the arena + self.data.deinit(self.arena.child_allocator); self.arena.deinit(); - self.data.deinit(); } /// Parse a complete command string. @@ -86,7 +86,7 @@ pub const Parser = struct { var parser = init(alloc); defer parser.deinit(); for (data) |c| try parser.feed(c); - return try parser.complete(); + return try parser.complete(alloc); } /// Feed a single byte to the parser. @@ -136,7 +136,7 @@ pub const Parser = struct { else => {}, }, - .data => try self.data.append(c), + .data => try self.data.append(self.arena.child_allocator, c), } } @@ -145,7 +145,7 @@ pub const Parser = struct { /// /// The allocator given will be used for the long-lived data /// of the final command. - pub fn complete(self: *Parser) !Command { + pub fn complete(self: *Parser, alloc: Allocator) !Command { switch (self.state) { // We can't ever end in the control key state and be valid. // This means the command looked something like "a=1,b" @@ -194,14 +194,14 @@ pub const Parser = struct { return .{ .control = control, .quiet = quiet, - .data = try self.decodeData(), + .data = try self.decodeData(alloc), }; } /// Decodes the payload data from base64 and returns it as a slice. /// This function will destroy the contents of self.data, it should /// only be used once we are done collecting payload bytes. - fn decodeData(self: *Parser) ![]const u8 { + fn decodeData(self: *Parser, alloc: Allocator) ![]const u8 { if (self.data.items.len == 0) { return ""; } @@ -225,7 +225,7 @@ pub const Parser = struct { // Remove the extra bytes. self.data.items.len = decoded.len; - return try self.data.toOwnedSlice(); + return try self.data.toOwnedSlice(alloc); } fn accumulateValue(self: *Parser, c: u8, overflow_state: State) !void { @@ -276,7 +276,7 @@ pub const Response = struct { placement_id: u32 = 0, message: []const u8 = "OK", - pub fn encode(self: Response, writer: anytype) !void { + pub fn encode(self: Response, writer: *std.Io.Writer) !void { // We only encode a result if we have either an id or an image number. if (self.id == 0 and self.image_number == 0) return; @@ -969,7 +969,7 @@ test "transmission command" { const input = "f=24,s=10,v=20"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .transmit); @@ -987,7 +987,7 @@ test "transmission ignores 'm' if medium is not direct" { const input = "a=t,t=t,m=1"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .transmit); @@ -1004,7 +1004,7 @@ test "transmission respects 'm' if medium is direct" { const input = "a=t,t=d,m=1"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .transmit); @@ -1021,7 +1021,7 @@ test "query command" { const input = "i=31,s=1,v=1,a=q,t=d,f=24;QUFBQQ"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .query); @@ -1041,7 +1041,7 @@ test "display command" { const input = "a=p,U=1,i=31,c=80,r=120"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .display); @@ -1059,7 +1059,7 @@ test "delete command" { const input = "a=d,d=p,x=3,y=4"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .delete); @@ -1079,7 +1079,7 @@ test "no control data" { const input = ";QUFBQQ"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .transmit); @@ -1094,7 +1094,7 @@ test "ignore unknown keys (long)" { const input = "f=24,s=10,v=20,hello=world"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .transmit); @@ -1112,7 +1112,7 @@ test "ignore very long values" { const input = "f=24,s=10,v=2000000000000000000000000000000000000000"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .transmit); @@ -1130,7 +1130,7 @@ test "ensure very large negative values don't get skipped" { const input = "a=p,i=1,z=-2000000000"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .display); @@ -1147,7 +1147,7 @@ test "ensure proper overflow error for u32" { const input = "a=p,i=10000000000"; for (input) |c| try p.feed(c); - try testing.expectError(error.Overflow, p.complete()); + try testing.expectError(error.Overflow, p.complete(alloc)); } test "ensure proper overflow error for i32" { @@ -1158,7 +1158,7 @@ test "ensure proper overflow error for i32" { const input = "a=p,i=1,z=-9999999999"; for (input) |c| try p.feed(c); - try testing.expectError(error.Overflow, p.complete()); + try testing.expectError(error.Overflow, p.complete(alloc)); } test "all i32 values" { @@ -1171,7 +1171,7 @@ test "all i32 values" { defer p.deinit(); const input = "a=p,i=1,z=-1"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .display); @@ -1186,7 +1186,7 @@ test "all i32 values" { defer p.deinit(); const input = "a=p,i=1,H=-1"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .display); @@ -1201,7 +1201,7 @@ test "all i32 values" { defer p.deinit(); const input = "a=p,i=1,V=-1"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .display); @@ -1214,41 +1214,41 @@ test "all i32 values" { test "response: encode nothing without ID or image number" { const testing = std.testing; var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); + var writer: std.Io.Writer = .fixed(&buf); var r: Response = .{}; - try r.encode(fbs.writer()); - try testing.expectEqualStrings("", fbs.getWritten()); + try r.encode(&writer); + try testing.expectEqualStrings("", writer.buffered()); } test "response: encode with only image id" { const testing = std.testing; var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); + var writer: std.Io.Writer = .fixed(&buf); var r: Response = .{ .id = 4 }; - try r.encode(fbs.writer()); - try testing.expectEqualStrings("\x1b_Gi=4;OK\x1b\\", fbs.getWritten()); + try r.encode(&writer); + try testing.expectEqualStrings("\x1b_Gi=4;OK\x1b\\", writer.buffered()); } test "response: encode with only image number" { const testing = std.testing; var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); + var writer: std.Io.Writer = .fixed(&buf); var r: Response = .{ .image_number = 4 }; - try r.encode(fbs.writer()); - try testing.expectEqualStrings("\x1b_GI=4;OK\x1b\\", fbs.getWritten()); + try r.encode(&writer); + try testing.expectEqualStrings("\x1b_GI=4;OK\x1b\\", writer.buffered()); } test "response: encode with image ID and number" { const testing = std.testing; var buf: [1024]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); + var writer: std.Io.Writer = .fixed(&buf); var r: Response = .{ .id = 12, .image_number = 4 }; - try r.encode(fbs.writer()); - try testing.expectEqualStrings("\x1b_Gi=12,I=4;OK\x1b\\", fbs.getWritten()); + try r.encode(&writer); + try testing.expectEqualStrings("\x1b_Gi=12,I=4;OK\x1b\\", writer.buffered()); } test "delete range command 1" { @@ -1259,7 +1259,7 @@ test "delete range command 1" { const input = "a=d,d=r,x=3,y=4"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .delete); @@ -1279,7 +1279,7 @@ test "delete range command 2" { const input = "a=d,d=R,x=5,y=11"; for (input) |c| try p.feed(c); - const command = try p.complete(); + const command = try p.complete(alloc); defer command.deinit(alloc); try testing.expect(command.control == .delete); @@ -1299,7 +1299,7 @@ test "delete range command 3" { const input = "a=d,d=R,x=5,y=4"; for (input) |c| try p.feed(c); - try testing.expectError(error.InvalidFormat, p.complete()); + try testing.expectError(error.InvalidFormat, p.complete(alloc)); } test "delete range command 4" { @@ -1310,7 +1310,7 @@ test "delete range command 4" { const input = "a=d,d=R,x=5"; for (input) |c| try p.feed(c); - try testing.expectError(error.InvalidFormat, p.complete()); + try testing.expectError(error.InvalidFormat, p.complete(alloc)); } test "delete range command 5" { @@ -1321,5 +1321,5 @@ test "delete range command 5" { const input = "a=d,d=R,y=5"; for (input) |c| try p.feed(c); - try testing.expectError(error.InvalidFormat, p.complete()); + try testing.expectError(error.InvalidFormat, p.complete(alloc)); } diff --git a/src/terminal/kitty/graphics_image.zig b/src/terminal/kitty/graphics_image.zig index f32b70be2..268f71601 100644 --- a/src/terminal/kitty/graphics_image.zig +++ b/src/terminal/kitty/graphics_image.zig @@ -259,15 +259,16 @@ pub const LoadingImage = struct { }; } - var buf_reader = std.io.bufferedReader(file.reader()); - const reader = buf_reader.reader(); + var buf: [4096]u8 = undefined; + var buf_reader = file.reader(&buf); + const reader = &buf_reader.interface; // Read the file - var managed = std.ArrayList(u8).init(alloc); - errdefer managed.deinit(); + var managed: std.ArrayList(u8) = .empty; + errdefer managed.deinit(alloc); const size: usize = if (t.size > 0) @min(t.size, max_size) else max_size; - reader.readAllArrayList(&managed, size) catch |err| { - log.warn("failed to read temporary file: {}", .{err}); + reader.appendRemaining(alloc, &managed, .limited(size)) catch { + log.warn("failed to read temporary file: {?}", .{buf_reader.err}); return error.InvalidData; }; @@ -402,14 +403,15 @@ pub const LoadingImage = struct { fn decompressZlib(self: *LoadingImage, alloc: Allocator) !void { // Open our zlib stream - var fbs = std.io.fixedBufferStream(self.data.items); - var stream = std.compress.zlib.decompressor(fbs.reader()); + var buf: [std.compress.flate.max_window_len]u8 = undefined; + var reader: std.Io.Reader = .fixed(self.data.items); + var stream: std.compress.flate.Decompress = .init(&reader, .zlib, &buf); // Write it to an array list - var list = std.ArrayList(u8).init(alloc); - errdefer list.deinit(); - stream.reader().readAllArrayList(&list, max_size) catch |err| { - log.warn("failed to read decompressed data: {}", .{err}); + var list: std.ArrayList(u8) = .empty; + errdefer list.deinit(alloc); + stream.reader.appendRemaining(alloc, &list, .limited(max_size)) catch { + log.warn("failed to read decompressed data: {?}", .{stream.err}); return error.DecompressionFailed; }; diff --git a/src/terminal/kitty/graphics_storage.zig b/src/terminal/kitty/graphics_storage.zig index 0c3022e4a..8aef0ece5 100644 --- a/src/terminal/kitty/graphics_storage.zig +++ b/src/terminal/kitty/graphics_storage.zig @@ -526,8 +526,8 @@ pub const ImageStorage = struct { used: bool, }; - var candidates = std.ArrayList(Candidate).init(alloc); - defer candidates.deinit(); + var candidates: std.ArrayList(Candidate) = .empty; + defer candidates.deinit(alloc); var it = self.images.iterator(); while (it.next()) |kv| { @@ -548,7 +548,7 @@ pub const ImageStorage = struct { break :used false; }; - try candidates.append(.{ + try candidates.append(alloc, .{ .id = img.id, .time = img.transmit_time, .used = used, diff --git a/src/terminal/kitty/key.zig b/src/terminal/kitty/key.zig index 0883c90f2..8594c4c39 100644 --- a/src/terminal/kitty/key.zig +++ b/src/terminal/kitty/key.zig @@ -8,7 +8,7 @@ const std = @import("std"); pub const FlagStack = struct { const len = 8; - flags: [len]Flags = @splat(.{}), + flags: [len]Flags = @splat(.disabled), idx: u3 = 0, /// Return the current stack value @@ -51,12 +51,12 @@ pub const FlagStack = struct { // could send a huge number of pop commands to waste cpu. if (n >= self.flags.len) { self.idx = 0; - self.flags = @splat(.{}); + self.flags = @splat(.disabled); return; } for (0..n) |_| { - self.flags[self.idx] = .{}; + self.flags[self.idx] = .disabled; self.idx -%= 1; } } @@ -83,6 +83,15 @@ pub const Flags = packed struct(u5) { report_all: bool = false, report_associated: bool = false, + /// Kitty keyboard protocol disabled (all flags off). + pub const disabled: Flags = .{ + .disambiguate = false, + .report_events = false, + .report_alternates = false, + .report_all = false, + .report_associated = false, + }; + /// Sets all modes on. pub const @"true": Flags = .{ .disambiguate = true, diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 4064c0c9c..59b5d0d53 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -1,8 +1,6 @@ const builtin = @import("builtin"); -const build_options = @import("terminal_options"); const charsets = @import("charsets.zig"); -const sanitize = @import("sanitize.zig"); const stream = @import("stream.zig"); const ansi = @import("ansi.zig"); const csi = @import("csi.zig"); @@ -21,7 +19,7 @@ pub const page = @import("page.zig"); pub const parse_table = @import("parse_table.zig"); pub const search = @import("search.zig"); pub const size = @import("size.zig"); -pub const tmux = if (build_options.tmux_control_mode) @import("tmux.zig") else struct {}; +pub const tmux = if (options.tmux_control_mode) @import("tmux.zig") else struct {}; pub const x11_color = @import("x11_color.zig"); pub const Charset = charsets.Charset; @@ -39,6 +37,7 @@ pub const Pin = PageList.Pin; pub const Point = point.Point; pub const Screen = @import("Screen.zig"); pub const ScreenType = Terminal.ScreenType; +pub const Scrollbar = PageList.Scrollbar; pub const Selection = @import("Selection.zig"); pub const SizeReportStyle = csi.SizeReportStyle; pub const StringMap = @import("StringMap.zig"); @@ -60,11 +59,11 @@ pub const EraseLine = csi.EraseLine; pub const TabClear = csi.TabClear; pub const Attribute = sgr.Attribute; -pub const isSafePaste = sanitize.isSafePaste; +pub const Options = @import("build_options.zig").Options; +pub const options = @import("terminal_options"); /// This is set to true when we're building the C library. -pub const is_c_lib = @import("root") == @import("../lib_vt.zig"); -pub const c_api = @import("c_api.zig"); +pub const c_api = if (options.c_abi) @import("c/main.zig") else void; test { @import("std").testing.refAllDecls(@This()); diff --git a/src/terminal/modes.zig b/src/terminal/modes.zig index 9a74db73c..13b7c1eac 100644 --- a/src/terminal/modes.zig +++ b/src/terminal/modes.zig @@ -43,7 +43,7 @@ pub const ModeState = struct { } /// Get the value of a mode. - pub fn get(self: *ModeState, mode: Mode) bool { + pub fn get(self: *const ModeState, mode: Mode) bool { switch (mode) { inline else => |mode_comptime| { const entry = comptime entryForMode(mode_comptime); diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index bd7337b42..f7324636a 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -7,54 +7,71 @@ const osc = @This(); const std = @import("std"); const builtin = @import("builtin"); +const build_options = @import("terminal_options"); const mem = std.mem; const assert = std.debug.assert; 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 log = std.log.scoped(.osc); -pub const Command = union(enum) { +pub const Command = union(Key) { /// This generally shouldn't ever be set except as an initial zero value. /// Ignore it. invalid, /// Set the window title of the terminal /// - /// If title mode 0 is set text is expect to be hex encoded (i.e. utf-8 + /// If title mode 0 is set text is expect to be hex encoded (i.e. utf-8 /// with each code unit further encoded with two hex digits). /// /// If title mode 2 is set or the terminal is setup for unconditional /// utf-8 titles text is interpreted as utf-8. Else text is interpreted /// as latin1. - change_window_title: []const u8, + change_window_title: [:0]const u8, /// Set the icon of the terminal window. The name of the icon is not /// well defined, so this is currently ignored by Ghostty at the time /// of writing this. We just parse it so that we don't get parse errors /// in the log. - change_window_icon: []const u8, + 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. - /// - /// "aid" is an optional "application identifier" that helps disambiguate - /// nested shell sessions. It can be anything but is usually a process ID. - /// - /// "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 prompt_start: struct { - aid: ?[]const u8 = null, + /// "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" @@ -70,7 +87,11 @@ pub const Command = union(enum) { /// 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: void, + 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. /// @@ -94,7 +115,7 @@ pub const Command = union(enum) { /// contents is set on the clipboard. clipboard_contents: struct { kind: u8, - data: []const u8, + data: [:0]const u8, }, /// OSC 7. Reports the current working directory of the shell. This is @@ -104,7 +125,7 @@ pub const Command = union(enum) { report_pwd: struct { /// The reported pwd value. This is not checked for validity. It should /// be a file URL but it is up to the caller to utilize this value. - value: []const u8, + value: [:0]const u8, }, /// OSC 22. Set the mouse shape. There doesn't seem to be a standard @@ -112,7 +133,7 @@ pub const Command = union(enum) { /// are moving towards using the W3C CSS cursor names. For OSC parsing, /// we just parse whatever string is given. mouse_shape: struct { - value: []const u8, + value: [:0]const u8, }, /// OSC color operations to set, reset, or report color settings. Some OSCs @@ -136,14 +157,14 @@ pub const Command = union(enum) { /// Show a desktop notification (OSC 9 or OSC 777) show_desktop_notification: struct { - title: []const u8, - body: []const u8, + title: [:0]const u8, + body: [:0]const u8, }, /// Start a hyperlink (OSC 8) hyperlink_start: struct { - id: ?[]const u8 = null, - uri: []const u8, + id: ?[:0]const u8 = null, + uri: [:0]const u8, }, /// End a hyperlink (OSC 8) @@ -155,12 +176,12 @@ pub const Command = union(enum) { }, /// ConEmu show GUI message box (OSC 9;2) - conemu_show_message_box: []const u8, + conemu_show_message_box: [:0]const u8, /// ConEmu change tab title (OSC 9;3) conemu_change_tab_title: union(enum) { reset, - value: []const u8, + value: [:0]const u8, }, /// ConEmu progress report (OSC 9;4) @@ -170,7 +191,35 @@ pub const Command = union(enum) { conemu_wait_input, /// ConEmu GUI macro (OSC 9;6) - conemu_guimacro: []const u8, + conemu_guimacro: [:0]const u8, + + pub const Key = LibEnum( + if (build_options.c_abi) .c else .zig, + // NOTE: Order matters, see LibEnum documentation. + &.{ + "invalid", + "change_window_title", + "change_window_icon", + "prompt_start", + "prompt_end", + "end_of_input", + "end_of_command", + "clipboard_contents", + "report_pwd", + "mouse_shape", + "color_operation", + "kitty_color_protocol", + "show_desktop_notification", + "hyperlink_start", + "hyperlink_end", + "conemu_sleep", + "conemu_show_message_box", + "conemu_change_tab_title", + "conemu_progress_report", + "conemu_wait_input", + "conemu_guimacro", + }, + ); pub const ProgressReport = struct { pub const State = enum(c_int) { @@ -244,7 +293,7 @@ pub const Terminator = enum { self: Terminator, comptime _: []const u8, _: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { try writer.writeAll(self.string()); } @@ -275,7 +324,7 @@ pub const Parser = struct { /// Temporary state that is dependent on the current state. temp_state: union { /// Current string parameter being populated - str: *[]const u8, + str: *[:0]const u8, /// Current numeric parameter being populated num: u16, @@ -395,9 +444,9 @@ pub const Parser = struct { conemu_guimacro, }; - pub fn init() Parser { + pub fn init(alloc: ?Allocator) Parser { var result: Parser = .{ - .alloc = null, + .alloc = alloc, .state = .empty, .command = .invalid, .buf_start = 0, @@ -420,18 +469,12 @@ pub const Parser = struct { return result; } - pub fn initAlloc(alloc: Allocator) Parser { - var result: Parser = .init(); - result.alloc = alloc; - return result; - } - /// This must be called to clean up any allocated memory. pub fn deinit(self: *Parser) void { self.reset(); } - /// Reset the parser start. + /// 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. @@ -445,7 +488,7 @@ pub const Parser = struct { // Some commands have their own memory management we need to clear. switch (self.command) { - .kitty_color_protocol => |*v| v.list.deinit(), + .kitty_color_protocol => |*v| v.list.deinit(self.alloc.?), .color_operation => |*v| v.requests.deinit(self.alloc.?), else => {}, } @@ -468,7 +511,10 @@ pub const Parser = struct { // 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. - if (self.buf_idx >= self.buf.len) { + // + // We always keep space for 1 byte at the end to null-terminate + // values. + if (self.buf_idx >= self.buf.len - 1) { if (self.state != .invalid) { log.warn( "OSC sequence too long (> {d}), ignoring. state={}", @@ -788,15 +834,15 @@ pub const Parser = struct { .@"21" => switch (c) { ';' => kitty: { - const alloc = self.alloc orelse { + 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 = std.ArrayList(kitty_color.OSC.Request).init(alloc), + .list = .empty, }, }; @@ -1007,7 +1053,8 @@ pub const Parser = struct { .notification_title => switch (c) { ';' => { - self.command.show_desktop_notification.title = self.buf[self.buf_start .. self.buf_idx - 1]; + 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; @@ -1252,7 +1299,7 @@ pub const Parser = struct { 'C' => { self.state = .semantic_option_start; - self.command = .{ .end_of_input = {} }; + self.command = .{ .end_of_input = .{} }; self.complete = true; }, @@ -1376,7 +1423,8 @@ pub const Parser = struct { fn endHyperlink(self: *Parser) void { switch (self.command) { .hyperlink_start => |*v| { - const value = self.buf[self.buf_start..self.buf_idx]; + 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; @@ -1390,10 +1438,12 @@ pub const Parser = struct { } fn endHyperlinkOptionValue(self: *Parser) void { - const value = if (self.buf_start == self.buf_idx) + const value: [:0]const u8 = if (self.buf_start == self.buf_idx) "" - else - self.buf[self.buf_start .. self.buf_idx - 1]; + 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) { @@ -1408,18 +1458,31 @@ pub const Parser = struct { } fn endSemanticOptionValue(self: *Parser) void { - const value = self.buf[self.buf_start..self.buf_idx]; + 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")) { - // 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. + // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers switch (self.command) { .prompt_start => |*v| { const valid = if (value.len == 1) valid: { @@ -1438,7 +1501,48 @@ pub const Parser = struct { }, 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. @@ -1465,7 +1569,9 @@ pub const Parser = struct { } fn endString(self: *Parser) void { - self.temp_state.str.* = self.buf[self.buf_start..self.buf_idx]; + 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 { @@ -1510,18 +1616,22 @@ pub const Parser = struct { 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(.{ .reset = key }) catch |err| { + v.list.append(alloc, .{ .reset = key }) catch |err| { log.warn("unable to append kitty color protocol option: {}", .{err}); return; }; } else if (mem.eql(u8, "?", value)) { - v.list.append(.{ .query = key }) catch |err| { + v.list.append(alloc, .{ .query = key }) catch |err| { log.warn("unable to append kitty color protocol option: {}", .{err}); return; }; } else { - v.list.append(.{ + v.list.append(alloc, .{ .set = .{ .key = key, .color = RGB.parse(value) catch |err| switch (err) { @@ -1559,15 +1669,25 @@ pub const Parser = struct { } fn endAllocableString(self: *Parser) void { + const alloc = self.alloc.?; const list = self.buf_dynamic.?; - self.temp_state.str.* = list.items; + list.append(alloc, 0) catch { + 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 /// the response terminator. - pub fn end(self: *Parser, terminator_ch: ?u8) ?Command { + /// + /// 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}", @@ -1626,7 +1746,7 @@ pub const Parser = struct { else => {}, } - return self.command; + return &self.command; } }; @@ -1634,36 +1754,91 @@ test { _ = osc_color; } -test "OSC: change_window_title" { +test "OSC 0: change_window_title" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); p.next('0'); p.next(';'); p.next('a'); p.next('b'); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .change_window_title); try testing.expectEqualStrings("ab", cmd.change_window_title); } -test "OSC: change_window_title with 2" { +test "OSC 0: longer than buffer" { const testing = std.testing; - var p: Parser = .init(); + 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).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .change_window_title); try testing.expectEqualStrings("ab", cmd.change_window_title); } -test "OSC: change_window_title with utf8" { +test "OSC 2: change_window_title with utf8" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); p.next('2'); p.next(';'); // '—' EM DASH U+2014 (E2 80 94) @@ -1677,812 +1852,26 @@ test "OSC: change_window_title with utf8" { p.next(0xE2); p.next(0x80); p.next(0x90); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .change_window_title); try testing.expectEqualStrings("— ‐", cmd.change_window_title); } -test "OSC: change_window_title empty" { +test "OSC 2: change_window_title empty" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); p.next('2'); p.next(';'); - const cmd = p.end(null).?; + const cmd = p.end(null).?.*; try testing.expect(cmd == .change_window_title); try testing.expectEqualStrings("", cmd.change_window_title); } -test "OSC: change_window_icon" { +test "OSC 4: empty param" { const testing = std.testing; - var p: Parser = .init(); - 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: prompt_start" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: prompt_start with single option" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: prompt_start with redraw disabled" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: prompt_start with redraw invalid value" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: prompt_start with continuation" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: prompt_start with secondary" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: end_of_command no exit code" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "133;D"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .end_of_command); -} - -test "OSC: end_of_command with exit code" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: prompt_end" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "133;B"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .prompt_end); -} - -test "OSC: end_of_input" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "133;C"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .end_of_input); -} - -test "OSC: get/set clipboard" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: get/set clipboard (optional parameter)" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: get/set clipboard with allocator" { - const testing = std.testing; - - var p: Parser = .initAlloc(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: clear clipboard" { - const testing = std.testing; - - var p: Parser = .init(); - 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); -} - -test "OSC: report pwd" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: report pwd empty" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: pointer cursor" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: longer than buffer" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;1 ConEmu sleep" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;1 ConEmu sleep with no value default to 100ms" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;1 conemu sleep cannot exceed 10000ms" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;1 conemu sleep invalid input" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;1 conemu sleep -> desktop notification 1" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;1 conemu sleep -> desktop notification 2" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9 show desktop notification" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9 show single character desktop notification" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 777 show desktop notification with title" { - const testing = std.testing; - - var p: Parser = .init(); - - 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"); -} - -test "OSC: OSC 9;2 ConEmu message box" { - const testing = std.testing; - - var p: Parser = .init(); - - 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(); - - 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(); - - 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(); - - 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: OSC 9;2 message box -> desktop notification 1" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;2 message box -> desktop notification 2" { - const testing = std.testing; - - var p: Parser = .init(); - - 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(); - - 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(); - - 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(); - - 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: OSC 9;3 change tab title -> desktop notification 1" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;3 message box -> desktop notification 2" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;4 ConEmu progress set" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;4 ConEmu progress set overflow" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;4 ConEmu progress set single digit" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;4 ConEmu progress set double digit" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;4 ConEmu progress set extra semicolon ignored" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;4 ConEmu progress remove with no progress" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;4 ConEmu progress remove with double semicolon" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;4 ConEmu progress remove ignores progress" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;4 ConEmu progress remove extra semicolon" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;4 ConEmu progress error" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;4 ConEmu progress error with progress" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;4 progress pause" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;4 ConEmu progress pause with progress" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;4 progress -> desktop notification 1" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;4 progress -> desktop notification 2" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;4 progress -> desktop notification 3" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;4 progress -> desktop notification 4" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: OSC 9;5 ConEmu wait input" { - const testing = std.testing; - - var p: Parser = .init(); - - const input = "9;5"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?; - try testing.expect(cmd == .conemu_wait_input); -} - -test "OSC: OSC 9;5 ConEmu wait ignores trailing characters" { - const testing = std.testing; - - var p: Parser = .init(); - - 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: empty param" { - const testing = std.testing; - - var p: Parser = .init(); + var p: Parser = .init(null); const input = "4;;"; for (input) |ch| p.next(ch); @@ -2491,93 +1880,122 @@ test "OSC: empty param" { try testing.expect(cmd == null); } -test "OSC: hyperlink" { +// 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(); + 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').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .hyperlink_start); try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); } -test "OSC: hyperlink with id set" { +test "OSC 8: hyperlink with id set" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "8;id=foo;http://example.com"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + 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: hyperlink with empty id" { +test "OSC 8: hyperlink with empty id" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "8;id=;http://example.com"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + 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: hyperlink with incomplete key" { +test "OSC 8: hyperlink with incomplete key" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "8;id;http://example.com"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + 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: hyperlink with empty key" { +test "OSC 8: hyperlink with empty key" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "8;=value;http://example.com"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + 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: hyperlink with empty key and id" { +test "OSC 8: hyperlink with empty key and id" { const testing = std.testing; - var p: Parser = .init(); + 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').?; + 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: hyperlink with empty uri" { +test "OSC 8: hyperlink with empty uri" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "8;id=foo;"; for (input) |ch| p.next(ch); @@ -2586,29 +2004,613 @@ test "OSC: hyperlink with empty uri" { try testing.expect(cmd == null); } -test "OSC: hyperlink end" { +test "OSC 8: hyperlink end" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); const input = "8;;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .hyperlink_end); } -test "OSC: kitty color protocol" { +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 = .initAlloc(testing.allocator); + 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').?; + 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); { @@ -2670,10 +2672,10 @@ test "OSC: kitty color protocol" { } } -test "OSC: kitty color protocol without allocator" { +test "OSC 21: kitty color protocol without allocator" { const testing = std.testing; - var p: Parser = .init(); + var p: Parser = .init(null); defer p.deinit(); const input = "21;foreground=?"; @@ -2681,32 +2683,32 @@ test "OSC: kitty color protocol without allocator" { try testing.expect(p.end('\x1b') == null); } -test "OSC: kitty color protocol double reset" { +test "OSC 21: kitty color protocol double reset" { const testing = std.testing; - var p: Parser = .initAlloc(testing.allocator); + 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').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .kitty_color_protocol); p.reset(); p.reset(); } -test "OSC: kitty color protocol reset after invalid" { +test "OSC 21: kitty color protocol reset after invalid" { const testing = std.testing; - var p: Parser = .initAlloc(testing.allocator); + 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').?; + const cmd = p.end('\x1b').?.*; try testing.expect(cmd == .kitty_color_protocol); p.reset(); @@ -2718,58 +2720,602 @@ test "OSC: kitty color protocol reset after invalid" { p.reset(); } -test "OSC: kitty color protocol no key" { +test "OSC 21: kitty color protocol no key" { const testing = std.testing; - var p: Parser = .initAlloc(testing.allocator); + var p: Parser = .init(testing.allocator); defer p.deinit(); const input = "21;"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + 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: 9;6: ConEmu guimacro 1" { +test "OSC 22: pointer cursor" { const testing = std.testing; - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); + var p: Parser = .init(null); - const input = "9;6;a"; + const input = "22;pointer"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; - try testing.expect(cmd == .conemu_guimacro); - try testing.expectEqualStrings("a", cmd.conemu_guimacro); + const cmd = p.end(null).?.*; + try testing.expect(cmd == .mouse_shape); + try testing.expectEqualStrings("pointer", cmd.mouse_shape.value); } -test "OSC: 9;6: ConEmu guimacro 2" { +test "OSC 52: get/set clipboard" { const testing = std.testing; - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); + var p: Parser = .init(null); - const input = "9;6;ab"; + const input = "52;s;?"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; - try testing.expect(cmd == .conemu_guimacro); - try testing.expectEqualStrings("ab", cmd.conemu_guimacro); + 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: 9;6: ConEmu guimacro 3 incomplete -> desktop notification" { +test "OSC 52: get/set clipboard (optional parameter)" { const testing = std.testing; - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); + var p: Parser = .init(null); - const input = "9;6"; + const input = "52;;?"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b').?; + 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("6", cmd.show_desktop_notification.body); + try testing.expectEqualStrings(cmd.show_desktop_notification.title, "Title"); + try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Body"); } diff --git a/src/terminal/osc/color.zig b/src/terminal/osc/color.zig index 8a8e8b942..9fd81ed63 100644 --- a/src/terminal/osc/color.zig +++ b/src/terminal/osc/color.zig @@ -279,7 +279,7 @@ pub const ColoredTarget = struct { color: RGB, }; -test "osc4" { +test "OSC 4:" { const testing = std.testing; const alloc = testing.allocator; @@ -401,7 +401,7 @@ test "osc4" { } } -test "osc5" { +test "OSC 5:" { const testing = std.testing; const alloc = testing.allocator; @@ -433,7 +433,7 @@ test "osc5" { } } -test "osc4: multiple requests" { +test "OSC 4: multiple requests" { const testing = std.testing; const alloc = testing.allocator; @@ -489,7 +489,7 @@ test "osc4: multiple requests" { } } -test "osc104" { +test "OSC 104:" { const testing = std.testing; const alloc = testing.allocator; @@ -540,7 +540,7 @@ test "osc104" { } } -test "osc104 empty index" { +test "OSC 104: empty index" { const testing = std.testing; const alloc = testing.allocator; @@ -557,7 +557,7 @@ test "osc104 empty index" { ); } -test "osc104 invalid index" { +test "OSC 104: invalid index" { const testing = std.testing; const alloc = testing.allocator; @@ -570,7 +570,7 @@ test "osc104 invalid index" { ); } -test "osc104 reset all" { +test "OSC 104: reset all" { const testing = std.testing; const alloc = testing.allocator; @@ -583,7 +583,7 @@ test "osc104 reset all" { ); } -test "osc105 reset all" { +test "OSC 105: reset all" { const testing = std.testing; const alloc = testing.allocator; @@ -597,7 +597,7 @@ test "osc105 reset all" { } // OSC 10-19: Get/Set Dynamic Colors -test "dynamic" { +test "OSC 10: OSC 11: OSC 12: OSC: 13: OSC 14: OSC 15: OSC: 16: OSC 17: OSC 18: OSC 19: dynamic" { const testing = std.testing; const alloc = testing.allocator; @@ -625,7 +625,7 @@ test "dynamic" { } } -test "dynamic multiple" { +test "OSC 10: OSC 11: OSC 12: OSC: 13: OSC 14: OSC 15: OSC: 16: OSC 17: OSC 18: OSC 19: dynamic multiple" { const testing = std.testing; const alloc = testing.allocator; @@ -657,7 +657,7 @@ test "dynamic multiple" { } // OSC 110-119: Reset Dynamic Colors -test "reset dynamic" { +test "OSC 110: OSC 111: OSC 112: OSC: 113: OSC 114: OSC 115: OSC: 116: OSC 117: OSC 118: OSC 119: reset dynamic" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/terminal/page.zig b/src/terminal/page.zig index b1a24e9a9..331168a27 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -86,7 +86,7 @@ pub const Page = struct { assert(std.heap.page_size_min % @max( @alignOf(Row), @alignOf(Cell), - style.Set.base_align, + style.Set.base_align.toByteUnits(), ) == 0); } @@ -191,7 +191,7 @@ pub const Page = struct { /// The backing memory is always allocated using mmap directly. /// You cannot use custom allocators with this structure because /// it is critical to performance that we use mmap. - pub fn init(cap: Capacity) !Page { + pub inline fn init(cap: Capacity) !Page { const l = layout(cap); // We use mmap directly to avoid Zig allocator overhead @@ -215,7 +215,7 @@ pub const Page = struct { /// Initialize a new page using the given backing memory. /// It is up to the caller to not call deinit on these pages. - pub fn initBuf(buf: OffsetBuf, l: Layout) Page { + pub inline fn initBuf(buf: OffsetBuf, l: Layout) Page { const cap = l.capacity; const rows = buf.member(Row, l.rows_start); const cells = buf.member(Cell, l.cells_start); @@ -270,13 +270,13 @@ pub const Page = struct { /// Deinitialize the page, freeing any backing memory. Do NOT call /// this if you allocated the backing memory yourself (i.e. you used /// initBuf). - pub fn deinit(self: *Page) void { + pub inline fn deinit(self: *Page) void { posix.munmap(self.memory); self.* = undefined; } /// Reinitialize the page with the same capacity. - pub fn reinit(self: *Page) void { + pub inline fn reinit(self: *Page) void { // We zero the page memory as u64 instead of u8 because // we can and it's empirically quite a bit faster. @memset(@as([*]u64, @ptrCast(self.memory))[0 .. self.memory.len / 8], 0); @@ -306,7 +306,7 @@ pub const Page = struct { /// Temporarily pause integrity checks. This is useful when you are /// doing a lot of operations that would trigger integrity check /// violations but you know the page will end up in a consistent state. - pub fn pauseIntegrityChecks(self: *Page, v: bool) void { + pub inline fn pauseIntegrityChecks(self: *Page, v: bool) void { if (build_options.slow_runtime_safety) { if (v) { self.pause_integrity_checks += 1; @@ -319,7 +319,7 @@ pub const Page = struct { /// A helper that can be used to assert the integrity of the page /// when runtime safety is enabled. This is a no-op when runtime /// safety is disabled. This uses the libc allocator. - pub fn assertIntegrity(self: *const Page) void { + pub inline fn assertIntegrity(self: *const Page) void { if (comptime build_options.slow_runtime_safety) { var debug_allocator: std.heap.DebugAllocator(.{}) = .init; defer _ = debug_allocator.deinit(); @@ -603,7 +603,7 @@ pub const Page = struct { /// Clone the contents of this page. This will allocate new memory /// using the page allocator. If you want to manage memory manually, /// use cloneBuf. - pub fn clone(self: *const Page) !Page { + pub inline fn clone(self: *const Page) !Page { const backing = try posix.mmap( null, self.memory.len, @@ -619,7 +619,7 @@ pub const Page = struct { /// Clone the entire contents of this page. /// /// The buffer must be at least the size of self.memory. - pub fn cloneBuf(self: *const Page, buf: []align(std.heap.page_size_min) u8) Page { + pub inline fn cloneBuf(self: *const Page, buf: []align(std.heap.page_size_min) u8) Page { assert(buf.len >= self.memory.len); // The entire concept behind a page is that everything is stored @@ -671,7 +671,7 @@ pub const Page = struct { /// If the other page has more columns, the extra columns will be /// truncated. If the other page has fewer columns, the extra columns /// will be zeroed. - pub fn cloneFrom( + pub inline fn cloneFrom( self: *Page, other: *const Page, y_start: usize, @@ -695,7 +695,7 @@ pub const Page = struct { } /// Clone a single row from another page into this page. - pub fn cloneRowFrom( + pub inline fn cloneRowFrom( self: *Page, other: *const Page, dst_row: *Row, @@ -912,13 +912,13 @@ pub const Page = struct { } /// Get a single row. y must be valid. - pub fn getRow(self: *const Page, y: usize) *Row { + pub inline fn getRow(self: *const Page, y: usize) *Row { assert(y < self.size.rows); return &self.rows.ptr(self.memory)[y]; } /// Get the cells for a row. - pub fn getCells(self: *const Page, row: *Row) []Cell { + pub inline fn getCells(self: *const Page, row: *Row) []Cell { if (build_options.slow_runtime_safety) { const rows = self.rows.ptr(self.memory); const cells = self.cells.ptr(self.memory); @@ -931,7 +931,7 @@ pub const Page = struct { } /// Get the row and cell for the given X/Y within this page. - pub fn getRowAndCell(self: *const Page, x: usize, y: usize) struct { + pub inline fn getRowAndCell(self: *const Page, x: usize, y: usize) struct { row: *Row, cell: *Cell, } { @@ -1016,7 +1016,7 @@ pub const Page = struct { } /// Swap two cells within the same row as quickly as possible. - pub fn swapCells( + pub inline fn swapCells( self: *Page, src: *Cell, dst: *Cell, @@ -1077,7 +1077,7 @@ pub const Page = struct { /// active, Page cannot know this and it will still be ref counted down. /// The best solution for this is to artificially increment the ref count /// prior to calling this function. - pub fn clearCells( + pub inline fn clearCells( self: *Page, row: *Row, left: usize, @@ -1127,14 +1127,14 @@ pub const Page = struct { } /// Returns the hyperlink ID for the given cell. - pub fn lookupHyperlink(self: *const Page, cell: *const Cell) ?hyperlink.Id { + pub inline fn lookupHyperlink(self: *const Page, cell: *const Cell) ?hyperlink.Id { const cell_offset = getOffset(Cell, self.memory, cell); const map = self.hyperlink_map.map(self.memory); return map.get(cell_offset); } /// Clear the hyperlink from the given cell. - pub fn clearHyperlink(self: *Page, row: *Row, cell: *Cell) void { + pub inline fn clearHyperlink(self: *Page, row: *Row, cell: *Cell) void { defer self.assertIntegrity(); // Get our ID @@ -1258,7 +1258,7 @@ pub const Page = struct { /// Caller is responsible for updating the refcount in the hyperlink /// set as necessary by calling `use` if the id was not acquired with /// `add`. - pub fn setHyperlink(self: *Page, row: *Row, cell: *Cell, id: hyperlink.Id) error{HyperlinkMapOutOfMemory}!void { + pub inline fn setHyperlink(self: *Page, row: *Row, cell: *Cell, id: hyperlink.Id) error{HyperlinkMapOutOfMemory}!void { defer self.assertIntegrity(); const cell_offset = getOffset(Cell, self.memory, cell); @@ -1300,7 +1300,7 @@ pub const Page = struct { /// Move the hyperlink from one cell to another. This can't fail /// because we avoid any allocations since we're just moving data. /// Destination must NOT have a hyperlink. - fn moveHyperlink(self: *Page, src: *Cell, dst: *Cell) void { + inline fn moveHyperlink(self: *Page, src: *Cell, dst: *Cell) void { assert(src.hyperlink); assert(!dst.hyperlink); @@ -1320,19 +1320,19 @@ pub const Page = struct { /// Returns the number of hyperlinks in the page. This isn't the byte /// size but the total number of unique cells that have hyperlink data. - pub fn hyperlinkCount(self: *const Page) usize { + pub inline fn hyperlinkCount(self: *const Page) usize { return self.hyperlink_map.map(self.memory).count(); } /// Returns the hyperlink capacity for the page. This isn't the byte /// size but the number of unique cells that can have hyperlink data. - pub fn hyperlinkCapacity(self: *const Page) usize { + pub inline fn hyperlinkCapacity(self: *const Page) usize { return self.hyperlink_map.map(self.memory).capacity(); } /// Set the graphemes for the given cell. This asserts that the cell /// has no graphemes set, and only contains a single codepoint. - pub fn setGraphemes( + pub inline fn setGraphemes( self: *Page, row: *Row, cell: *Cell, @@ -1433,7 +1433,7 @@ pub const Page = struct { /// Returns the codepoints for the given cell. These are the codepoints /// in addition to the first codepoint. The first codepoint is NOT /// included since it is on the cell itself. - pub fn lookupGrapheme(self: *const Page, cell: *const Cell) ?[]u21 { + pub inline fn lookupGrapheme(self: *const Page, cell: *const Cell) ?[]u21 { const cell_offset = getOffset(Cell, self.memory, cell); const map = self.grapheme_map.map(self.memory); const slice = map.get(cell_offset) orelse return null; @@ -1446,7 +1446,7 @@ pub const Page = struct { /// WARNING: This will NOT change the content_tag on the cells because /// there are scenarios where we want to move graphemes without changing /// the content tag. Callers beware but assertIntegrity should catch this. - fn moveGrapheme(self: *Page, src: *Cell, dst: *Cell) void { + inline fn moveGrapheme(self: *Page, src: *Cell, dst: *Cell) void { if (build_options.slow_runtime_safety) { assert(src.hasGrapheme()); assert(!dst.hasGrapheme()); @@ -1462,7 +1462,7 @@ pub const Page = struct { } /// Clear the graphemes for a given cell. - pub fn clearGrapheme(self: *Page, row: *Row, cell: *Cell) void { + pub inline fn clearGrapheme(self: *Page, row: *Row, cell: *Cell) void { defer self.assertIntegrity(); if (build_options.slow_runtime_safety) assert(cell.hasGrapheme()); @@ -1488,13 +1488,13 @@ pub const Page = struct { /// Returns the number of graphemes in the page. This isn't the byte /// size but the total number of unique cells that have grapheme data. - pub fn graphemeCount(self: *const Page) usize { + pub inline fn graphemeCount(self: *const Page) usize { return self.grapheme_map.map(self.memory).count(); } /// Returns the grapheme capacity for the page. This isn't the byte /// size but the number of unique cells that can have grapheme data. - pub fn graphemeCapacity(self: *const Page) usize { + pub inline fn graphemeCapacity(self: *const Page) usize { return self.grapheme_map.map(self.memory).capacity(); } @@ -1528,7 +1528,21 @@ pub const Page = struct { }; /// See cell_map - pub const CellMap = std.ArrayList(CellMapEntry); + pub const CellMap = struct { + alloc: Allocator, + map: std.ArrayList(CellMapEntry), + + pub fn init(alloc: Allocator) CellMap { + return .{ + .alloc = alloc, + .map = .empty, + }; + } + + pub fn deinit(self: *CellMap) void { + self.map.deinit(self.alloc); + } + }; /// The x/y coordinate of a single cell in the cell map. pub const CellMapEntry = struct { @@ -1547,7 +1561,7 @@ pub const Page = struct { /// it makes it easier to test input contents. pub fn encodeUtf8( self: *const Page, - writer: anytype, + writer: *std.Io.Writer, opts: EncodeUtf8Options, ) anyerror!EncodeUtf8Options.TrailingUtf8State { var blank_rows: usize = opts.preceding.rows; @@ -1583,7 +1597,7 @@ pub const Page = struct { // This is tested in Screen.zig, i.e. one test is // "cell map with newlines" if (opts.cell_map) |cell_map| { - try cell_map.append(.{ + try cell_map.map.append(cell_map.alloc, .{ .x = last_x, .y = @intCast(y - blank_rows + i - 1), }); @@ -1618,9 +1632,9 @@ pub const Page = struct { continue; } if (blank_cells > 0) { - try writer.writeByteNTimes(' ', blank_cells); + try writer.splatByteAll(' ', blank_cells); if (opts.cell_map) |cell_map| { - for (0..blank_cells) |i| try cell_map.append(.{ + for (0..blank_cells) |i| try cell_map.map.append(cell_map.alloc, .{ .x = @intCast(x - blank_cells + i), .y = y, }); @@ -1634,7 +1648,7 @@ pub const Page = struct { try writer.print("{u}", .{cell.content.codepoint}); if (opts.cell_map) |cell_map| { last_x = x + 1; - try cell_map.append(.{ + try cell_map.map.append(cell_map.alloc, .{ .x = x, .y = y, }); @@ -1645,7 +1659,7 @@ pub const Page = struct { try writer.print("{u}", .{cell.content.codepoint}); if (opts.cell_map) |cell_map| { last_x = x + 1; - try cell_map.append(.{ + try cell_map.map.append(cell_map.alloc, .{ .x = x, .y = y, }); @@ -1653,7 +1667,7 @@ pub const Page = struct { for (self.lookupGrapheme(cell).?) |cp| { try writer.print("{u}", .{cp}); - if (opts.cell_map) |cell_map| try cell_map.append(.{ + if (opts.cell_map) |cell_map| try cell_map.map.append(cell_map.alloc, .{ .x = x, .y = y, }); @@ -1676,7 +1690,7 @@ pub const Page = struct { /// The returned value is a DynamicBitSetUnmanaged but it is NOT /// actually dynamic; do NOT call resize on this. It is safe to /// read and write but do not resize it. - pub fn dirtyBitSet(self: *const Page) std.DynamicBitSetUnmanaged { + pub inline fn dirtyBitSet(self: *const Page) std.DynamicBitSetUnmanaged { return .{ .bit_length = self.capacity.rows, .masks = self.dirty.ptr(self.memory), @@ -1686,14 +1700,14 @@ pub const Page = struct { /// Returns true if the given row is dirty. This is NOT very /// efficient if you're checking many rows and you should use /// dirtyBitSet directly instead. - pub fn isRowDirty(self: *const Page, y: usize) bool { + pub inline fn isRowDirty(self: *const Page, y: usize) bool { return self.dirtyBitSet().isSet(y); } /// Returns true if this page is dirty at all. If you plan on /// checking any additional rows, you should use dirtyBitSet and /// check this on your own so you have the set available. - pub fn isDirty(self: *const Page) bool { + pub inline fn isDirty(self: *const Page) bool { return self.dirtyBitSet().findFirstSet() != null; } @@ -1722,7 +1736,7 @@ pub const Page = struct { /// The memory layout for a page given a desired minimum cols /// and rows size. - pub fn layout(cap: Capacity) Layout { + pub inline fn layout(cap: Capacity) Layout { const rows_count: usize = @intCast(cap.rows); const rows_start = 0; const rows_end: usize = rows_start + (rows_count * @sizeOf(Row)); @@ -1743,25 +1757,25 @@ pub const Page = struct { const dirty_end: usize = dirty_start + (dirty_usize_length * @sizeOf(usize)); const styles_layout: style.Set.Layout = .init(cap.styles); - const styles_start = alignForward(usize, dirty_end, style.Set.base_align); + const styles_start = alignForward(usize, dirty_end, style.Set.base_align.toByteUnits()); const styles_end = styles_start + styles_layout.total_size; const grapheme_alloc_layout = GraphemeAlloc.layout(cap.grapheme_bytes); - const grapheme_alloc_start = alignForward(usize, styles_end, GraphemeAlloc.base_align); + 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_map_layout = GraphemeMap.layout(@intCast(grapheme_count)); - const grapheme_map_start = alignForward(usize, grapheme_alloc_end, GraphemeMap.base_align); + 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; const string_layout = StringAlloc.layout(cap.string_bytes); - const string_start = alignForward(usize, grapheme_map_end, StringAlloc.base_align); + const string_start = alignForward(usize, grapheme_map_end, StringAlloc.base_align.toByteUnits()); const string_end = string_start + string_layout.total_size; const hyperlink_count = @divFloor(cap.hyperlink_bytes, @sizeOf(hyperlink.Set.Item)); const hyperlink_set_layout: hyperlink.Set.Layout = .init(@intCast(hyperlink_count)); - const hyperlink_set_start = alignForward(usize, string_end, hyperlink.Set.base_align); + const hyperlink_set_start = alignForward(usize, string_end, hyperlink.Set.base_align.toByteUnits()); const hyperlink_set_end = hyperlink_set_start + hyperlink_set_layout.total_size; const hyperlink_map_count: u32 = count: { @@ -1773,7 +1787,7 @@ pub const Page = struct { break :count std.math.ceilPowerOfTwoAssert(u32, mult); }; const hyperlink_map_layout = hyperlink.Map.layout(hyperlink_map_count); - const hyperlink_map_start = alignForward(usize, hyperlink_set_end, hyperlink.Map.base_align); + const hyperlink_map_start = alignForward(usize, hyperlink_set_end, hyperlink.Map.base_align.toByteUnits()); const hyperlink_map_end = hyperlink_map_start + hyperlink_map_layout.total_size; const total_size = alignForward(usize, hyperlink_map_end, std.heap.page_size_min); @@ -1867,12 +1881,12 @@ pub const Capacity = struct { // 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); - const hyperlink_set_start = alignBackward(usize, hyperlink_map_start - layout.hyperlink_set_layout.total_size, hyperlink.Set.base_align); - const string_alloc_start = alignBackward(usize, hyperlink_set_start - layout.string_alloc_layout.total_size, StringAlloc.base_align); - const grapheme_map_start = alignBackward(usize, string_alloc_start - layout.grapheme_map_layout.total_size, GraphemeMap.base_align); - const grapheme_alloc_start = alignBackward(usize, grapheme_map_start - layout.grapheme_alloc_layout.total_size, GraphemeAlloc.base_align); - const styles_start = alignBackward(usize, grapheme_alloc_start - layout.styles_layout.total_size, style.Set.base_align); + 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, style.Set.base_align.toByteUnits()); // The size per row is: // - The row metadata itself diff --git a/src/terminal/point.zig b/src/terminal/point.zig index f2544f90c..e7e2a8840 100644 --- a/src/terminal/point.zig +++ b/src/terminal/point.zig @@ -56,7 +56,7 @@ pub const Point = union(Tag) { screen: Coordinate, history: Coordinate, - pub fn coord(self: Point) Coordinate { + pub inline fn coord(self: Point) Coordinate { return switch (self) { .active, .viewport, diff --git a/src/terminal/ref_counted_set.zig b/src/terminal/ref_counted_set.zig index 153e331a6..e07de4e97 100644 --- a/src/terminal/ref_counted_set.zig +++ b/src/terminal/ref_counted_set.zig @@ -59,12 +59,12 @@ pub fn RefCountedSet( return struct { const Self = @This(); - pub const base_align = @max( + pub const base_align: std.mem.Alignment = .fromByteUnits(@max( @alignOf(Context), @alignOf(Layout), @alignOf(Item), @alignOf(Id), - ); + )); /// Set item pub const Item = struct { diff --git a/src/terminal/sanitize.zig b/src/terminal/sanitize.zig deleted file mode 100644 index f96e8a00e..000000000 --- a/src/terminal/sanitize.zig +++ /dev/null @@ -1,14 +0,0 @@ -const std = @import("std"); - -/// Returns true if the data looks safe to paste. -pub fn isSafePaste(data: []const u8) bool { - return std.mem.indexOf(u8, data, "\n") == null and - std.mem.indexOf(u8, data, "\x1b[201~") == null; -} - -test isSafePaste { - const testing = std.testing; - try testing.expect(isSafePaste("hello")); - try testing.expect(!isSafePaste("hello\n")); - try testing.expect(!isSafePaste("hello\nworld")); -} diff --git a/src/terminal/search.zig b/src/terminal/search.zig index b3c6494a3..d9f6c5663 100644 --- a/src/terminal/search.zig +++ b/src/terminal/search.zig @@ -55,7 +55,7 @@ pub const PageListSearch = struct { needle: []const u8, ) Allocator.Error!PageListSearch { var window = try SlidingWindow.init(alloc, needle); - errdefer window.deinit(alloc); + errdefer window.deinit(); return .{ .list = list, @@ -63,16 +63,13 @@ pub const PageListSearch = struct { }; } - pub fn deinit(self: *PageListSearch, alloc: Allocator) void { - self.window.deinit(alloc); + pub fn deinit(self: *PageListSearch) void { + self.window.deinit(); } /// Find the next match for the needle in the pagelist. This returns /// null when there are no more matches. - pub fn next( - self: *PageListSearch, - alloc: Allocator, - ) Allocator.Error!?Selection { + pub fn next(self: *PageListSearch) Allocator.Error!?Selection { // Try to search for the needle in the window. If we find a match // then we can return that and we're done. if (self.window.next()) |sel| return sel; @@ -89,7 +86,7 @@ pub const PageListSearch = struct { // until we find a match or we reach the end of the pagelist. // This append then next pattern limits memory usage of the window. while (node_) |node| : (node_ = node.next) { - try self.window.append(alloc, node); + try self.window.append(node); if (self.window.next()) |sel| return sel; } @@ -115,6 +112,14 @@ pub const PageListSearch = struct { /// and repeat the process. This will always maintain the minimum /// required memory to search for the needle. const SlidingWindow = struct { + /// The allocator to use for all the data within this window. We + /// store this rather than passing it around because its already + /// part of multiple elements (eg. Meta's CellMap) and we want to + /// ensure we always use a consistent allocator. Additionally, only + /// a small amount of sliding windows are expected to be in use + /// at any one time so the memory overhead isn't that large. + alloc: Allocator, + /// The data buffer is a circular buffer of u8 that contains the /// encoded page text that we can use to search for the needle. data: DataBuf, @@ -163,6 +168,7 @@ const SlidingWindow = struct { errdefer alloc.free(overlap_buf); return .{ + .alloc = alloc, .data = data, .meta = meta, .needle = needle, @@ -170,13 +176,13 @@ const SlidingWindow = struct { }; } - pub fn deinit(self: *SlidingWindow, alloc: Allocator) void { - alloc.free(self.overlap_buf); - self.data.deinit(alloc); + pub fn deinit(self: *SlidingWindow) void { + self.alloc.free(self.overlap_buf); + self.data.deinit(self.alloc); var meta_it = self.meta.iterator(.forward); while (meta_it.next()) |meta| meta.deinit(); - self.meta.deinit(alloc); + self.meta.deinit(self.alloc); } /// Clear all data but retain allocated capacity. @@ -206,7 +212,10 @@ const SlidingWindow = struct { // Search the first slice for the needle. if (std.mem.indexOf(u8, slices[0], self.needle)) |idx| { - return self.selection(idx, self.needle.len); + return self.selection( + idx, + self.needle.len, + ); } // Search the overlap buffer for the needle. @@ -244,7 +253,10 @@ const SlidingWindow = struct { // Search the last slice for the needle. if (std.mem.indexOf(u8, slices[1], self.needle)) |idx| { - return self.selection(slices[0].len + idx, self.needle.len); + return self.selection( + slices[0].len + idx, + self.needle.len, + ); } // No match. We keep `needle.len - 1` bytes available to @@ -254,15 +266,15 @@ const SlidingWindow = struct { var saved: usize = 0; while (meta_it.next()) |meta| { const needed = self.needle.len - 1 - saved; - if (meta.cell_map.items.len >= needed) { + if (meta.cell_map.map.items.len >= needed) { // We save up to this meta. We set our data offset // to exactly where it needs to be to continue // searching. - self.data_offset = meta.cell_map.items.len - needed; + self.data_offset = meta.cell_map.map.items.len - needed; break; } - saved += meta.cell_map.items.len; + saved += meta.cell_map.map.items.len; } else { // If we exited the while loop naturally then we // never got the amount we needed and so there is @@ -284,7 +296,7 @@ const SlidingWindow = struct { var prune_data_len: usize = 0; for (0..prune_count) |_| { const meta = meta_it.next().?; - prune_data_len += meta.cell_map.items.len; + prune_data_len += meta.cell_map.map.items.len; meta.deinit(); } self.meta.deleteOldest(prune_count); @@ -384,16 +396,16 @@ const SlidingWindow = struct { // meta_i is the index we expect to find the match in the // cell map within this meta if it contains it. const meta_i = idx - offset.*; - if (meta_i >= meta.cell_map.items.len) { + if (meta_i >= meta.cell_map.map.items.len) { // This meta doesn't contain the match. This means we // can also prune this set of data because we only look // forward. - offset.* += meta.cell_map.items.len; + offset.* += meta.cell_map.map.items.len; continue; } // We found the meta that contains the start of the match. - const map = meta.cell_map.items[meta_i]; + const map = meta.cell_map.map.items[meta_i]; return .{ .node = meta.node, .y = map.y, @@ -411,13 +423,15 @@ const SlidingWindow = struct { /// via a search (via next()). pub fn append( self: *SlidingWindow, - alloc: Allocator, node: *PageList.List.Node, ) Allocator.Error!void { // Initialize our metadata for the node. var meta: Meta = .{ .node = node, - .cell_map = .init(alloc), + .cell_map = .{ + .alloc = self.alloc, + .map = .empty, + }, }; errdefer meta.deinit(); @@ -425,27 +439,27 @@ const SlidingWindow = struct { // temporary memory, and then copy it into our circular buffer. // In the future, we should benchmark and see if we can encode // directly into the circular buffer. - var encoded: std.ArrayListUnmanaged(u8) = .{}; - defer encoded.deinit(alloc); + var encoded: std.Io.Writer.Allocating = .init(self.alloc); + defer encoded.deinit(); // Encode the page into the buffer. const page: *const Page = &meta.node.data; _ = page.encodeUtf8( - encoded.writer(alloc), + &encoded.writer, .{ .cell_map = &meta.cell_map }, ) catch { // writer uses anyerror but the only realistic error on // an ArrayList is out of memory. return error.OutOfMemory; }; - assert(meta.cell_map.items.len == encoded.items.len); + assert(meta.cell_map.map.items.len == encoded.written().len); // Ensure our buffers are big enough to store what we need. - try self.data.ensureUnusedCapacity(alloc, encoded.items.len); - try self.meta.ensureUnusedCapacity(alloc, 1); + try self.data.ensureUnusedCapacity(self.alloc, encoded.written().len); + try self.meta.ensureUnusedCapacity(self.alloc, 1); // Append our new node to the circular buffer. - try self.data.appendSlice(encoded.items); + try self.data.appendSlice(encoded.written()); try self.meta.append(meta); self.assertIntegrity(); @@ -462,7 +476,7 @@ const SlidingWindow = struct { // Integrity check: verify our data matches our metadata exactly. var meta_it = self.meta.iterator(.forward); var data_len: usize = 0; - while (meta_it.next()) |m| data_len += m.cell_map.items.len; + while (meta_it.next()) |m| data_len += m.cell_map.map.items.len; assert(data_len == self.data.len()); // Integrity check: verify our data offset is within bounds. @@ -480,11 +494,11 @@ test "PageListSearch single page" { try testing.expect(s.pages.pages.first == s.pages.pages.last); var search = try PageListSearch.init(alloc, &s.pages, "boo!"); - defer search.deinit(alloc); + defer search.deinit(); // We should be able to find two matches. { - const sel = (try search.next(alloc)).?; + const sel = (try search.next()).?; try testing.expectEqual(point.Point{ .active = .{ .x = 7, .y = 0, @@ -495,7 +509,7 @@ test "PageListSearch single page" { } }, s.pages.pointFromPin(.active, sel.end()).?); } { - const sel = (try search.next(alloc)).?; + const sel = (try search.next()).?; try testing.expectEqual(point.Point{ .active = .{ .x = 19, .y = 0, @@ -505,8 +519,8 @@ test "PageListSearch single page" { .y = 0, } }, s.pages.pointFromPin(.active, sel.end()).?); } - try testing.expect((try search.next(alloc)) == null); - try testing.expect((try search.next(alloc)) == null); + try testing.expect((try search.next()) == null); + try testing.expect((try search.next()) == null); } test "SlidingWindow empty on init" { @@ -514,7 +528,7 @@ test "SlidingWindow empty on init" { const alloc = testing.allocator; var w = try SlidingWindow.init(alloc, "boo!"); - defer w.deinit(alloc); + defer w.deinit(); try testing.expectEqual(0, w.data.len()); try testing.expectEqual(0, w.meta.len()); } @@ -524,7 +538,7 @@ test "SlidingWindow single append" { const alloc = testing.allocator; var w = try SlidingWindow.init(alloc, "boo!"); - defer w.deinit(alloc); + defer w.deinit(); var s = try Screen.init(alloc, 80, 24, 0); defer s.deinit(); @@ -533,7 +547,7 @@ test "SlidingWindow single append" { // We want to test single-page cases. try testing.expect(s.pages.pages.first == s.pages.pages.last); const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node); + try w.append(node); // We should be able to find two matches. { @@ -567,7 +581,7 @@ test "SlidingWindow single append no match" { const alloc = testing.allocator; var w = try SlidingWindow.init(alloc, "nope!"); - defer w.deinit(alloc); + defer w.deinit(); var s = try Screen.init(alloc, 80, 24, 0); defer s.deinit(); @@ -576,7 +590,7 @@ test "SlidingWindow single append no match" { // We want to test single-page cases. try testing.expect(s.pages.pages.first == s.pages.pages.last); const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node); + try w.append(node); // No matches try testing.expect(w.next() == null); @@ -591,7 +605,7 @@ test "SlidingWindow two pages" { const alloc = testing.allocator; var w = try SlidingWindow.init(alloc, "boo!"); - defer w.deinit(alloc); + defer w.deinit(); var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit(); @@ -609,8 +623,8 @@ test "SlidingWindow two pages" { // Add both pages const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node); - try w.append(alloc, node.next.?); + try w.append(node); + try w.append(node.next.?); // Search should find two matches { @@ -644,7 +658,7 @@ test "SlidingWindow two pages match across boundary" { const alloc = testing.allocator; var w = try SlidingWindow.init(alloc, "hello, world"); - defer w.deinit(alloc); + defer w.deinit(); var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit(); @@ -661,8 +675,8 @@ test "SlidingWindow two pages match across boundary" { // Add both pages const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node); - try w.append(alloc, node.next.?); + try w.append(node); + try w.append(node.next.?); // Search should find a match { @@ -688,7 +702,7 @@ test "SlidingWindow two pages no match prunes first page" { const alloc = testing.allocator; var w = try SlidingWindow.init(alloc, "nope!"); - defer w.deinit(alloc); + defer w.deinit(); var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit(); @@ -706,8 +720,8 @@ test "SlidingWindow two pages no match prunes first page" { // Add both pages const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node); - try w.append(alloc, node.next.?); + try w.append(node); + try w.append(node.next.?); // Search should find nothing try testing.expect(w.next() == null); @@ -737,18 +751,18 @@ test "SlidingWindow two pages no match keeps both pages" { try s.testWriteString("hello. boo!"); // Imaginary needle for search. Doesn't match! - var needle_list = std.ArrayList(u8).init(alloc); - defer needle_list.deinit(); - try needle_list.appendNTimes('x', first_page_rows * s.pages.cols); + var needle_list: std.ArrayList(u8) = .empty; + defer needle_list.deinit(alloc); + try needle_list.appendNTimes(alloc, 'x', first_page_rows * s.pages.cols); const needle: []const u8 = needle_list.items; var w = try SlidingWindow.init(alloc, needle); - defer w.deinit(alloc); + defer w.deinit(); // Add both pages const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node); - try w.append(alloc, node.next.?); + try w.append(node); + try w.append(node.next.?); // Search should find nothing try testing.expect(w.next() == null); @@ -763,7 +777,7 @@ test "SlidingWindow single append across circular buffer boundary" { const alloc = testing.allocator; var w = try SlidingWindow.init(alloc, "abc"); - defer w.deinit(alloc); + defer w.deinit(); var s = try Screen.init(alloc, 80, 24, 0); defer s.deinit(); @@ -776,8 +790,8 @@ test "SlidingWindow single append across circular buffer boundary" { // our implementation changes our test will fail. try testing.expect(s.pages.pages.first == s.pages.pages.last); const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node); - try w.append(alloc, node); + try w.append(node); + try w.append(node); { // No wrap around yet const slices = w.data.getPtrSlice(0, w.data.len()); @@ -793,7 +807,7 @@ test "SlidingWindow single append across circular buffer boundary" { w.needle = "boo"; // Add new page, now wraps - try w.append(alloc, node); + try w.append(node); { const slices = w.data.getPtrSlice(0, w.data.len()); try testing.expect(slices[0].len > 0); @@ -818,7 +832,7 @@ test "SlidingWindow single append match on boundary" { const alloc = testing.allocator; var w = try SlidingWindow.init(alloc, "abcd"); - defer w.deinit(alloc); + defer w.deinit(); var s = try Screen.init(alloc, 80, 24, 0); defer s.deinit(); @@ -831,8 +845,8 @@ test "SlidingWindow single append match on boundary" { // our implementation changes our test will fail. try testing.expect(s.pages.pages.first == s.pages.pages.last); const node: *PageList.List.Node = s.pages.pages.first.?; - try w.append(alloc, node); - try w.append(alloc, node); + try w.append(node); + try w.append(node); { // No wrap around yet const slices = w.data.getPtrSlice(0, w.data.len()); @@ -848,7 +862,7 @@ test "SlidingWindow single append match on boundary" { w.needle = "boo!"; // Add new page, now wraps - try w.append(alloc, node); + try w.append(node); { const slices = w.data.getPtrSlice(0, w.data.len()); try testing.expect(slices[0].len > 0); diff --git a/src/terminal/size.zig b/src/terminal/size.zig index 6cedfdf6d..8322ddb41 100644 --- a/src/terminal/size.zig +++ b/src/terminal/size.zig @@ -31,7 +31,7 @@ pub fn Offset(comptime T: type) type { }; /// Returns a pointer to the start of the data, properly typed. - pub fn ptr(self: Self, base: anytype) [*]T { + pub inline fn ptr(self: Self, base: anytype) [*]T { // The offset must be properly aligned for the type since // our return type is naturally aligned. We COULD modify this // to return arbitrary alignment, but its not something we need. diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index a58e01576..c85e72f0f 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -64,7 +64,7 @@ pub fn Stream(comptime Handler: type) type { } /// Process a string of characters. - pub fn nextSlice(self: *Self, input: []const u8) !void { + pub inline fn nextSlice(self: *Self, input: []const u8) !void { // Disable SIMD optimizations if build requests it or if our // manual debug mode is on. if (comptime debug or !build_options.simd) { @@ -87,7 +87,7 @@ pub fn Stream(comptime Handler: type) type { } } - fn nextSliceCapped(self: *Self, input: []const u8, cp_buf: []u32) !void { + inline fn nextSliceCapped(self: *Self, input: []const u8, cp_buf: []u32) !void { assert(input.len <= cp_buf.len); var offset: usize = 0; @@ -144,7 +144,7 @@ pub fn Stream(comptime Handler: type) type { /// /// Expects input to start with 0x1B, use consumeUntilGround first /// if the stream may be in the middle of an escape sequence. - fn consumeAllEscapes(self: *Self, input: []const u8) !usize { + inline fn consumeAllEscapes(self: *Self, input: []const u8) !usize { var offset: usize = 0; while (input[offset] == 0x1B) { self.parser.state = .escape; @@ -158,7 +158,7 @@ pub fn Stream(comptime Handler: type) type { /// Parses escape sequences until the parser reaches the ground state. /// Returns the number of bytes consumed from the provided input. - fn consumeUntilGround(self: *Self, input: []const u8) !usize { + inline fn consumeUntilGround(self: *Self, input: []const u8) !usize { var offset: usize = 0; while (self.parser.state != .ground) { if (offset >= input.len) return input.len; @@ -171,7 +171,7 @@ pub fn Stream(comptime Handler: type) type { /// Like nextSlice but takes one byte and is necessarily a scalar /// operation that can't use SIMD. Prefer nextSlice if you can and /// try to get multiple bytes at once. - pub fn next(self: *Self, c: u8) !void { + pub inline fn next(self: *Self, c: u8) !void { // The scalar path can be responsible for decoding UTF-8. if (self.parser.state == .ground) { try self.nextUtf8(c); @@ -185,7 +185,7 @@ pub fn Stream(comptime Handler: type) type { /// /// This assumes we're in the UTF-8 decoding state. If we may not /// be in the UTF-8 decoding state call nextSlice or next. - fn nextUtf8(self: *Self, c: u8) !void { + inline fn nextUtf8(self: *Self, c: u8) !void { assert(self.parser.state == .ground); const res = self.utf8decoder.next(c); @@ -278,16 +278,23 @@ pub fn Stream(comptime Handler: type) type { return; } - const actions = self.parser.next(c); + // We explicitly inline this call here for performance reasons. + // + // We do this rather than mark Parser.next as inline because doing + // that causes weird behavior in some tests- I'm not sure if they + // miscompile or it's just very counter-intuitive comptime stuff, + // but regardless, this is the easy solution. + const actions = @call(.always_inline, Parser.next, .{ &self.parser, c }); + for (actions) |action_opt| { const action = action_opt orelse continue; - if (comptime debug) log.info("action: {}", .{action}); + if (comptime debug) log.info("action: {f}", .{action}); // If this handler handles everything manually then we do nothing // if it can be processed. if (@hasDecl(T, "handleManually")) { const processed = self.handler.handleManually(action) catch |err| err: { - log.warn("error handling action manually err={} action={}", .{ + log.warn("error handling action manually err={} action={f}", .{ err, action, }); @@ -326,15 +333,15 @@ pub fn Stream(comptime Handler: type) type { } } - pub fn print(self: *Self, c: u21) !void { + pub inline fn print(self: *Self, c: u21) !void { if (@hasDecl(T, "print")) { try self.handler.print(c); } } - pub fn execute(self: *Self, c: u8) !void { + pub inline fn execute(self: *Self, c: u8) !void { const c0: ansi.C0 = @enumFromInt(c); - if (comptime debug) log.info("execute: {}", .{c0}); + if (comptime debug) log.info("execute: {f}", .{c0}); switch (c0) { // We ignore SOH/STX: https://github.com/microsoft/terminal/issues/10786 .NUL, .SOH, .STX => {}, @@ -383,7 +390,7 @@ pub fn Stream(comptime Handler: type) type { } } - fn csiDispatch(self: *Self, input: Parser.Action.CSI) !void { + inline fn csiDispatch(self: *Self, input: Parser.Action.CSI) !void { switch (input.final) { // CUU - Cursor Up 'A', 'k' => switch (input.intermediates.len) { @@ -392,12 +399,12 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid cursor up command: {}", .{input}); + log.warn("invalid cursor up command: {f}", .{input}); return; }, }, false, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI A with intermediates: {s}", @@ -412,12 +419,12 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid cursor down command: {}", .{input}); + log.warn("invalid cursor down command: {f}", .{input}); return; }, }, false, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI B with intermediates: {s}", @@ -432,11 +439,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid cursor right command: {}", .{input}); + log.warn("invalid cursor right command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI C with intermediates: {s}", @@ -451,11 +458,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid cursor left command: {}", .{input}); + log.warn("invalid cursor left command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI D with intermediates: {s}", @@ -470,12 +477,12 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid cursor up command: {}", .{input}); + log.warn("invalid cursor up command: {f}", .{input}); return; }, }, true, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI E with intermediates: {s}", @@ -490,12 +497,12 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid cursor down command: {}", .{input}); + log.warn("invalid cursor down command: {f}", .{input}); return; }, }, true, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI F with intermediates: {s}", @@ -509,8 +516,8 @@ pub fn Stream(comptime Handler: type) type { 0 => if (@hasDecl(T, "setCursorCol")) switch (input.params.len) { 0 => try self.handler.setCursorCol(1), 1 => try self.handler.setCursorCol(input.params[0]), - else => log.warn("invalid HPA command: {}", .{input}), - } else log.warn("unimplemented CSI callback: {}", .{input}), + else => log.warn("invalid HPA command: {f}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI G with intermediates: {s}", @@ -525,8 +532,8 @@ pub fn Stream(comptime Handler: type) type { 0 => try self.handler.setCursorPos(1, 1), 1 => try self.handler.setCursorPos(input.params[0], 1), 2 => try self.handler.setCursorPos(input.params[0], input.params[1]), - else => log.warn("invalid CUP command: {}", .{input}), - } else log.warn("unimplemented CSI callback: {}", .{input}), + else => log.warn("invalid CUP command: {f}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI H with intermediates: {s}", @@ -541,11 +548,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid horizontal tab command: {}", .{input}); + log.warn("invalid horizontal tab command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI I with intermediates: {s}", @@ -562,7 +569,7 @@ pub fn Stream(comptime Handler: type) type { }; const protected = protected_ orelse { - log.warn("invalid erase display command: {}", .{input}); + log.warn("invalid erase display command: {f}", .{input}); return; }; @@ -573,12 +580,12 @@ pub fn Stream(comptime Handler: type) type { }; const mode = mode_ orelse { - log.warn("invalid erase display command: {}", .{input}); + log.warn("invalid erase display command: {f}", .{input}); return; }; try self.handler.eraseDisplay(mode, protected); - } else log.warn("unimplemented CSI callback: {}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), // Erase Line 'K' => if (@hasDecl(T, "eraseLine")) { @@ -589,7 +596,7 @@ pub fn Stream(comptime Handler: type) type { }; const protected = protected_ orelse { - log.warn("invalid erase line command: {}", .{input}); + log.warn("invalid erase line command: {f}", .{input}); return; }; @@ -600,12 +607,12 @@ pub fn Stream(comptime Handler: type) type { }; const mode = mode_ orelse { - log.warn("invalid erase line command: {}", .{input}); + log.warn("invalid erase line command: {f}", .{input}); return; }; try self.handler.eraseLine(mode, protected); - } else log.warn("unimplemented CSI callback: {}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), // IL - Insert Lines // TODO: test @@ -613,8 +620,8 @@ pub fn Stream(comptime Handler: type) type { 0 => if (@hasDecl(T, "insertLines")) switch (input.params.len) { 0 => try self.handler.insertLines(1), 1 => try self.handler.insertLines(input.params[0]), - else => log.warn("invalid IL command: {}", .{input}), - } else log.warn("unimplemented CSI callback: {}", .{input}), + else => log.warn("invalid IL command: {f}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI L with intermediates: {s}", @@ -628,8 +635,8 @@ pub fn Stream(comptime Handler: type) type { 0 => if (@hasDecl(T, "deleteLines")) switch (input.params.len) { 0 => try self.handler.deleteLines(1), 1 => try self.handler.deleteLines(input.params[0]), - else => log.warn("invalid DL command: {}", .{input}), - } else log.warn("unimplemented CSI callback: {}", .{input}), + else => log.warn("invalid DL command: {f}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI M with intermediates: {s}", @@ -644,11 +651,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid delete characters command: {}", .{input}); + log.warn("invalid delete characters command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI P with intermediates: {s}", @@ -664,11 +671,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid scroll up command: {}", .{input}); + log.warn("invalid scroll up command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI S with intermediates: {s}", @@ -683,11 +690,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid scroll down command: {}", .{input}); + log.warn("invalid scroll down command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI T with intermediates: {s}", @@ -704,7 +711,7 @@ pub fn Stream(comptime Handler: type) type { if (@hasDecl(T, "tabSet")) try self.handler.tabSet() else - log.warn("unimplemented tab set callback: {}", .{input}); + log.warn("unimplemented tab set callback: {f}", .{input}); return; } @@ -718,12 +725,12 @@ pub fn Stream(comptime Handler: type) type { 2 => if (@hasDecl(T, "tabClear")) try self.handler.tabClear(.current) else - log.warn("unimplemented tab clear callback: {}", .{input}), + log.warn("unimplemented tab clear callback: {f}", .{input}), 5 => if (@hasDecl(T, "tabClear")) try self.handler.tabClear(.all) else - log.warn("unimplemented tab clear callback: {}", .{input}), + log.warn("unimplemented tab clear callback: {f}", .{input}), else => {}, }, @@ -731,7 +738,7 @@ pub fn Stream(comptime Handler: type) type { else => {}, } - log.warn("invalid cursor tabulation control: {}", .{input}); + log.warn("invalid cursor tabulation control: {f}", .{input}); return; }, @@ -739,8 +746,8 @@ pub fn Stream(comptime Handler: type) type { if (@hasDecl(T, "tabReset")) try self.handler.tabReset() else - log.warn("unimplemented tab reset callback: {}", .{input}); - } else log.warn("invalid cursor tabulation control: {}", .{input}), + log.warn("unimplemented tab reset callback: {f}", .{input}); + } else log.warn("invalid cursor tabulation control: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI W with intermediates: {s}", @@ -755,11 +762,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid erase characters command: {}", .{input}); + log.warn("invalid erase characters command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI X with intermediates: {s}", @@ -774,11 +781,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid horizontal tab back command: {}", .{input}); + log.warn("invalid horizontal tab back command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI Z with intermediates: {s}", @@ -793,11 +800,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid HPR command: {}", .{input}); + log.warn("invalid HPR command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI a with intermediates: {s}", @@ -812,11 +819,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid print repeat command: {}", .{input}); + log.warn("invalid print repeat command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI b with intermediates: {s}", @@ -835,12 +842,12 @@ pub fn Stream(comptime Handler: type) type { }, else => @as(?ansi.DeviceAttributeReq, null), } orelse { - log.warn("invalid device attributes command: {}", .{input}); + log.warn("invalid device attributes command: {f}", .{input}); return; }; try self.handler.deviceAttributes(req, input.params); - } else log.warn("unimplemented CSI callback: {}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), // VPA - Cursor Vertical Position Absolute 'd' => switch (input.intermediates.len) { @@ -849,11 +856,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid VPA command: {}", .{input}); + log.warn("invalid VPA command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI d with intermediates: {s}", @@ -868,11 +875,11 @@ pub fn Stream(comptime Handler: type) type { 0 => 1, 1 => input.params[0], else => { - log.warn("invalid VPR command: {}", .{input}); + log.warn("invalid VPR command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI e with intermediates: {s}", @@ -887,11 +894,11 @@ pub fn Stream(comptime Handler: type) type { switch (input.params.len) { 1 => @enumFromInt(input.params[0]), else => { - log.warn("invalid tab clear command: {}", .{input}); + log.warn("invalid tab clear command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}), + ) else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( "ignoring unimplemented CSI g with intermediates: {s}", @@ -906,7 +913,7 @@ pub fn Stream(comptime Handler: type) type { if (input.intermediates.len == 1 and input.intermediates[0] == '?') break :ansi false; - log.warn("invalid set mode command: {}", .{input}); + log.warn("invalid set mode command: {f}", .{input}); break :mode; }; @@ -917,7 +924,7 @@ pub fn Stream(comptime Handler: type) type { log.warn("unimplemented mode: {}", .{mode_int}); } } - } else log.warn("unimplemented CSI callback: {}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), // RM - Reset Mode 'l' => if (@hasDecl(T, "setMode")) mode: { @@ -926,7 +933,7 @@ pub fn Stream(comptime Handler: type) type { if (input.intermediates.len == 1 and input.intermediates[0] == '?') break :ansi false; - log.warn("invalid set mode command: {}", .{input}); + log.warn("invalid set mode command: {f}", .{input}); break :mode; }; @@ -937,7 +944,7 @@ pub fn Stream(comptime Handler: type) type { log.warn("unimplemented mode: {}", .{mode_int}); } } - } else log.warn("unimplemented CSI callback: {}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), // SGR - Select Graphic Rendition 'm' => switch (input.intermediates.len) { @@ -951,7 +958,7 @@ pub fn Stream(comptime Handler: type) type { // log.info("SGR attribute: {}", .{attr}); try self.handler.setAttribute(attr); } - } else log.warn("unimplemented CSI callback: {}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), 1 => switch (input.intermediates[0]) { '>' => if (@hasDecl(T, "setModifyKeyFormat")) blk: { @@ -967,13 +974,13 @@ pub fn Stream(comptime Handler: type) type { 2 => .{ .function_keys = {} }, 4 => .{ .other_keys = .none }, else => { - log.warn("invalid setModifyKeyFormat: {}", .{input}); + log.warn("invalid setModifyKeyFormat: {f}", .{input}); break :blk; }, }; if (input.params.len > 2) { - log.warn("invalid setModifyKeyFormat: {}", .{input}); + log.warn("invalid setModifyKeyFormat: {f}", .{input}); break :blk; } @@ -993,7 +1000,7 @@ pub fn Stream(comptime Handler: type) type { } try self.handler.setModifyKeyFormat(format); - } else log.warn("unimplemented setModifyKeyFormat: {}", .{input}), + } else log.warn("unimplemented setModifyKeyFormat: {f}", .{input}), else => log.warn( "unknown CSI m with intermediate: {}", @@ -1022,12 +1029,12 @@ pub fn Stream(comptime Handler: type) type { input.intermediates[0] == '?') { if (!@hasDecl(T, "deviceStatusReport")) { - log.warn("unimplemented CSI callback: {}", .{input}); + log.warn("unimplemented CSI callback: {f}", .{input}); return; } if (input.params.len != 1) { - log.warn("invalid device status report command: {}", .{input}); + log.warn("invalid device status report command: {f}", .{input}); return; } @@ -1036,12 +1043,12 @@ pub fn Stream(comptime Handler: type) type { if (input.intermediates.len == 1 and input.intermediates[0] == '?') break :question true; - log.warn("invalid set mode command: {}", .{input}); + log.warn("invalid set mode command: {f}", .{input}); return; }; const req = device_status.reqFromInt(input.params[0], question) orelse { - log.warn("invalid device status report command: {}", .{input}); + log.warn("invalid device status report command: {f}", .{input}); return; }; @@ -1060,7 +1067,7 @@ pub fn Stream(comptime Handler: type) type { // only support reverting back to modify other keys in // numeric except format. try self.handler.setModifyKeyFormat(.{ .other_keys = .numeric_except }); - } else log.warn("unimplemented setModifyKeyFormat: {}", .{input}), + } else log.warn("unimplemented setModifyKeyFormat: {f}", .{input}), else => log.warn( "unknown CSI n with intermediate: {}", @@ -1094,13 +1101,13 @@ pub fn Stream(comptime Handler: type) type { }; if (input.params.len != 1) { - log.warn("invalid DECRQM command: {}", .{input}); + log.warn("invalid DECRQM command: {f}", .{input}); break :decrqm; } if (@hasDecl(T, "requestMode")) { try self.handler.requestMode(input.params[0], ansi_mode); - } else log.warn("unimplemented DECRQM callback: {}", .{input}); + } else log.warn("unimplemented DECRQM callback: {f}", .{input}); }, else => log.warn( @@ -1119,11 +1126,11 @@ pub fn Stream(comptime Handler: type) type { 0 => ansi.CursorStyle.default, 1 => @enumFromInt(input.params[0]), else => { - log.warn("invalid set curor style command: {}", .{input}); + log.warn("invalid set curor style command: {f}", .{input}); return; }, }, - ) else log.warn("unimplemented CSI callback: {}", .{input}); + ) else log.warn("unimplemented CSI callback: {f}", .{input}); }, // DECSCA @@ -1140,12 +1147,12 @@ pub fn Stream(comptime Handler: type) type { }; const mode = mode_ orelse { - log.warn("invalid set protected mode command: {}", .{input}); + log.warn("invalid set protected mode command: {f}", .{input}); return; }; try self.handler.setProtectedMode(mode); - } else log.warn("unimplemented CSI callback: {}", .{input}); + } else log.warn("unimplemented CSI callback: {f}", .{input}); }, // XTVERSION @@ -1173,10 +1180,10 @@ pub fn Stream(comptime Handler: type) type { 0 => try self.handler.setTopAndBottomMargin(0, 0), 1 => try self.handler.setTopAndBottomMargin(input.params[0], 0), 2 => try self.handler.setTopAndBottomMargin(input.params[0], input.params[1]), - else => log.warn("invalid DECSTBM command: {}", .{input}), + else => log.warn("invalid DECSTBM command: {f}", .{input}), } } else log.warn( - "unimplemented CSI callback: {}", + "unimplemented CSI callback: {f}", .{input}, ), @@ -1196,13 +1203,13 @@ pub fn Stream(comptime Handler: type) type { }, else => log.warn( - "unknown CSI s with intermediate: {}", + "unknown CSI s with intermediate: {f}", .{input}, ), }, else => log.warn( - "ignoring unimplemented CSI s with intermediates: {s}", + "ignoring unimplemented CSI s with intermediates: {f}", .{input}, ), }, @@ -1218,10 +1225,10 @@ pub fn Stream(comptime Handler: type) type { 0 => try self.handler.setLeftAndRightMarginAmbiguous(), 1 => try self.handler.setLeftAndRightMargin(input.params[0], 0), 2 => try self.handler.setLeftAndRightMargin(input.params[0], input.params[1]), - else => log.warn("invalid DECSLRM command: {}", .{input}), + else => log.warn("invalid DECSLRM command: {f}", .{input}), } } else log.warn( - "unimplemented CSI callback: {}", + "unimplemented CSI callback: {f}", .{input}, ), @@ -1247,30 +1254,30 @@ pub fn Stream(comptime Handler: type) type { 0 => false, 1 => true, else => { - log.warn("invalid XTSHIFTESCAPE command: {}", .{input}); + log.warn("invalid XTSHIFTESCAPE command: {f}", .{input}); break :capture; }, }, else => { - log.warn("invalid XTSHIFTESCAPE command: {}", .{input}); + log.warn("invalid XTSHIFTESCAPE command: {f}", .{input}); break :capture; }, }; try self.handler.setMouseShiftCapture(capture); } else log.warn( - "unimplemented CSI callback: {}", + "unimplemented CSI callback: {f}", .{input}, ), else => log.warn( - "unknown CSI s with intermediate: {}", + "unknown CSI s with intermediate: {f}", .{input}, ), }, else => log.warn( - "ignoring unimplemented CSI s with intermediates: {s}", + "ignoring unimplemented CSI s with intermediates: {f}", .{input}, ), }, @@ -1289,7 +1296,7 @@ pub fn Stream(comptime Handler: type) type { .{}, ); } else log.warn( - "ignoring CSI 14 t with extra parameters: {}", + "ignoring CSI 14 t with extra parameters: {f}", .{input}, ), 16 => if (input.params.len == 1) { @@ -1301,7 +1308,7 @@ pub fn Stream(comptime Handler: type) type { .{}, ); } else log.warn( - "ignoring CSI 16 t with extra parameters: {s}", + "ignoring CSI 16 t with extra parameters: {f}", .{input}, ), 18 => if (input.params.len == 1) { @@ -1313,7 +1320,7 @@ pub fn Stream(comptime Handler: type) type { .{}, ); } else log.warn( - "ignoring CSI 18 t with extra parameters: {s}", + "ignoring CSI 18 t with extra parameters: {f}", .{input}, ), 21 => if (input.params.len == 1) { @@ -1325,7 +1332,7 @@ pub fn Stream(comptime Handler: type) type { .{}, ); } else log.warn( - "ignoring CSI 21 t with extra parameters: {s}", + "ignoring CSI 21 t with extra parameters: {f}", .{input}, ), inline 22, 23 => |number| if ((input.params.len == 2 or @@ -1352,21 +1359,21 @@ pub fn Stream(comptime Handler: type) type { .{}, ); } else log.warn( - "ignoring CSI 22/23 t with extra parameters: {s}", + "ignoring CSI 22/23 t with extra parameters: {f}", .{input}, ), else => log.warn( - "ignoring CSI t with unimplemented parameter: {s}", + "ignoring CSI t with unimplemented parameter: {f}", .{input}, ), } } else log.err( - "ignoring CSI t with no parameters: {s}", + "ignoring CSI t with no parameters: {f}", .{input}, ); }, else => log.warn( - "ignoring unimplemented CSI t with intermediates: {s}", + "ignoring unimplemented CSI t with intermediates: {f}", .{input}, ), }, @@ -1375,7 +1382,7 @@ pub fn Stream(comptime Handler: type) type { 0 => if (@hasDecl(T, "restoreCursor")) try self.handler.restoreCursor() else - log.warn("unimplemented CSI callback: {}", .{input}), + log.warn("unimplemented CSI callback: {f}", .{input}), // Kitty keyboard protocol 1 => switch (input.intermediates[0]) { @@ -1386,7 +1393,7 @@ pub fn Stream(comptime Handler: type) type { '>' => if (@hasDecl(T, "pushKittyKeyboard")) push: { const flags: u5 = if (input.params.len == 1) std.math.cast(u5, input.params[0]) orelse { - log.warn("invalid pushKittyKeyboard command: {}", .{input}); + log.warn("invalid pushKittyKeyboard command: {f}", .{input}); break :push; } else @@ -1407,7 +1414,7 @@ pub fn Stream(comptime Handler: type) type { '=' => if (@hasDecl(T, "setKittyKeyboard")) set: { const flags: u5 = if (input.params.len >= 1) std.math.cast(u5, input.params[0]) orelse { - log.warn("invalid setKittyKeyboard command: {}", .{input}); + log.warn("invalid setKittyKeyboard command: {f}", .{input}); break :set; } else @@ -1423,7 +1430,7 @@ pub fn Stream(comptime Handler: type) type { 2 => .@"or", 3 => .not, else => { - log.warn("invalid setKittyKeyboard command: {}", .{input}); + log.warn("invalid setKittyKeyboard command: {f}", .{input}); break :set; }, }; @@ -1435,13 +1442,13 @@ pub fn Stream(comptime Handler: type) type { }, else => log.warn( - "unknown CSI s with intermediate: {}", + "unknown CSI s with intermediate: {f}", .{input}, ), }, else => log.warn( - "ignoring unimplemented CSI u: {}", + "ignoring unimplemented CSI u: {f}", .{input}, ), }, @@ -1451,11 +1458,11 @@ pub fn Stream(comptime Handler: type) type { 0 => if (@hasDecl(T, "insertBlanks")) switch (input.params.len) { 0 => try self.handler.insertBlanks(1), 1 => try self.handler.insertBlanks(input.params[0]), - else => log.warn("invalid ICH command: {}", .{input}), - } else log.warn("unimplemented CSI callback: {}", .{input}), + else => log.warn("invalid ICH command: {f}", .{input}), + } else log.warn("unimplemented CSI callback: {f}", .{input}), else => log.warn( - "ignoring unimplemented CSI @: {}", + "ignoring unimplemented CSI @: {f}", .{input}, ), }, @@ -1480,17 +1487,17 @@ pub fn Stream(comptime Handler: type) type { break :decsasd true; }; - if (!success) log.warn("unimplemented CSI callback: {}", .{input}); + if (!success) log.warn("unimplemented CSI callback: {f}", .{input}); }, else => if (@hasDecl(T, "csiUnimplemented")) try self.handler.csiUnimplemented(input) else - log.warn("unimplemented CSI action: {}", .{input}), + log.warn("unimplemented CSI action: {f}", .{input}), } } - fn oscDispatch(self: *Self, cmd: osc.Command) !void { + inline fn oscDispatch(self: *Self, cmd: osc.Command) !void { switch (cmd) { .change_window_title => |title| { if (@hasDecl(T, "changeWindowTitle")) { @@ -1635,7 +1642,7 @@ pub fn Stream(comptime Handler: type) type { } } - fn configureCharset( + inline fn configureCharset( self: *Self, intermediates: []const u8, set: charsets.Charset, @@ -1669,7 +1676,7 @@ pub fn Stream(comptime Handler: type) type { }); } - fn escDispatch( + inline fn escDispatch( self: *Self, action: Parser.Action.ESC, ) !void { @@ -1683,10 +1690,10 @@ pub fn Stream(comptime Handler: type) type { '7' => if (@hasDecl(T, "saveCursor")) switch (action.intermediates.len) { 0 => try self.handler.saveCursor(), else => { - log.warn("invalid command: {}", .{action}); + log.warn("invalid command: {f}", .{action}); return; }, - } else log.warn("unimplemented ESC callback: {}", .{action}), + } else log.warn("unimplemented ESC callback: {f}", .{action}), '8' => blk: { switch (action.intermediates.len) { @@ -1694,14 +1701,14 @@ pub fn Stream(comptime Handler: type) type { 0 => if (@hasDecl(T, "restoreCursor")) { try self.handler.restoreCursor(); break :blk {}; - } else log.warn("unimplemented restore cursor callback: {}", .{action}), + } else log.warn("unimplemented restore cursor callback: {f}", .{action}), 1 => switch (action.intermediates[0]) { // DECALN - Fill Screen with E '#' => if (@hasDecl(T, "decaln")) { try self.handler.decaln(); break :blk {}; - } else log.warn("unimplemented ESC callback: {}", .{action}), + } else log.warn("unimplemented ESC callback: {f}", .{action}), else => {}, }, @@ -1709,146 +1716,146 @@ pub fn Stream(comptime Handler: type) type { else => {}, // fall through } - log.warn("unimplemented ESC action: {}", .{action}); + log.warn("unimplemented ESC action: {f}", .{action}); }, // IND - Index 'D' => if (@hasDecl(T, "index")) switch (action.intermediates.len) { 0 => try self.handler.index(), else => { - log.warn("invalid index command: {}", .{action}); + log.warn("invalid index command: {f}", .{action}); return; }, - } else log.warn("unimplemented ESC callback: {}", .{action}), + } else log.warn("unimplemented ESC callback: {f}", .{action}), // NEL - Next Line 'E' => if (@hasDecl(T, "nextLine")) switch (action.intermediates.len) { 0 => try self.handler.nextLine(), else => { - log.warn("invalid next line command: {}", .{action}); + log.warn("invalid next line command: {f}", .{action}); return; }, - } else log.warn("unimplemented ESC callback: {}", .{action}), + } else log.warn("unimplemented ESC callback: {f}", .{action}), // HTS - Horizontal Tab Set 'H' => if (@hasDecl(T, "tabSet")) switch (action.intermediates.len) { 0 => try self.handler.tabSet(), else => { - log.warn("invalid tab set command: {}", .{action}); + log.warn("invalid tab set command: {f}", .{action}); return; }, - } else log.warn("unimplemented tab set callback: {}", .{action}), + } else log.warn("unimplemented tab set callback: {f}", .{action}), // RI - Reverse Index 'M' => if (@hasDecl(T, "reverseIndex")) switch (action.intermediates.len) { 0 => try self.handler.reverseIndex(), else => { - log.warn("invalid reverse index command: {}", .{action}); + log.warn("invalid reverse index command: {f}", .{action}); return; }, - } else log.warn("unimplemented ESC callback: {}", .{action}), + } else log.warn("unimplemented ESC callback: {f}", .{action}), // SS2 - Single Shift 2 'N' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { 0 => try self.handler.invokeCharset(.GL, .G2, true), else => { - log.warn("invalid single shift 2 command: {}", .{action}); + log.warn("invalid single shift 2 command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), + } else log.warn("unimplemented invokeCharset: {f}", .{action}), // SS3 - Single Shift 3 'O' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { 0 => try self.handler.invokeCharset(.GL, .G3, true), else => { - log.warn("invalid single shift 3 command: {}", .{action}); + log.warn("invalid single shift 3 command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), + } else log.warn("unimplemented invokeCharset: {f}", .{action}), // SPA - Start of Guarded Area 'V' => if (@hasDecl(T, "setProtectedMode") and action.intermediates.len == 0) { try self.handler.setProtectedMode(ansi.ProtectedMode.iso); - } else log.warn("unimplemented ESC callback: {}", .{action}), + } else log.warn("unimplemented ESC callback: {f}", .{action}), // EPA - End of Guarded Area 'W' => if (@hasDecl(T, "setProtectedMode") and action.intermediates.len == 0) { try self.handler.setProtectedMode(ansi.ProtectedMode.off); - } else log.warn("unimplemented ESC callback: {}", .{action}), + } else log.warn("unimplemented ESC callback: {f}", .{action}), // DECID 'Z' => if (@hasDecl(T, "deviceAttributes") and action.intermediates.len == 0) { try self.handler.deviceAttributes(.primary, &.{}); - } else log.warn("unimplemented ESC callback: {}", .{action}), + } else log.warn("unimplemented ESC callback: {f}", .{action}), // RIS - Full Reset 'c' => if (@hasDecl(T, "fullReset")) switch (action.intermediates.len) { 0 => try self.handler.fullReset(), else => { - log.warn("invalid full reset command: {}", .{action}); + log.warn("invalid full reset command: {f}", .{action}); return; }, - } else log.warn("unimplemented ESC callback: {}", .{action}), + } else log.warn("unimplemented ESC callback: {f}", .{action}), // LS2 - Locking Shift 2 'n' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { 0 => try self.handler.invokeCharset(.GL, .G2, false), else => { - log.warn("invalid single shift 2 command: {}", .{action}); + log.warn("invalid single shift 2 command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), + } else log.warn("unimplemented invokeCharset: {f}", .{action}), // LS3 - Locking Shift 3 'o' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { 0 => try self.handler.invokeCharset(.GL, .G3, false), else => { - log.warn("invalid single shift 3 command: {}", .{action}); + log.warn("invalid single shift 3 command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), + } else log.warn("unimplemented invokeCharset: {f}", .{action}), // LS1R - Locking Shift 1 Right '~' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { 0 => try self.handler.invokeCharset(.GR, .G1, false), else => { - log.warn("invalid locking shift 1 right command: {}", .{action}); + log.warn("invalid locking shift 1 right command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), + } else log.warn("unimplemented invokeCharset: {f}", .{action}), // LS2R - Locking Shift 2 Right '}' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { 0 => try self.handler.invokeCharset(.GR, .G2, false), else => { - log.warn("invalid locking shift 2 right command: {}", .{action}); + log.warn("invalid locking shift 2 right command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), + } else log.warn("unimplemented invokeCharset: {f}", .{action}), // LS3R - Locking Shift 3 Right '|' => if (@hasDecl(T, "invokeCharset")) switch (action.intermediates.len) { 0 => try self.handler.invokeCharset(.GR, .G3, false), else => { - log.warn("invalid locking shift 3 right command: {}", .{action}); + log.warn("invalid locking shift 3 right command: {f}", .{action}); return; }, - } else log.warn("unimplemented invokeCharset: {}", .{action}), + } else log.warn("unimplemented invokeCharset: {f}", .{action}), // Set application keypad mode '=' => if (@hasDecl(T, "setMode") and action.intermediates.len == 0) { try self.handler.setMode(.keypad_keys, true); - } else log.warn("unimplemented setMode: {}", .{action}), + } else log.warn("unimplemented setMode: {f}", .{action}), // Reset application keypad mode '>' => if (@hasDecl(T, "setMode") and action.intermediates.len == 0) { try self.handler.setMode(.keypad_keys, false); - } else log.warn("unimplemented setMode: {}", .{action}), + } else log.warn("unimplemented setMode: {f}", .{action}), else => if (@hasDecl(T, "escUnimplemented")) try self.handler.escUnimplemented(action) else - log.warn("unimplemented ESC action: {}", .{action}), + log.warn("unimplemented ESC action: {f}", .{action}), // Sets ST (string terminator). We don't have to do anything // because our parser always accepts ST. diff --git a/src/terminal/style.zig b/src/terminal/style.zig index 4f51cbc71..eac577a53 100644 --- a/src/terminal/style.zig +++ b/src/terminal/style.zig @@ -60,7 +60,7 @@ pub const Style = struct { self: Color, comptime fmt: []const u8, options: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { _ = fmt; _ = options; @@ -228,7 +228,7 @@ pub const Style = struct { self: Style, comptime fmt: []const u8, options: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { _ = fmt; _ = options; diff --git a/src/terminal/tmux.zig b/src/terminal/tmux.zig index 1ea9f8c39..67c5a979c 100644 --- a/src/terminal/tmux.zig +++ b/src/terminal/tmux.zig @@ -17,7 +17,7 @@ pub const Client = struct { state: State = .idle, /// The buffer used to store in-progress notifications, output, etc. - buffer: std.ArrayList(u8), + buffer: std.Io.Writer.Allocating, /// The maximum size in bytes of the buffer. This is used to limit /// memory usage. If the buffer exceeds this size, the client will @@ -49,7 +49,7 @@ pub const Client = struct { // Handle a byte of input. pub fn put(self: *Client, byte: u8) !?Notification { - if (self.buffer.items.len >= self.max_bytes) { + if (self.buffer.written().len >= self.max_bytes) { self.broken(); return error.OutOfMemory; } @@ -81,18 +81,19 @@ pub const Client = struct { // If we're in a block then we accumulate until we see a newline // and then we check to see if that line ended the block. .block => if (byte == '\n') { + const written = self.buffer.written(); const idx = if (std.mem.lastIndexOfScalar( u8, - self.buffer.items, + written, '\n', )) |v| v + 1 else 0; - const line = self.buffer.items[idx..]; + const line = written[idx..]; if (std.mem.startsWith(u8, line, "%end") or std.mem.startsWith(u8, line, "%error")) { const err = std.mem.startsWith(u8, line, "%error"); - const output = std.mem.trimRight(u8, self.buffer.items[0..idx], "\r\n"); + const output = std.mem.trimRight(u8, written[0..idx], "\r\n"); // If it is an error then log it. if (err) log.warn("tmux control mode error={s}", .{output}); @@ -107,7 +108,7 @@ pub const Client = struct { }, } - try self.buffer.append(byte); + try self.buffer.writer.writeByte(byte); return null; } @@ -116,7 +117,7 @@ pub const Client = struct { assert(self.state == .notification); const line = line: { - var line = self.buffer.items; + var line = self.buffer.written(); if (line[line.len - 1] == '\r') line = line[0 .. line.len - 1]; break :line line; }; @@ -274,7 +275,7 @@ pub const Client = struct { // Mark the tmux state as broken. fn broken(self: *Client) void { self.state = .broken; - self.buffer.clearAndFree(); + self.buffer.deinit(); } }; @@ -313,7 +314,7 @@ test "tmux begin/end empty" { const testing = std.testing; const alloc = testing.allocator; - var c: Client = .{ .buffer = std.ArrayList(u8).init(alloc) }; + var c: Client = .{ .buffer = .init(alloc) }; defer c.deinit(); for ("%begin 1578922740 269 1\n") |byte| try testing.expect(try c.put(byte) == null); for ("%end 1578922740 269 1") |byte| try testing.expect(try c.put(byte) == null); @@ -326,7 +327,7 @@ test "tmux begin/error empty" { const testing = std.testing; const alloc = testing.allocator; - var c: Client = .{ .buffer = std.ArrayList(u8).init(alloc) }; + var c: Client = .{ .buffer = .init(alloc) }; defer c.deinit(); for ("%begin 1578922740 269 1\n") |byte| try testing.expect(try c.put(byte) == null); for ("%error 1578922740 269 1") |byte| try testing.expect(try c.put(byte) == null); @@ -339,7 +340,7 @@ test "tmux begin/end data" { const testing = std.testing; const alloc = testing.allocator; - var c: Client = .{ .buffer = std.ArrayList(u8).init(alloc) }; + var c: Client = .{ .buffer = .init(alloc) }; defer c.deinit(); for ("%begin 1578922740 269 1\n") |byte| try testing.expect(try c.put(byte) == null); for ("hello\nworld\n") |byte| try testing.expect(try c.put(byte) == null); @@ -353,7 +354,7 @@ test "tmux output" { const testing = std.testing; const alloc = testing.allocator; - var c: Client = .{ .buffer = std.ArrayList(u8).init(alloc) }; + var c: Client = .{ .buffer = .init(alloc) }; defer c.deinit(); for ("%output %42 foo bar baz") |byte| try testing.expect(try c.put(byte) == null); const n = (try c.put('\n')).?; @@ -366,7 +367,7 @@ test "tmux session-changed" { const testing = std.testing; const alloc = testing.allocator; - var c: Client = .{ .buffer = std.ArrayList(u8).init(alloc) }; + var c: Client = .{ .buffer = .init(alloc) }; defer c.deinit(); for ("%session-changed $42 foo") |byte| try testing.expect(try c.put(byte) == null); const n = (try c.put('\n')).?; @@ -379,7 +380,7 @@ test "tmux sessions-changed" { const testing = std.testing; const alloc = testing.allocator; - var c: Client = .{ .buffer = std.ArrayList(u8).init(alloc) }; + var c: Client = .{ .buffer = .init(alloc) }; defer c.deinit(); for ("%sessions-changed") |byte| try testing.expect(try c.put(byte) == null); const n = (try c.put('\n')).?; @@ -390,7 +391,7 @@ test "tmux sessions-changed carriage return" { const testing = std.testing; const alloc = testing.allocator; - var c: Client = .{ .buffer = std.ArrayList(u8).init(alloc) }; + var c: Client = .{ .buffer = .init(alloc) }; defer c.deinit(); for ("%sessions-changed\r") |byte| try testing.expect(try c.put(byte) == null); const n = (try c.put('\n')).?; @@ -401,7 +402,7 @@ test "tmux window-add" { const testing = std.testing; const alloc = testing.allocator; - var c: Client = .{ .buffer = std.ArrayList(u8).init(alloc) }; + var c: Client = .{ .buffer = .init(alloc) }; defer c.deinit(); for ("%window-add @14") |byte| try testing.expect(try c.put(byte) == null); const n = (try c.put('\n')).?; @@ -413,7 +414,7 @@ test "tmux window-renamed" { const testing = std.testing; const alloc = testing.allocator; - var c: Client = .{ .buffer = std.ArrayList(u8).init(alloc) }; + var c: Client = .{ .buffer = .init(alloc) }; defer c.deinit(); for ("%window-renamed @42 bar") |byte| try testing.expect(try c.put(byte) == null); const n = (try c.put('\n')).?; diff --git a/src/terminfo/Source.zig b/src/terminfo/Source.zig index 7692e6f54..91fee1ace 100644 --- a/src/terminfo/Source.zig +++ b/src/terminfo/Source.zig @@ -43,7 +43,7 @@ pub const Capability = struct { /// Encode as a terminfo source file. The encoding is always done in a /// human-readable format with whitespace. Fields are always written in the /// order of the slices on this struct; this will not do any reordering. -pub fn encode(self: Source, writer: anytype) !void { +pub fn encode(self: Source, writer: *std.Io.Writer) !void { // Encode the names in the order specified for (self.names, 0..) |name, i| { if (i != 0) try writer.writeAll("|"); @@ -115,9 +115,10 @@ pub fn xtgettcapMap(comptime self: Source) std.StaticStringMap([]const u8) { }, .numeric => |v| numeric: { var buf: [10]u8 = undefined; - const num_len = std.fmt.formatIntBuf(&buf, v, 10, .upper, .{}); + var writer: std.Io.Writer = .fixed(&buf); + writer.printInt(v, 10, .upper, .{}) catch unreachable; const final = buf; - break :numeric final[0..num_len]; + break :numeric final[0..writer.end]; }, }, }; @@ -229,8 +230,8 @@ test "encode" { // Encode var buf: [1024]u8 = undefined; - var buf_stream = std.io.fixedBufferStream(&buf); - try src.encode(buf_stream.writer()); + var writer: std.Io.Writer = .fixed(&buf); + try src.encode(&writer); const expected = "ghostty|xterm-ghostty|Ghostty,\n" ++ @@ -238,5 +239,5 @@ test "encode" { "\tccc@,\n" ++ "\tcolors#256,\n" ++ "\tbel=^G,\n"; - try std.testing.expectEqualStrings(@as([]const u8, expected), buf_stream.getWritten()); + try std.testing.expectEqualStrings(@as([]const u8, expected), writer.buffered()); } diff --git a/src/terminfo/ghostty.zig b/src/terminfo/ghostty.zig index f96154c9b..6451836e7 100644 --- a/src/terminfo/ghostty.zig +++ b/src/terminfo/ghostty.zig @@ -391,7 +391,7 @@ pub const ghostty: Source = .{ test "encode" { // Encode var buf: [1024 * 16]u8 = undefined; - var buf_stream = std.io.fixedBufferStream(&buf); - try ghostty.encode(buf_stream.writer()); - try std.testing.expect(buf_stream.getWritten().len > 0); + var writer: std.Io.Writer = .fixed(&buf); + try ghostty.encode(&writer); + try std.testing.expect(writer.buffered().len > 0); } diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 77fd2cc68..5dfda9a14 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -102,24 +102,17 @@ pub fn threadEnter( errdefer self.subprocess.stop(); // Watcher to detect subprocess exit - var process: ?xev.Process = process: { + var process: ?xev.Process = if (self.subprocess.process) |v| switch (v) { + .fork_exec => |cmd| try xev.Process.init( + cmd.pid orelse return error.ProcessNoPid, + ), + // If we're executing via Flatpak then we can't do // traditional process watching (its implemented // as a special case in os/flatpak.zig) since the // command is on the host. - if (comptime build_config.flatpak) { - if (self.subprocess.flatpak_command != null) { - break :process null; - } - } - - // Get the pid from the subprocess - const command = self.subprocess.command orelse - return error.ProcessNotStarted; - const pid = command.pid orelse - return error.ProcessNoPid; - break :process try xev.Process.init(pid); - }; + .flatpak => null, + } else return error.ProcessNotStarted; errdefer if (process) |*p| p.deinit(); // Track our process start time for abnormal exits @@ -167,17 +160,19 @@ pub fn threadEnter( termio.Termio.ThreadData, td, processExit, - ) else if (comptime build_config.flatpak) { - // If we're in flatpak and we have a flatpak command - // then we can run the special flatpak logic for watching. - if (self.subprocess.flatpak_command) |*c| { - c.waitXev( + ) else if (comptime build_config.flatpak) flatpak: { + switch (self.subprocess.process orelse break :flatpak) { + // If we're in flatpak and we have a flatpak command + // then we can run the special flatpak logic for watching. + .flatpak => |*c| c.waitXev( td.loop, &td.backend.exec.flatpak_wait_c, termio.Termio.ThreadData, td, flatpakExit, - ); + ), + + .fork_exec => {}, } } @@ -587,10 +582,29 @@ const Subprocess = struct { grid_size: renderer.GridSize, screen_size: renderer.ScreenSize, pty: ?Pty = null, - command: ?Command = null, - flatpak_command: ?FlatpakHostCommand = null, + process: ?Process = null, linux_cgroup: Command.LinuxCgroup = Command.linux_cgroup_default, + /// Union that represents the running process type. + const Process = union(enum) { + /// Standard POSIX fork/exec + fork_exec: Command, + + /// Flatpak DBus command + flatpak: FlatpakHostCommand, + }; + + const ArgsFormatter = struct { + args: []const [:0]const u8, + + pub fn format(this: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void { + for (this.args, 0..) |a, i| { + if (i > 0) try writer.writeAll(", "); + try writer.print("`{s}`", .{a}); + } + } + }; + /// Initialize the subprocess. This will NOT start it, this only sets /// up the internal state necessary to start it later. pub fn init(gpa: Allocator, cfg: Config) !Subprocess { @@ -872,7 +886,7 @@ const Subprocess = struct { read: Pty.Fd, write: Pty.Fd, } { - assert(self.pty == null and self.command == null); + assert(self.pty == null and self.process == null); // This function is funny because on POSIX systems it can // fail in the forked process. This is flipped to true if @@ -897,7 +911,24 @@ const Subprocess = struct { self.pty = null; }; - log.debug("starting command command={s}", .{self.args}); + // Cleanup we only run in our parent when we successfully start + // the process. + defer if (!in_child and self.process != null) { + if (comptime builtin.os.tag != .windows) { + // Once our subcommand is started we can close the slave + // side. This prevents the slave fd from being leaked to + // future children. + _ = posix.close(pty.slave); + } + + // Successful start we can clear out some memory. + if (self.env) |*env| { + env.deinit(); + self.env = null; + } + }; + + log.debug("starting command command={f}", .{ArgsFormatter{ .args = self.args }}); // If we can't access the cwd, then don't set any cwd and inherit. // This is important because our cwd can be set by the shell (OSC 7) @@ -948,28 +979,23 @@ const Subprocess = struct { } // Flatpak command must have a stable pointer. - self.flatpak_command = .{ + self.process = .{ .flatpak = .{ .argv = self.args, .cwd = cwd, .env = if (self.env) |*env| env else null, .stdin = pty.slave, .stdout = pty.slave, .stderr = pty.slave, - }; - var cmd = &self.flatpak_command.?; + } }; + var cmd = &self.process.?.flatpak; const pid = try cmd.spawn(alloc); errdefer killCommandFlatpak(cmd); - log.info("started subcommand on host via flatpak API path={s} pid={?}", .{ + log.info("started subcommand on host via flatpak API path={s} pid={}", .{ self.args[0], pid, }); - // Once started, we can close the pty child side. We do this after - // wait right now but that is fine too. This lets us read the - // parent and detect EOF. - _ = posix.close(pty.slave); - return .{ .read = pty.master, .write = pty.master, @@ -1022,20 +1048,7 @@ const Subprocess = struct { log.info("subcommand cgroup={s}", .{self.linux_cgroup orelse "-"}); } - if (comptime builtin.os.tag != .windows) { - // Once our subcommand is started we can close the slave - // side. This prevents the slave fd from being leaked to - // future children. - _ = posix.close(pty.slave); - } - - // Successful start we can clear out some memory. - if (self.env) |*env| { - env.deinit(); - self.env = null; - } - - self.command = cmd; + self.process = .{ .fork_exec = cmd }; return switch (builtin.os.tag) { .windows => .{ .read = pty.out_pipe, @@ -1060,7 +1073,7 @@ const Subprocess = struct { /// Called to notify that we exited externally so we can unset our /// running state. pub fn externalExit(self: *Subprocess) void { - self.command = null; + self.process = null; } /// Stop the subprocess. This is safe to call anytime. This will wait @@ -1068,25 +1081,23 @@ const Subprocess = struct { /// for it to terminate, so it will not block. /// This does not close the pty. pub fn stop(self: *Subprocess) void { - // Kill our command - if (self.command) |*cmd| { - // Note: this will also wait for the command to exit, so - // DO NOT call cmd.wait - killCommand(cmd) catch |err| - log.err("error sending SIGHUP to command, may hang: {}", .{err}); - self.command = null; - } + switch (self.process orelse return) { + .fork_exec => |*cmd| { + // Note: this will also wait for the command to exit, so + // DO NOT call cmd.wait + killCommand(cmd) catch |err| + log.err("error sending SIGHUP to command, may hang: {}", .{err}); + }, - // Kill our Flatpak command - if (comptime build_config.flatpak) { - if (self.flatpak_command) |*cmd| { + .flatpak => |*cmd| if (comptime build_config.flatpak) { killCommandFlatpak(cmd) catch |err| log.err("error sending SIGHUP to command, may hang: {}", .{err}); _ = cmd.wait() catch |err| log.err("error waiting for command to exit: {}", .{err}); - self.flatpak_command = null; - } + }, } + + self.process = null; } /// Resize the pty subprocess. This is safe to call anytime. @@ -1126,41 +1137,45 @@ const Subprocess = struct { _ = try command.wait(false); }, - else => if (getpgid(pid)) |pgid| { - // It is possible to send a killpg between the time that - // our child process calls setsid but before or simultaneous - // to calling execve. In this case, the direct child dies - // but grandchildren survive. To work around this, we loop - // and repeatedly kill the process group until all - // descendents are well and truly dead. We will not rest - // until the entire family tree is obliterated. - while (true) { - switch (posix.errno(c.killpg(pgid, c.SIGHUP))) { - .SUCCESS => log.debug("process group killed pgid={}", .{pgid}), - else => |err| killpg: { - if ((comptime builtin.target.os.tag.isDarwin()) and - err == .PERM) - { - log.debug("killpg failed with EPERM, expected on Darwin and ignoring", .{}); - break :killpg; - } + else => try killPid(pid), + } + } + } - log.warn("error killing process group pgid={} err={}", .{ pgid, err }); - return error.KillFailed; - }, - } + fn killPid(pid: c.pid_t) !void { + const pgid = getpgid(pid) orelse return; - // See Command.zig wait for why we specify WNOHANG. - // The gist is that it lets us detect when children - // are still alive without blocking so that we can - // kill them again. - const res = posix.waitpid(pid, std.c.W.NOHANG); - log.debug("waitpid result={}", .{res.pid}); - if (res.pid != 0) break; - std.time.sleep(10 * std.time.ns_per_ms); + // It is possible to send a killpg between the time that + // our child process calls setsid but before or simultaneous + // to calling execve. In this case, the direct child dies + // but grandchildren survive. To work around this, we loop + // and repeatedly kill the process group until all + // descendents are well and truly dead. We will not rest + // until the entire family tree is obliterated. + while (true) { + switch (posix.errno(c.killpg(pgid, c.SIGHUP))) { + .SUCCESS => log.debug("process group killed pgid={}", .{pgid}), + else => |err| killpg: { + if ((comptime builtin.target.os.tag.isDarwin()) and + err == .PERM) + { + log.debug("killpg failed with EPERM, expected on Darwin and ignoring", .{}); + break :killpg; } + + log.warn("error killing process group pgid={} err={}", .{ pgid, err }); + return error.KillFailed; }, } + + // See Command.zig wait for why we specify WNOHANG. + // The gist is that it lets us detect when children + // are still alive without blocking so that we can + // kill them again. + const res = posix.waitpid(pid, std.c.W.NOHANG); + log.debug("waitpid result={}", .{res.pid}); + if (res.pid != 0) break; + std.Thread.sleep(10 * std.time.ns_per_ms); } } @@ -1180,7 +1195,7 @@ const Subprocess = struct { const pgid = c.getpgid(pid); if (pgid == my_pgid) { log.warn("pgid is our own, retrying", .{}); - std.time.sleep(10 * std.time.ns_per_ms); + std.Thread.sleep(10 * std.time.ns_per_ms); continue; } @@ -1429,7 +1444,7 @@ fn execCommand( // grow if necessary for a longer command (uncommon). 9, ); - defer args.deinit(); + defer args.deinit(alloc); // The reason for executing login this way is unclear. This // comment will attempt to explain but prepare for a truly @@ -1476,40 +1491,41 @@ fn execCommand( // macOS. // // Awesome. - try args.append("/usr/bin/login"); - if (hush) try args.append("-q"); - try args.append("-flp"); - try args.append(username); + try args.append(alloc, "/usr/bin/login"); + if (hush) try args.append(alloc, "-q"); + try args.append(alloc, "-flp"); + try args.append(alloc, username); switch (command) { // Direct args can be passed directly to login, since // login uses execvp we don't need to worry about PATH // searching. - .direct => |v| try args.appendSlice(v), + .direct => |v| try args.appendSlice(alloc, v), .shell => |v| { // Use "exec" to replace the bash process with // our intended command so we don't have a parent // process hanging around. - const cmd = try std.fmt.allocPrintZ( + const cmd = try std.fmt.allocPrintSentinel( alloc, "exec -l {s}", .{v}, + 0, ); // We execute bash with "--noprofile --norc" so that it doesn't // load startup files so that (1) our shell integration doesn't // break and (2) user configuration doesn't mess this process // up. - try args.append("/bin/bash"); - try args.append("--noprofile"); - try args.append("--norc"); - try args.append("-c"); - try args.append(cmd); + try args.append(alloc, "/bin/bash"); + try args.append(alloc, "--noprofile"); + try args.append(alloc, "--norc"); + try args.append(alloc, "-c"); + try args.append(alloc, cmd); }, } - return try args.toOwnedSlice(); + return try args.toOwnedSlice(alloc); } return switch (command) { @@ -1518,7 +1534,7 @@ fn execCommand( .shell => |v| shell: { var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 4); - defer args.deinit(); + defer args.deinit(alloc); if (comptime builtin.os.tag == .windows) { // We run our shell wrapped in `cmd.exe` so that we don't have @@ -1539,21 +1555,21 @@ fn execCommand( "cmd.exe", }); - try args.append(cmd); - try args.append("/C"); + try args.append(alloc, cmd); + try args.append(alloc, "/C"); } else { // We run our shell wrapped in `/bin/sh` so that we don't have // to parse the command line ourselves if it has arguments. // Additionally, some environments (NixOS, I found) use /bin/sh // to setup some environment variables that are important to // have set. - try args.append("/bin/sh"); - if (internal_os.isFlatpak()) try args.append("-l"); - try args.append("-c"); + try args.append(alloc, "/bin/sh"); + if (internal_os.isFlatpak()) try args.append(alloc, "-l"); + try args.append(alloc, "-c"); } - try args.append(v); - break :shell try args.toOwnedSlice(); + try args.append(alloc, v); + break :shell try args.toOwnedSlice(alloc); }, }; } diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index edf966df7..bb616e623 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -471,15 +471,23 @@ fn stopCallback( fn startScrollTimer(self: *Thread, cb: *CallbackData) void { self.scroll_active = true; - // Start the timer which loops - self.scroll.run( - &self.loop, - &self.scroll_c, - selection_scroll_ms, - CallbackData, - cb, - selectionScrollCallback, - ); + switch (self.scroll_c.state()) { + // If it is already active, e.g. startScrollTimer is called multiple + // times, then we just return. We can't simply check `scroll_active` + // because its possible that `stopScrollTimer` was called but there + // was no loop tick between then and now to halt out completion. + .active => return, + + // If the completion is not active then we need to start it. + .dead => self.scroll.run( + &self.loop, + &self.scroll_c, + selection_scroll_ms, + CallbackData, + cb, + selectionScrollCallback, + ), + } } fn stopScrollTimer(self: *Thread) void { diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index 30519b6e2..8b2648dbd 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -175,7 +175,9 @@ pub fn setupFeatures( inline for (fields) |field| n += field.name.len; break :capacity n; }; - var buffer = try std.BoundedArray(u8, capacity).init(0); + + var buf: [capacity]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); // Sort the fields so that the output is deterministic. This is // done at comptime so it has no runtime cost @@ -197,13 +199,13 @@ pub fn setupFeatures( inline for (fields_sorted) |name| { if (@field(features, name)) { - if (buffer.len > 0) try buffer.append(','); - try buffer.appendSlice(name); + if (writer.end > 0) try writer.writeByte(','); + try writer.writeAll(name); } } - if (buffer.len > 0) { - try env.put("GHOSTTY_SHELL_FEATURES", buffer.slice()); + if (writer.end > 0) { + try env.put("GHOSTTY_SHELL_FEATURES", buf[0..writer.end]); } } @@ -219,8 +221,8 @@ test "setup features" { var env = EnvMap.init(alloc); defer env.deinit(); - try setupFeatures(&env, .{ .cursor = true, .sudo = true, .title = true, .@"ssh-env" = true, .@"ssh-terminfo" = true }); - try testing.expectEqualStrings("cursor,ssh-env,ssh-terminfo,sudo,title", env.get("GHOSTTY_SHELL_FEATURES").?); + try setupFeatures(&env, .{ .cursor = true, .sudo = true, .title = true, .@"ssh-env" = true, .@"ssh-terminfo" = true, .path = true }); + try testing.expectEqualStrings("cursor,path,ssh-env,ssh-terminfo,sudo,title", env.get("GHOSTTY_SHELL_FEATURES").?); } // Test: all features disabled @@ -228,7 +230,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 }); + try setupFeatures(&env, .{ .cursor = false, .sudo = false, .title = false, .@"ssh-env" = false, .@"ssh-terminfo" = false, .path = false }); try testing.expect(env.get("GHOSTTY_SHELL_FEATURES") == null); } @@ -237,7 +239,7 @@ test "setup features" { var env = EnvMap.init(alloc); defer env.deinit(); - try setupFeatures(&env, .{ .cursor = false, .sudo = true, .title = false, .@"ssh-env" = true, .@"ssh-terminfo" = false }); + try setupFeatures(&env, .{ .cursor = false, .sudo = true, .title = false, .@"ssh-env" = true, .@"ssh-terminfo" = false, .path = false }); try testing.expectEqualStrings("ssh-env,sudo", env.get("GHOSTTY_SHELL_FEATURES").?); } } @@ -257,8 +259,8 @@ fn setupBash( resource_dir: []const u8, env: *EnvMap, ) !?config.Command { - var args = try std.ArrayList([:0]const u8).initCapacity(alloc, 3); - defer args.deinit(); + var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 3); + defer args.deinit(alloc); // Iterator that yields each argument in the original command line. // This will allocate once proportionate to the command line length. @@ -267,21 +269,22 @@ fn setupBash( // Start accumulating arguments with the executable and initial flags. if (iter.next()) |exe| { - try args.append(try alloc.dupeZ(u8, exe)); + try args.append(alloc, try alloc.dupeZ(u8, exe)); } else return null; - try args.append("--posix"); + 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("--login"); + try args.append(alloc, "--login"); } // Stores the list of intercepted command line flags that will be passed // to our shell integration script: --norc --noprofile // We always include at least "1" so the script can differentiate between // being manually sourced or automatically injected (from here). - var inject = try std.BoundedArray(u8, 32).init(0); - try inject.appendSlice("1"); + var buf: [32]u8 = undefined; + var inject: std.Io.Writer = .fixed(&buf); + try inject.writeAll("1"); // Walk through the rest of the given arguments. If we see an option that // would require complex or unsupported integration behavior, we bail out @@ -296,9 +299,9 @@ fn setupBash( if (std.mem.eql(u8, arg, "--posix")) { return null; } else if (std.mem.eql(u8, arg, "--norc")) { - try inject.appendSlice(" --norc"); + try inject.writeAll(" --norc"); } else if (std.mem.eql(u8, arg, "--noprofile")) { - try inject.appendSlice(" --noprofile"); + try inject.writeAll(" --noprofile"); } else if (std.mem.eql(u8, arg, "--rcfile") or std.mem.eql(u8, arg, "--init-file")) { rcfile = iter.next(); } else if (arg.len > 1 and arg[0] == '-' and arg[1] != '-') { @@ -306,20 +309,20 @@ fn setupBash( if (std.mem.indexOfScalar(u8, arg, 'c') != null) { return null; } - try args.append(try alloc.dupeZ(u8, arg)); + try args.append(alloc, try alloc.dupeZ(u8, 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(try alloc.dupeZ(u8, arg)); + try args.append(alloc, try alloc.dupeZ(u8, arg)); while (iter.next()) |remaining_arg| { - try args.append(try alloc.dupeZ(u8, remaining_arg)); + try args.append(alloc, try alloc.dupeZ(u8, remaining_arg)); } break; } else { - try args.append(try alloc.dupeZ(u8, arg)); + try args.append(alloc, try alloc.dupeZ(u8, arg)); } } - try env.put("GHOSTTY_BASH_INJECT", inject.slice()); + try env.put("GHOSTTY_BASH_INJECT", buf[0..inject.end]); if (rcfile) |v| { try env.put("GHOSTTY_BASH_RCFILE", v); } @@ -356,7 +359,7 @@ fn setupBash( // 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() }; + return .{ .direct = try args.toOwnedSlice(alloc) }; } test "bash" { diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index f9bc03500..dd8669d90 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -186,19 +186,19 @@ pub const StreamHandler = struct { _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); } - pub fn dcsHook(self: *StreamHandler, dcs: terminal.DCS) !void { + pub inline fn dcsHook(self: *StreamHandler, dcs: terminal.DCS) !void { var cmd = self.dcs.hook(self.alloc, dcs) orelse return; defer cmd.deinit(); try self.dcsCommand(&cmd); } - pub fn dcsPut(self: *StreamHandler, byte: u8) !void { + pub inline fn dcsPut(self: *StreamHandler, byte: u8) !void { var cmd = self.dcs.put(byte) orelse return; defer cmd.deinit(); try self.dcsCommand(&cmd); } - pub fn dcsUnhook(self: *StreamHandler) !void { + pub inline fn dcsUnhook(self: *StreamHandler) !void { var cmd = self.dcs.unhook() orelse return; defer cmd.deinit(); try self.dcsCommand(&cmd); @@ -293,11 +293,11 @@ pub const StreamHandler = struct { } } - pub fn apcStart(self: *StreamHandler) !void { + pub inline fn apcStart(self: *StreamHandler) !void { self.apc.start(); } - pub fn apcPut(self: *StreamHandler, byte: u8) !void { + pub inline fn apcPut(self: *StreamHandler, byte: u8) !void { self.apc.feed(self.alloc, byte); } @@ -310,11 +310,11 @@ pub const StreamHandler = struct { .kitty => |*kitty_cmd| { if (self.terminal.kittyGraphics(self.alloc, kitty_cmd)) |resp| { var buf: [1024]u8 = undefined; - var buf_stream = std.io.fixedBufferStream(&buf); - try resp.encode(buf_stream.writer()); - const final = buf_stream.getWritten(); + var writer: std.Io.Writer = .fixed(&buf); + try resp.encode(&writer); + const final = writer.buffered(); if (final.len > 2) { - log.debug("kitty graphics response: {s}", .{std.fmt.fmtSliceHexLower(final)}); + log.debug("kitty graphics response: {x}", .{final}); self.messageWriter(try termio.Message.writeReq(self.alloc, final)); } } @@ -322,23 +322,23 @@ pub const StreamHandler = struct { } } - pub fn print(self: *StreamHandler, ch: u21) !void { + pub inline fn print(self: *StreamHandler, ch: u21) !void { try self.terminal.print(ch); } - pub fn printRepeat(self: *StreamHandler, count: usize) !void { + pub inline fn printRepeat(self: *StreamHandler, count: usize) !void { try self.terminal.printRepeat(count); } - pub fn bell(self: *StreamHandler) !void { + pub inline fn bell(self: *StreamHandler) !void { self.surfaceMessageWriter(.ring_bell); } - pub fn backspace(self: *StreamHandler) !void { + pub inline fn backspace(self: *StreamHandler) !void { self.terminal.backspace(); } - pub fn horizontalTab(self: *StreamHandler, count: u16) !void { + pub inline fn horizontalTab(self: *StreamHandler, count: u16) !void { for (0..count) |_| { const x = self.terminal.screen.cursor.x; try self.terminal.horizontalTab(); @@ -346,7 +346,7 @@ pub const StreamHandler = struct { } } - pub fn horizontalTabBack(self: *StreamHandler, count: u16) !void { + pub inline fn horizontalTabBack(self: *StreamHandler, count: u16) !void { for (0..count) |_| { const x = self.terminal.screen.cursor.x; try self.terminal.horizontalTabBack(); @@ -354,61 +354,61 @@ pub const StreamHandler = struct { } } - pub fn linefeed(self: *StreamHandler) !void { + pub inline fn linefeed(self: *StreamHandler) !void { // Small optimization: call index instead of linefeed because they're // identical and this avoids one layer of function call overhead. try self.terminal.index(); } - pub fn carriageReturn(self: *StreamHandler) !void { + pub inline fn carriageReturn(self: *StreamHandler) !void { self.terminal.carriageReturn(); } - pub fn setCursorLeft(self: *StreamHandler, amount: u16) !void { + pub inline fn setCursorLeft(self: *StreamHandler, amount: u16) !void { self.terminal.cursorLeft(amount); } - pub fn setCursorRight(self: *StreamHandler, amount: u16) !void { + pub inline fn setCursorRight(self: *StreamHandler, amount: u16) !void { self.terminal.cursorRight(amount); } - pub fn setCursorDown(self: *StreamHandler, amount: u16, carriage: bool) !void { + pub inline fn setCursorDown(self: *StreamHandler, amount: u16, carriage: bool) !void { self.terminal.cursorDown(amount); if (carriage) self.terminal.carriageReturn(); } - pub fn setCursorUp(self: *StreamHandler, amount: u16, carriage: bool) !void { + pub inline fn setCursorUp(self: *StreamHandler, amount: u16, carriage: bool) !void { self.terminal.cursorUp(amount); if (carriage) self.terminal.carriageReturn(); } - pub fn setCursorCol(self: *StreamHandler, col: u16) !void { + pub inline fn setCursorCol(self: *StreamHandler, col: u16) !void { self.terminal.setCursorPos(self.terminal.screen.cursor.y + 1, col); } - pub fn setCursorColRelative(self: *StreamHandler, offset: u16) !void { + pub inline fn setCursorColRelative(self: *StreamHandler, offset: u16) !void { self.terminal.setCursorPos( self.terminal.screen.cursor.y + 1, self.terminal.screen.cursor.x + 1 +| offset, ); } - pub fn setCursorRow(self: *StreamHandler, row: u16) !void { + pub inline fn setCursorRow(self: *StreamHandler, row: u16) !void { self.terminal.setCursorPos(row, self.terminal.screen.cursor.x + 1); } - pub fn setCursorRowRelative(self: *StreamHandler, offset: u16) !void { + pub inline fn setCursorRowRelative(self: *StreamHandler, offset: u16) !void { self.terminal.setCursorPos( self.terminal.screen.cursor.y + 1 +| offset, self.terminal.screen.cursor.x + 1, ); } - pub fn setCursorPos(self: *StreamHandler, row: u16, col: u16) !void { + pub inline fn setCursorPos(self: *StreamHandler, row: u16, col: u16) !void { self.terminal.setCursorPos(row, col); } - pub fn eraseDisplay(self: *StreamHandler, mode: terminal.EraseDisplay, protected: bool) !void { + pub inline fn eraseDisplay(self: *StreamHandler, mode: terminal.EraseDisplay, protected: bool) !void { if (mode == .complete) { // Whenever we erase the full display, scroll to bottom. try self.terminal.scrollViewport(.{ .bottom = {} }); @@ -418,48 +418,48 @@ pub const StreamHandler = struct { self.terminal.eraseDisplay(mode, protected); } - pub fn eraseLine(self: *StreamHandler, mode: terminal.EraseLine, protected: bool) !void { + pub inline fn eraseLine(self: *StreamHandler, mode: terminal.EraseLine, protected: bool) !void { self.terminal.eraseLine(mode, protected); } - pub fn deleteChars(self: *StreamHandler, count: usize) !void { + pub inline fn deleteChars(self: *StreamHandler, count: usize) !void { self.terminal.deleteChars(count); } - pub fn eraseChars(self: *StreamHandler, count: usize) !void { + pub inline fn eraseChars(self: *StreamHandler, count: usize) !void { self.terminal.eraseChars(count); } - pub fn insertLines(self: *StreamHandler, count: usize) !void { + pub inline fn insertLines(self: *StreamHandler, count: usize) !void { self.terminal.insertLines(count); } - pub fn insertBlanks(self: *StreamHandler, count: usize) !void { + pub inline fn insertBlanks(self: *StreamHandler, count: usize) !void { self.terminal.insertBlanks(count); } - pub fn deleteLines(self: *StreamHandler, count: usize) !void { + pub inline fn deleteLines(self: *StreamHandler, count: usize) !void { self.terminal.deleteLines(count); } - pub fn reverseIndex(self: *StreamHandler) !void { + pub inline fn reverseIndex(self: *StreamHandler) !void { self.terminal.reverseIndex(); } - pub fn index(self: *StreamHandler) !void { + pub inline fn index(self: *StreamHandler) !void { try self.terminal.index(); } - pub fn nextLine(self: *StreamHandler) !void { + pub inline fn nextLine(self: *StreamHandler) !void { try self.terminal.index(); self.terminal.carriageReturn(); } - pub fn setTopAndBottomMargin(self: *StreamHandler, top: u16, bot: u16) !void { + pub inline fn setTopAndBottomMargin(self: *StreamHandler, top: u16, bot: u16) !void { self.terminal.setTopAndBottomMargin(top, bot); } - pub fn setLeftAndRightMarginAmbiguous(self: *StreamHandler) !void { + pub inline fn setLeftAndRightMarginAmbiguous(self: *StreamHandler) !void { if (self.terminal.modes.get(.enable_left_and_right_margin)) { try self.setLeftAndRightMargin(0, 0); } else { @@ -467,7 +467,7 @@ pub const StreamHandler = struct { } } - pub fn setLeftAndRightMargin(self: *StreamHandler, left: u16, right: u16) !void { + pub inline fn setLeftAndRightMargin(self: *StreamHandler, left: u16, right: u16) !void { self.terminal.setLeftAndRightMargin(left, right); } @@ -504,12 +504,12 @@ pub const StreamHandler = struct { self.messageWriter(msg); } - pub fn saveMode(self: *StreamHandler, mode: terminal.Mode) !void { + pub inline fn saveMode(self: *StreamHandler, mode: terminal.Mode) !void { // log.debug("save mode={}", .{mode}); self.terminal.modes.save(mode); } - pub fn restoreMode(self: *StreamHandler, mode: terminal.Mode) !void { + pub inline fn restoreMode(self: *StreamHandler, mode: terminal.Mode) !void { // For restore mode we have to restore but if we set it, we // always have to call setMode because setting some modes have // side effects and we want to make sure we process those. @@ -696,11 +696,11 @@ pub const StreamHandler = struct { } } - pub fn setMouseShiftCapture(self: *StreamHandler, v: bool) !void { + pub inline fn setMouseShiftCapture(self: *StreamHandler, v: bool) !void { self.terminal.flags.mouse_shift_capture = if (v) .true else .false; } - pub fn setAttribute(self: *StreamHandler, attr: terminal.Attribute) !void { + pub inline fn setAttribute(self: *StreamHandler, attr: terminal.Attribute) !void { switch (attr) { .unknown => |unk| log.warn("unimplemented or unknown SGR attribute: {any}", .{unk}), @@ -709,11 +709,11 @@ pub const StreamHandler = struct { } } - pub fn startHyperlink(self: *StreamHandler, uri: []const u8, id: ?[]const u8) !void { + pub inline fn startHyperlink(self: *StreamHandler, uri: []const u8, id: ?[]const u8) !void { try self.terminal.screen.startHyperlink(uri, id); } - pub fn endHyperlink(self: *StreamHandler) !void { + pub inline fn endHyperlink(self: *StreamHandler) !void { self.terminal.screen.endHyperlink(); } @@ -832,31 +832,31 @@ pub const StreamHandler = struct { } } - pub fn setProtectedMode(self: *StreamHandler, mode: terminal.ProtectedMode) !void { + pub inline fn setProtectedMode(self: *StreamHandler, mode: terminal.ProtectedMode) !void { self.terminal.setProtectedMode(mode); } - pub fn decaln(self: *StreamHandler) !void { + pub inline fn decaln(self: *StreamHandler) !void { try self.terminal.decaln(); } - pub fn tabClear(self: *StreamHandler, cmd: terminal.TabClear) !void { + pub inline fn tabClear(self: *StreamHandler, cmd: terminal.TabClear) !void { self.terminal.tabClear(cmd); } - pub fn tabSet(self: *StreamHandler) !void { + pub inline fn tabSet(self: *StreamHandler) !void { self.terminal.tabSet(); } - pub fn tabReset(self: *StreamHandler) !void { + pub inline fn tabReset(self: *StreamHandler) !void { self.terminal.tabReset(); } - pub fn saveCursor(self: *StreamHandler) !void { + pub inline fn saveCursor(self: *StreamHandler) !void { self.terminal.saveCursor(); } - pub fn restoreCursor(self: *StreamHandler) !void { + pub inline fn restoreCursor(self: *StreamHandler) !void { try self.terminal.restoreCursor(); } @@ -865,11 +865,11 @@ pub const StreamHandler = struct { self.messageWriter(try termio.Message.writeReq(self.alloc, self.enquiry_response)); } - pub fn scrollDown(self: *StreamHandler, count: usize) !void { + pub inline fn scrollDown(self: *StreamHandler, count: usize) !void { self.terminal.scrollDown(count); } - pub fn scrollUp(self: *StreamHandler, count: usize) !void { + pub inline fn scrollUp(self: *StreamHandler, count: usize) !void { self.terminal.scrollUp(count); } @@ -995,7 +995,7 @@ pub const StreamHandler = struct { self.surfaceMessageWriter(.{ .set_title = buf }); } - pub fn setMouseShape( + pub inline fn setMouseShape( self: *StreamHandler, shape: terminal.MouseShape, ) !void { @@ -1037,23 +1037,28 @@ pub const StreamHandler = struct { }); } - pub fn promptStart(self: *StreamHandler, aid: ?[]const u8, redraw: bool) !void { + pub inline fn promptStart(self: *StreamHandler, aid: ?[]const u8, redraw: bool) !void { _ = aid; self.terminal.markSemanticPrompt(.prompt); self.terminal.flags.shell_redraws_prompt = redraw; } - pub fn promptContinuation(self: *StreamHandler, aid: ?[]const u8) !void { + pub inline fn promptContinuation(self: *StreamHandler, aid: ?[]const u8) !void { _ = aid; self.terminal.markSemanticPrompt(.prompt_continuation); } - pub fn promptEnd(self: *StreamHandler) !void { + pub inline fn promptEnd(self: *StreamHandler) !void { self.terminal.markSemanticPrompt(.input); } - pub fn endOfInput(self: *StreamHandler) !void { + pub inline fn endOfInput(self: *StreamHandler) !void { self.terminal.markSemanticPrompt(.command); + self.surfaceMessageWriter(.start_command); + } + + pub inline fn endOfCommand(self: *StreamHandler, exit_code: ?u8) !void { + self.surfaceMessageWriter(.{ .stop_command = exit_code }); } pub fn reportPwd(self: *StreamHandler, url: []const u8) !void { @@ -1084,7 +1089,13 @@ pub const StreamHandler = struct { return; } - const uri: std.Uri = internal_os.hostname.parseUrl(url) catch |e| { + // Attempt to parse this file-style URI using options appropriate + // for this OSC 7 context (e.g. kitty-shell-cwd expects the full, + // unencoded path). + const uri: std.Uri = internal_os.uri.parse(url, .{ + .mac_address = comptime builtin.os.tag != .macos, + .raw_path = std.mem.startsWith(u8, url, "kitty-shell-cwd://"), + }) catch |e| { log.warn("invalid url in OSC 7: {}", .{e}); return; }; @@ -1092,26 +1103,18 @@ pub const StreamHandler = struct { if (!std.mem.eql(u8, "file", uri.scheme) and !std.mem.eql(u8, "kitty-shell-cwd", uri.scheme)) { - log.warn("OSC 7 scheme must be file, got: {s}", .{uri.scheme}); + log.warn("OSC 7 scheme must be file or kitty-shell-cwd, got: {s}", .{uri.scheme}); return; } - // RFC 793 defines port numbers as 16-bit numbers. 5 digits is sufficient to represent - // the maximum since 2^16 - 1 = 65_535. - // See https://www.rfc-editor.org/rfc/rfc793#section-3.1. - const PORT_NUMBER_MAX_DIGITS = 5; - // Make sure there is space for a max length hostname + the max number of digits. - var host_and_port_buf: [posix.HOST_NAME_MAX + PORT_NUMBER_MAX_DIGITS]u8 = undefined; - const hostname_from_uri = internal_os.hostname.bufPrintHostnameFromFileUri( - &host_and_port_buf, - uri, - ) catch |err| switch (err) { - error.NoHostnameInUri => { + var host_buffer: [std.Uri.host_name_max]u8 = undefined; + const host = uri.getHost(&host_buffer) catch |err| switch (err) { + error.UriMissingHost => { log.warn("OSC 7 uri must contain a hostname: {}", .{err}); return; }, - error.NoSpaceLeft => |e| { - log.warn("failed to get full hostname for OSC 7 validation: {}", .{e}); + error.UriHostTooLong => { + log.warn("failed to get full hostname for OSC 7 validation: {}", .{err}); return; }, }; @@ -1119,9 +1122,7 @@ pub const StreamHandler = struct { // OSC 7 is a little sketchy because anyone can send any value from // any host (such an SSH session). The best practice terminals follow // is to valid the hostname to be local. - const host_valid = internal_os.hostname.isLocalHostname( - hostname_from_uri, - ) catch |err| switch (err) { + const host_valid = internal_os.hostname.isLocal(host) catch |err| switch (err) { error.PermissionDenied, error.Unexpected, => { @@ -1130,42 +1131,16 @@ pub const StreamHandler = struct { }, }; if (!host_valid) { - log.warn("OSC 7 host must be local", .{}); + log.warn("OSC 7 host ({s}) must be local", .{host}); return; } - // We need to unescape the path. We first try to unescape onto - // the stack and fall back to heap allocation if we have to. - var pathBuf: [1024]u8 = undefined; - const path, const heap = path: { - // Get the raw string of the URI. Its unclear to me if the various - // tags of this enum guarantee no percent-encoding so we just - // check all of it. This isn't a performance critical path. - const path = switch (uri.path) { - .raw => |v| v, - .percent_encoded => |v| v, - }; - - // If the path doesn't have any escapes, we can use it directly. - if (std.mem.indexOfScalar(u8, path, '%') == null) - break :path .{ path, false }; - - // First try to stack-allocate - var fba = std.heap.FixedBufferAllocator.init(&pathBuf); - if (std.fmt.allocPrint(fba.allocator(), "{raw}", .{uri.path})) |v| - break :path .{ v, false } - else |_| {} - - // Fall back to heap - if (std.fmt.allocPrint(self.alloc, "{raw}", .{uri.path})) |v| - break :path .{ v, true } - else |_| {} - - // Fall back to using it directly... - log.warn("failed to unescape OSC 7 path, using it directly path={s}", .{path}); - break :path .{ path, false }; - }; - defer if (heap) self.alloc.free(path); + // We need the raw path, which might require unescaping. We try to + // avoid making any heap allocations by using the stack first. + var arena_alloc: std.heap.ArenaAllocator = .init(self.alloc); + var stack_alloc = std.heap.stackFallback(1024, arena_alloc.allocator()); + defer arena_alloc.deinit(); + const path = try uri.path.toRawMaybeAlloc(stack_alloc.get()); log.debug("terminal pwd: {s}", .{path}); try self.terminal.setPwd(path); @@ -1217,21 +1192,21 @@ pub const StreamHandler = struct { .dynamic => |dynamic| switch (dynamic) { .foreground => { self.foreground_color = set.color; - _ = self.renderer_mailbox.push(.{ + self.rendererMessageWriter(.{ .foreground_color = set.color, - }, .{ .forever = {} }); + }); }, .background => { self.background_color = set.color; - _ = self.renderer_mailbox.push(.{ + self.rendererMessageWriter(.{ .background_color = set.color, - }, .{ .forever = {} }); + }); }, .cursor => { self.cursor_color = set.color; - _ = self.renderer_mailbox.push(.{ + self.rendererMessageWriter(.{ .cursor_color = set.color, - }, .{ .forever = {} }); + }); }, .pointer_foreground, .pointer_background, @@ -1271,9 +1246,9 @@ pub const StreamHandler = struct { .dynamic => |dynamic| switch (dynamic) { .foreground => { self.foreground_color = null; - _ = self.renderer_mailbox.push(.{ + self.rendererMessageWriter(.{ .foreground_color = self.foreground_color, - }, .{ .forever = {} }); + }); self.surfaceMessageWriter(.{ .color_change = .{ .target = target, @@ -1282,9 +1257,9 @@ pub const StreamHandler = struct { }, .background => { self.background_color = null; - _ = self.renderer_mailbox.push(.{ + self.rendererMessageWriter(.{ .background_color = self.background_color, - }, .{ .forever = {} }); + }); self.surfaceMessageWriter(.{ .color_change = .{ .target = target, @@ -1294,9 +1269,9 @@ pub const StreamHandler = struct { .cursor => { self.cursor_color = null; - _ = self.renderer_mailbox.push(.{ + self.rendererMessageWriter(.{ .cursor_color = self.cursor_color, - }, .{ .forever = {} }); + }); if (self.default_cursor_color) |color| { self.surfaceMessageWriter(.{ .color_change = .{ @@ -1466,15 +1441,15 @@ pub const StreamHandler = struct { self: *StreamHandler, request: terminal.kitty.color.OSC, ) !void { - var buf = std.ArrayList(u8).init(self.alloc); - defer buf.deinit(); - const writer = buf.writer(); + var stream: std.Io.Writer.Allocating = .init(self.alloc); + defer stream.deinit(); + const writer = &stream.writer; for (request.list.items) |item| { switch (item) { .query => |key| { // If the writer buffer is empty, we need to write our prefix - if (buf.items.len == 0) try writer.writeAll("\x1b]21"); + if (stream.written().len == 0) try writer.writeAll("\x1b]21"); const color: terminal.color.RGB = switch (key) { .palette => |palette| self.terminal.color_palette.colors[palette], @@ -1483,17 +1458,17 @@ pub const StreamHandler = struct { .background => self.background_color orelse self.default_background_color, .cursor => self.cursor_color orelse self.default_cursor_color, else => { - log.warn("ignoring unsupported kitty color protocol key: {}", .{key}); + log.warn("ignoring unsupported kitty color protocol key: {f}", .{key}); continue; }, }, } orelse { - try writer.print(";{}=", .{key}); + try writer.print(";{f}=", .{key}); continue; }; try writer.print( - ";{}=rgb:{x:0>2}/{x:0>2}/{x:0>2}", + ";{f}=rgb:{x:0>2}/{x:0>2}/{x:0>2}", .{ key, color.r, color.g, color.b }, ); }, @@ -1520,7 +1495,7 @@ pub const StreamHandler = struct { }, else => { log.warn( - "ignoring unsupported kitty color protocol key: {}", + "ignoring unsupported kitty color protocol key: {f}", .{v.key}, ); continue; @@ -1555,7 +1530,7 @@ pub const StreamHandler = struct { }, else => { log.warn( - "ignoring unsupported kitty color protocol key: {}", + "ignoring unsupported kitty color protocol key: {f}", .{key}, ); continue; @@ -1571,12 +1546,12 @@ pub const StreamHandler = struct { } // If we had any writes to our buffer, we queue them now - if (buf.items.len > 0) { + if (stream.written().len > 0) { try writer.writeAll(request.terminator.string()); self.messageWriter(.{ .write_alloc = .{ .alloc = self.alloc, - .data = try buf.toOwnedSlice(), + .data = try stream.toOwnedSlice(), }, }); } diff --git a/src/unicode/Properties.zig b/src/unicode/Properties.zig index b7840743a..c8c4a581c 100644 --- a/src/unicode/Properties.zig +++ b/src/unicode/Properties.zig @@ -24,13 +24,9 @@ pub fn eql(a: Properties, b: Properties) bool { // Needed for lut.Generator pub fn format( self: Properties, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, + writer: *std.Io.Writer, ) !void { - _ = layout; - _ = opts; - try std.fmt.format(writer, + try writer.print( \\.{{ \\ .width= {}, \\ .grapheme_boundary_class= .{s}, diff --git a/src/unicode/grapheme.zig b/src/unicode/grapheme.zig index bfc09b854..2311bbeec 100644 --- a/src/unicode/grapheme.zig +++ b/src/unicode/grapheme.zig @@ -151,35 +151,39 @@ fn graphemeBreakClass( /// If you build this file as a binary, we will verify the grapheme break /// implementation. This iterates over billions of codepoints so it is /// SLOW. It's not meant to be run in CI, but it's useful for debugging. +/// TODO: this is hard to build with newer zig build, so +/// https://github.com/ghostty-org/ghostty/pull/7806 took the approach of +/// adding a `-Demit-unicode-test` option for `zig build`, but that +/// hasn't been done here. pub fn main() !void { - const ziglyph = @import("ziglyph"); + const uucode = @import("uucode"); // Set the min and max to control the test range. const min = 0; - const max = std.math.maxInt(u21) + 1; + const max = uucode.config.max_code_point + 1; var state: BreakState = .{}; - var zg_state: u3 = 0; + var uu_state: uucode.grapheme.BreakState = .default; for (min..max) |cp1| { if (cp1 % 1000 == 0) std.log.warn("progress cp1={}", .{cp1}); if (cp1 == '\r' or cp1 == '\n' or - ziglyph.grapheme_break.isControl(@intCast(cp1))) continue; + uucode.get(.grapheme_break, @intCast(cp1)) == .control) continue; for (min..max) |cp2| { if (cp2 == '\r' or cp2 == '\n' or - ziglyph.grapheme_break.isControl(@intCast(cp2))) continue; + uucode.get(.grapheme_break, @intCast(cp1)) == .control) continue; const gb = graphemeBreak(@intCast(cp1), @intCast(cp2), &state); - const zg_gb = ziglyph.graphemeBreak(@intCast(cp1), @intCast(cp2), &zg_state); - if (gb != zg_gb) { - std.log.warn("cp1={x} cp2={x} gb={} state={} zg_gb={} zg_state={}", .{ + const uu_gb = uucode.grapheme.isBreak(@intCast(cp1), @intCast(cp2), &uu_state); + if (gb != uu_gb) { + std.log.warn("cp1={x} cp2={x} gb={} state={} uu_gb={} uu_state={}", .{ cp1, cp2, gb, state, - zg_gb, - zg_state, + uu_gb, + uu_state, }); } } diff --git a/src/unicode/lut.zig b/src/unicode/lut.zig index e10c5c0b8..da90f1ee7 100644 --- a/src/unicode/lut.zig +++ b/src/unicode/lut.zig @@ -54,12 +54,14 @@ pub fn Generator( defer blocks_map.deinit(); // Our stages - var stage1 = std.ArrayList(u16).init(alloc); - defer stage1.deinit(); - var stage2 = std.ArrayList(u16).init(alloc); - defer stage2.deinit(); - var stage3 = std.ArrayList(Elem).init(alloc); - defer stage3.deinit(); + var stage1: std.ArrayList(u16) = .empty; + var stage2: std.ArrayList(u16) = .empty; + var stage3: std.ArrayList(Elem) = .empty; + defer { + stage1.deinit(alloc); + stage2.deinit(alloc); + stage3.deinit(alloc); + } var block: Block = undefined; var block_len: u16 = 0; @@ -74,7 +76,7 @@ pub fn Generator( } const idx = stage3.items.len; - try stage3.append(elem); + try stage3.append(alloc, elem); break :block_idx idx; }; @@ -96,11 +98,11 @@ pub fn Generator( u16, stage2.items.len, ) orelse return error.Stage2TooLarge; - for (block[0..block_len]) |entry| try stage2.append(entry); + for (block[0..block_len]) |entry| try stage2.append(alloc, entry); } // Map stage1 => stage2 and reset our block - try stage1.append(gop.value_ptr.*); + try stage1.append(alloc, gop.value_ptr.*); block_len = 0; } @@ -109,11 +111,11 @@ pub fn Generator( assert(stage2.items.len <= std.math.maxInt(u16)); assert(stage3.items.len <= std.math.maxInt(u16)); - const stage1_owned = try stage1.toOwnedSlice(); + const stage1_owned = try stage1.toOwnedSlice(alloc); errdefer alloc.free(stage1_owned); - const stage2_owned = try stage2.toOwnedSlice(); + const stage2_owned = try stage2.toOwnedSlice(alloc); errdefer alloc.free(stage2_owned); - const stage3_owned = try stage3.toOwnedSlice(); + const stage3_owned = try stage3.toOwnedSlice(alloc); errdefer alloc.free(stage3_owned); return .{ @@ -145,7 +147,7 @@ pub fn Tables(comptime Elem: type) type { /// Writes the lookup table as Zig to the given writer. The /// written file exports three constants: stage1, stage2, and /// stage3. These can be used to rebuild the lookup table in Zig. - pub fn writeZig(self: *const Self, writer: anytype) !void { + pub fn writeZig(self: *const Self, writer: *std.Io.Writer) !void { try writer.print( \\//! This file is auto-generated. Do not edit. \\ @@ -168,7 +170,13 @@ pub fn Tables(comptime Elem: type) type { \\ \\pub const stage3: [{}]Elem = .{{ , .{self.stage3.len}); - for (self.stage3) |entry| try writer.print("{},", .{entry}); + for (self.stage3) |entry| { + if (@typeInfo(@TypeOf(entry)) == .@"struct" and + @hasDecl(@TypeOf(entry), "format")) + try writer.print("{f},", .{entry}) + else + try writer.print("{},", .{entry}); + } try writer.writeAll( \\}; \\ }; diff --git a/src/unicode/props_table.zig b/src/unicode/props_table.zig index 80492346c..d168fbb9c 100644 --- a/src/unicode/props_table.zig +++ b/src/unicode/props_table.zig @@ -7,7 +7,7 @@ pub const table = table: { // build.zig process, but due to Zig's lazy analysis we can still reference // it here. // - // An example process is the `main` in `props_ziglyph.zig` + // An example process is the `main` in `props_uucode.zig` const generated = @import("unicode_tables").Tables(Properties); const Tables = lut.Tables(Properties); break :table Tables{ diff --git a/src/unicode/props_uucode.zig b/src/unicode/props_uucode.zig new file mode 100644 index 000000000..6aed7d7d5 --- /dev/null +++ b/src/unicode/props_uucode.zig @@ -0,0 +1,120 @@ +const props = @This(); +const std = @import("std"); +const assert = std.debug.assert; +const uucode = @import("uucode"); +const lut = @import("lut.zig"); +const Properties = @import("Properties.zig"); +const GraphemeBoundaryClass = Properties.GraphemeBoundaryClass; + +/// Gets the grapheme boundary class for a codepoint. +/// The use case for this is only in generating lookup tables. +fn graphemeBoundaryClass(cp: u21) GraphemeBoundaryClass { + if (cp > uucode.config.max_code_point) return .invalid; + + // We special-case modifier bases because we should not break + // if a modifier isn't next to a base. + if (uucode.get(.is_emoji_modifier, cp)) return .emoji_modifier; + if (uucode.get(.is_emoji_modifier_base, cp)) return .extended_pictographic_base; + + return switch (uucode.get(.grapheme_break, cp)) { + .extended_pictographic => .extended_pictographic, + .l => .L, + .v => .V, + .t => .T, + .lv => .LV, + .lvt => .LVT, + .prepend => .prepend, + .zwj => .zwj, + .spacing_mark => .spacing_mark, + .regional_indicator => .regional_indicator, + + .zwnj, + .indic_conjunct_break_extend, + .indic_conjunct_break_linker, + => .extend, + + // This is obviously not INVALID invalid, there is SOME grapheme + // boundary class for every codepoint. But we don't care about + // anything that doesn't fit into the above categories. Also note + // that `indic_conjunct_break_consonant` is `other` in + // 'GraphemeBreakProperty.txt' (it's missing). + .other, + .indic_conjunct_break_consonant, + .cr, + .lf, + .control, + => .invalid, + }; +} + +pub fn get(cp: u21) Properties { + const width = if (cp > uucode.config.max_code_point) + 1 + else + uucode.get(.width, cp); + + return .{ + .width = width, + .grapheme_boundary_class = graphemeBoundaryClass(cp), + }; +} + +/// Runnable binary to generate the lookup tables and output to stdout. +pub fn main() !void { + var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena_state.deinit(); + const alloc = arena_state.allocator(); + + const gen: lut.Generator( + Properties, + struct { + pub fn get(ctx: @This(), cp: u21) !Properties { + _ = ctx; + return props.get(cp); + } + + pub fn eql(ctx: @This(), a: Properties, b: Properties) bool { + _ = ctx; + return a.eql(b); + } + }, + ) = .{}; + + const t = try gen.generate(alloc); + defer alloc.free(t.stage1); + defer alloc.free(t.stage2); + defer alloc.free(t.stage3); + + var buf: [4096]u8 = undefined; + var stdout = std.fs.File.stdout().writer(&buf); + try t.writeZig(&stdout.interface); + try stdout.end(); + + // Uncomment when manually debugging to see our table sizes. + // std.log.warn("stage1={} stage2={} stage3={}", .{ + // t.stage1.len, + // t.stage2.len, + // t.stage3.len, + // }); +} + +test "unicode props: tables match uucode" { + if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest; + + const testing = std.testing; + const table = @import("props_table.zig").table; + + const min = 0xFF + 1; // start outside ascii + const max = std.math.maxInt(u21) + 1; + for (min..max) |cp| { + const t = table.get(@intCast(cp)); + const uu = if (cp > uucode.config.max_code_point) + 1 + else + uucode.get(.width, @intCast(cp)); + if (t.width != uu) { + std.log.warn("mismatch cp=U+{x} t={} uu={}", .{ cp, t.width, uu }); + try testing.expect(false); + } + } +} diff --git a/src/unicode/props_ziglyph.zig b/src/unicode/props_ziglyph.zig deleted file mode 100644 index 9af60e337..000000000 --- a/src/unicode/props_ziglyph.zig +++ /dev/null @@ -1,96 +0,0 @@ -const props = @This(); - -const std = @import("std"); -const assert = std.debug.assert; -const ziglyph = @import("ziglyph"); -const lut = @import("lut.zig"); -const Properties = @import("Properties.zig"); -const GraphemeBoundaryClass = Properties.GraphemeBoundaryClass; - -/// Gets the grapheme boundary class for a codepoint. This is VERY -/// SLOW. The use case for this is only in generating lookup tables. -fn graphemeBoundaryClass(cp: u21) GraphemeBoundaryClass { - // We special-case modifier bases because we should not break - // if a modifier isn't next to a base. - if (ziglyph.emoji.isEmojiModifierBase(cp)) { - assert(ziglyph.emoji.isExtendedPictographic(cp)); - return .extended_pictographic_base; - } - - if (ziglyph.emoji.isEmojiModifier(cp)) return .emoji_modifier; - if (ziglyph.emoji.isExtendedPictographic(cp)) return .extended_pictographic; - if (ziglyph.grapheme_break.isL(cp)) return .L; - if (ziglyph.grapheme_break.isV(cp)) return .V; - if (ziglyph.grapheme_break.isT(cp)) return .T; - if (ziglyph.grapheme_break.isLv(cp)) return .LV; - if (ziglyph.grapheme_break.isLvt(cp)) return .LVT; - if (ziglyph.grapheme_break.isPrepend(cp)) return .prepend; - if (ziglyph.grapheme_break.isExtend(cp)) return .extend; - if (ziglyph.grapheme_break.isZwj(cp)) return .zwj; - if (ziglyph.grapheme_break.isSpacingmark(cp)) return .spacing_mark; - if (ziglyph.grapheme_break.isRegionalIndicator(cp)) return .regional_indicator; - - // This is obviously not INVALID invalid, there is SOME grapheme - // boundary class for every codepoint. But we don't care about - // anything that doesn't fit into the above categories. - return .invalid; -} - -pub fn get(cp: u21) Properties { - const zg_width = ziglyph.display_width.codePointWidth(cp, .half); - return .{ - .width = @intCast(@min(2, @max(0, zg_width))), - .grapheme_boundary_class = graphemeBoundaryClass(cp), - }; -} - -/// Runnable binary to generate the lookup tables and output to stdout. -pub fn main() !void { - var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator); - defer arena_state.deinit(); - const alloc = arena_state.allocator(); - - const gen: lut.Generator( - Properties, - struct { - pub fn get(ctx: @This(), cp: u21) !Properties { - _ = ctx; - return props.get(cp); - } - - pub fn eql(ctx: @This(), a: Properties, b: Properties) bool { - _ = ctx; - return a.eql(b); - } - }, - ) = .{}; - - const t = try gen.generate(alloc); - defer alloc.free(t.stage1); - defer alloc.free(t.stage2); - defer alloc.free(t.stage3); - try t.writeZig(std.io.getStdOut().writer()); - - // Uncomment when manually debugging to see our table sizes. - // std.log.warn("stage1={} stage2={} stage3={}", .{ - // t.stage1.len, - // t.stage2.len, - // t.stage3.len, - // }); -} - -// This is not very fast in debug modes, so its commented by default. -// IMPORTANT: UNCOMMENT THIS WHENEVER MAKING CODEPOINTWIDTH CHANGES. -// test "unicode props: tables match ziglyph" { -// const testing = std.testing; -// -// const min = 0xFF + 1; // start outside ascii -// for (min..std.math.maxInt(u21)) |cp| { -// const t = table.get(@intCast(cp)); -// const zg = @min(2, @max(0, ziglyph.display_width.codePointWidth(@intCast(cp), .half))); -// if (t.width != zg) { -// std.log.warn("mismatch cp=U+{x} t={} zg={}", .{ cp, t, zg }); -// try testing.expect(false); -// } -// } -// } diff --git a/src/unicode/symbols_table.zig b/src/unicode/symbols_table.zig index 28263b9be..034d34428 100644 --- a/src/unicode/symbols_table.zig +++ b/src/unicode/symbols_table.zig @@ -6,7 +6,7 @@ pub const table = table: { // build.zig process, but due to Zig's lazy analysis we can still reference // it here. // - // An example process is the `main` in `symbols_ziglyph.zig` + // An example process is the `main` in `symbols_uucode.zig` const generated = @import("symbols_tables").Tables(bool); const Tables = lut.Tables(bool); break :table Tables{ diff --git a/src/unicode/symbols_uucode.zig b/src/unicode/symbols_uucode.zig new file mode 100644 index 000000000..8cbd59211 --- /dev/null +++ b/src/unicode/symbols_uucode.zig @@ -0,0 +1,65 @@ +const std = @import("std"); +const uucode = @import("uucode"); +const lut = @import("lut.zig"); + +/// Runnable binary to generate the lookup tables and output to stdout. +pub fn main() !void { + var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena_state.deinit(); + const alloc = arena_state.allocator(); + + const gen: lut.Generator( + bool, + struct { + pub fn get(ctx: @This(), cp: u21) !bool { + _ = ctx; + return if (cp > uucode.config.max_code_point) + false + else + uucode.get(.is_symbol, @intCast(cp)); + } + + pub fn eql(ctx: @This(), a: bool, b: bool) bool { + _ = ctx; + return a == b; + } + }, + ) = .{}; + + const t = try gen.generate(alloc); + defer alloc.free(t.stage1); + defer alloc.free(t.stage2); + defer alloc.free(t.stage3); + + var buf: [4096]u8 = undefined; + var stdout = std.fs.File.stdout().writer(&buf); + try t.writeZig(&stdout.interface); + try stdout.end(); + + // Uncomment when manually debugging to see our table sizes. + // std.log.warn("stage1={} stage2={} stage3={}", .{ + // t.stage1.len, + // t.stage2.len, + // t.stage3.len, + // }); +} + +test "unicode symbols: tables match uucode" { + if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest; + + const testing = std.testing; + const table = @import("symbols_table.zig").table; + + for (0..std.math.maxInt(u21)) |cp| { + const t = table.get(@intCast(cp)); + const uu = if (cp > uucode.config.max_code_point) + false + else + uucode.get(.is_symbol, @intCast(cp)); + + if (t != uu) { + std.log.warn("mismatch cp=U+{x} t={} uu={}", .{ cp, t, uu }); + try testing.expect(false); + } + } +} diff --git a/src/unicode/symbols_ziglyph.zig b/src/unicode/symbols_ziglyph.zig deleted file mode 100644 index 0b01e5398..000000000 --- a/src/unicode/symbols_ziglyph.zig +++ /dev/null @@ -1,83 +0,0 @@ -const props = @This(); -const std = @import("std"); -const assert = std.debug.assert; -const ziglyph = @import("ziglyph"); -const lut = @import("lut.zig"); - -/// Returns true of the codepoint is a "symbol-like" character, which -/// for now we define as anything in a private use area and anything -/// in several unicode blocks: -/// - Dingbats -/// - Emoticons -/// - Miscellaneous Symbols -/// - Enclosed Alphanumerics -/// - Enclosed Alphanumeric Supplement -/// - Miscellaneous Symbols and Pictographs -/// - Transport and Map Symbols -/// -/// In the future it may be prudent to expand this to encompass more -/// symbol-like characters, and/or exclude some PUA sections. -pub fn isSymbol(cp: u21) bool { - return ziglyph.general_category.isPrivateUse(cp) or - ziglyph.blocks.isDingbats(cp) or - ziglyph.blocks.isEmoticons(cp) or - ziglyph.blocks.isMiscellaneousSymbols(cp) or - ziglyph.blocks.isEnclosedAlphanumerics(cp) or - ziglyph.blocks.isEnclosedAlphanumericSupplement(cp) or - ziglyph.blocks.isMiscellaneousSymbolsAndPictographs(cp) or - ziglyph.blocks.isTransportAndMapSymbols(cp); -} - -/// Runnable binary to generate the lookup tables and output to stdout. -pub fn main() !void { - var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator); - defer arena_state.deinit(); - const alloc = arena_state.allocator(); - - const gen: lut.Generator( - bool, - struct { - pub fn get(ctx: @This(), cp: u21) !bool { - _ = ctx; - return isSymbol(cp); - } - - pub fn eql(ctx: @This(), a: bool, b: bool) bool { - _ = ctx; - return a == b; - } - }, - ) = .{}; - - const t = try gen.generate(alloc); - defer alloc.free(t.stage1); - defer alloc.free(t.stage2); - defer alloc.free(t.stage3); - try t.writeZig(std.io.getStdOut().writer()); - - // Uncomment when manually debugging to see our table sizes. - // std.log.warn("stage1={} stage2={} stage3={}", .{ - // t.stage1.len, - // t.stage2.len, - // t.stage3.len, - // }); -} - -// This is not very fast in debug modes, so its commented by default. -// IMPORTANT: UNCOMMENT THIS WHENEVER MAKING CHANGES. -test "unicode symbols: tables match ziglyph" { - if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest; - - const testing = std.testing; - const table = @import("symbols_table.zig").table; - - for (0..std.math.maxInt(u21)) |cp| { - const t = table.get(@intCast(cp)); - const zg = isSymbol(@intCast(cp)); - - if (t != zg) { - std.log.warn("mismatch cp=U+{x} t={} zg={}", .{ cp, t, zg }); - try testing.expect(false); - } - } -} diff --git a/typos.toml b/typos.toml index 5a23527d9..26876aef9 100644 --- a/typos.toml +++ b/typos.toml @@ -5,6 +5,9 @@ extend-exclude = [ "build.zig.zon.nix", "build.zig.zon.txt", "build.zig.zon.json", + # Build artifacts + "macos/build/*", + "zig-out/*", # vendored code "vendor/*", "pkg/*", @@ -55,6 +58,10 @@ typ = "typ" kend = "kend" # GTK GIR = "GIR" +# terminfo +rin = "rin" +# sprites +ower = "ower" [type.po] extend-glob = ["*.po"]