diff --git a/.github/scripts/ghostty-tip b/.github/scripts/ghostty-tip new file mode 100755 index 000000000..8b577bcac --- /dev/null +++ b/.github/scripts/ghostty-tip @@ -0,0 +1,19 @@ +#!/usr/bin/env nu + +# Check if a given commit SHA has a corresponding tip release. +# +# This does not validate that the commit SHA is valid for the +# Ghostty repository, only that a tip release exists for it. +def main [ + commit: string, # The full length commit SHA +] { + let url = $"https://tip.files.ghostty.org/($commit)/ghostty-macos-universal.zip" + + try { + http head $url + exit 0 + } catch { + print -e $"The SHA ($commit) does not have a corresponding tip release." + exit 1 + } +} diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml deleted file mode 100644 index 8c42a2a55..000000000 --- a/.github/workflows/release-pr.yml +++ /dev/null @@ -1,357 +0,0 @@ -on: - workflow_dispatch: {} - -name: Release PR - -jobs: - sentry-dsym-debug: - runs-on: namespace-profile-ghostty-sm - needs: [build-macos-debug] - steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - - name: Install sentry-cli - run: | - curl -sL https://sentry.io/get-cli/ | bash - - - name: Download dSYM - run: | - GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD) - curl -L https://pr.files.ghostty.dev/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-dsym.zip > dsym.zip - - - name: Upload dSYM to Sentry - env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - run: | - sentry-cli dif upload --project ghostty --wait dsym.zip - - sentry-dsym: - runs-on: namespace-profile-ghostty-sm - needs: [build-macos] - steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - - name: Install sentry-cli - run: | - curl -sL https://sentry.io/get-cli/ | bash - - - name: Download dSYM - run: | - GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD) - curl -L https://pr.files.ghostty.dev/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-dsym.zip > dsym.zip - - - name: Upload dSYM to Sentry - env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - run: | - sentry-cli dif upload --project ghostty --wait dsym.zip - - build-macos: - runs-on: namespace-profile-ghostty-macos-tahoe - timeout-minutes: 90 - steps: - - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - # Important so that build number generation works - fetch-depth: 0 - - # Install Nix and use that to run our tests so our environment matches exactly. - - uses: DeterminateSystems/nix-installer-action@main - with: - determinate: true - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - with: - name: ghostty - authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - - # Setup Sparkle - - name: Setup Sparkle - env: - SPARKLE_VERSION: 2.7.1 - run: | - mkdir -p .action/sparkle - cd .action/sparkle - curl -L https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-for-Swift-Package-Manager.zip > sparkle.zip - unzip sparkle.zip - echo "$(pwd)/bin" >> $GITHUB_PATH - - # Load Build Number - - name: Build Number - run: | - echo "GHOSTTY_BUILD=$(git rev-list --count head)" >> $GITHUB_ENV - echo "GHOSTTY_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - echo "GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD)" >> $GITHUB_ENV - - # GhosttyKit is the framework that is built from Zig for our native - # Mac app to access. Build this in release mode. - - name: Build GhosttyKit - run: nix develop -c zig build -Doptimize=ReleaseSafe -Demit-macos-app=false - - # The native app is built with native XCode tooling. This also does - # codesigning. IMPORTANT: this must NOT run in a Nix environment. - # Nix breaks xcodebuild so this has to be run outside. - - name: Build Ghostty.app - run: | - cd macos - sudo xcode-select -s /Applications/Xcode_26.0.app - xcodebuild -version - xcodebuild -target Ghostty -configuration Release - - # We inject the "build number" as simply the number of commits since HEAD. - # This will be a monotonically always increasing build number that we use. - - name: Update Info.plist - env: - SPARKLE_KEY_PUB: ${{ secrets.PROD_MACOS_SPARKLE_KEY_PUB }} - run: | - # Version Info - /usr/libexec/PlistBuddy -c "Set :GhosttyCommit $GHOSTTY_COMMIT" "macos/build/Release/Ghostty.app/Contents/Info.plist" - /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $GHOSTTY_BUILD" "macos/build/Release/Ghostty.app/Contents/Info.plist" - /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $GHOSTTY_COMMIT" "macos/build/Release/Ghostty.app/Contents/Info.plist" - - # Updater - /usr/libexec/PlistBuddy -c "Set :SUPublicEDKey $SPARKLE_KEY_PUB" "macos/build/Release/Ghostty.app/Contents/Info.plist" - /usr/libexec/PlistBuddy -c "Delete :SUEnableAutomaticChecks" "macos/build/Release/Ghostty.app/Contents/Info.plist" - - - name: Codesign app bundle - env: - MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }} - MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} - MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} - run: | - # Turn our base64-encoded certificate back to a regular .p12 file - echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12 - - # We need to create a new keychain, otherwise using the certificate will prompt - # with a UI dialog asking for the certificate password, which we can't - # use in a headless CI environment - security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain - security default-keychain -s build.keychain - security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain - security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain - - # Codesign Sparkle. Some notes here: - # - The XPC services aren't used since we don't sandbox Ghostty, - # but since they're part of the build, they still need to be - # codesigned. - # - The binaries in the "Versions" folders need to NOT be symlinks. - /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Downloader.xpc" - /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Installer.xpc" - /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate" - /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Updater.app" - /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework" - - # Codesign the app bundle - /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime --entitlements "macos/Ghostty.entitlements" macos/build/Release/Ghostty.app - - - name: "Notarize app bundle" - env: - APPLE_NOTARIZATION_ISSUER: ${{ secrets.APPLE_NOTARIZATION_ISSUER }} - APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} - APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }} - run: | - # Store the notarization credentials so that we can prevent a UI password dialog - # from blocking the CI - echo "Create keychain profile" - echo "$APPLE_NOTARIZATION_KEY" > notarization_key.p8 - xcrun notarytool store-credentials "notarytool-profile" --key notarization_key.p8 --key-id "$APPLE_NOTARIZATION_KEY_ID" --issuer "$APPLE_NOTARIZATION_ISSUER" - rm notarization_key.p8 - - # We can't notarize an app bundle directly, but we need to compress it as an archive. - # Therefore, we create a zip file containing our app bundle, so that we can send it to the - # notarization service - echo "Creating temp notarization archive" - ditto -c -k --keepParent "macos/build/Release/Ghostty.app" "notarization.zip" - - # Here we send the notarization request to the Apple's Notarization service, waiting for the result. - # This typically takes a few seconds inside a CI environment, but it might take more depending on the App - # characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if - # you're curious - echo "Notarize app" - xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait - - # Finally, we need to "attach the staple" to our executable, which will allow our app to be - # validated by macOS even when an internet connection is not available. - echo "Attach staple" - xcrun stapler staple "macos/build/Release/Ghostty.app" - - # Zip up the app - - name: Zip App - run: | - cd macos/build/Release - zip -9 -r --symlinks ../../../ghostty-macos-universal.zip Ghostty.app - zip -9 -r --symlinks ../../../ghostty-macos-universal-dsym.zip Ghostty.app.dSYM/ - - # Update Blob Storage - - name: Prep R2 Storage - run: | - mkdir blob - mkdir -p blob/${GHOSTTY_COMMIT_LONG} - cp ghostty-macos-universal.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal.zip - cp ghostty-macos-universal-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-dsym.zip - - name: Upload to R2 - uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4 - with: - r2-account-id: ${{ secrets.CF_R2_PR_ACCOUNT_ID }} - r2-access-key-id: ${{ secrets.CF_R2_PR_AWS_KEY }} - r2-secret-access-key: ${{ secrets.CF_R2_PR_SECRET_KEY }} - r2-bucket: ghostty-pr - source-dir: blob - destination-dir: ./ - - build-macos-debug: - runs-on: namespace-profile-ghostty-macos-tahoe - timeout-minutes: 90 - steps: - - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - # Important so that build number generation works - fetch-depth: 0 - - # Install Nix and use that to run our tests so our environment matches exactly. - - uses: DeterminateSystems/nix-installer-action@main - with: - determinate: true - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - with: - name: ghostty - authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - - # Setup Sparkle - - name: Setup Sparkle - env: - SPARKLE_VERSION: 2.7.1 - run: | - mkdir -p .action/sparkle - cd .action/sparkle - curl -L https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-for-Swift-Package-Manager.zip > sparkle.zip - unzip sparkle.zip - echo "$(pwd)/bin" >> $GITHUB_PATH - - # Load Build Number - - name: Build Number - run: | - echo "GHOSTTY_BUILD=$(git rev-list --count head)" >> $GITHUB_ENV - echo "GHOSTTY_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - echo "GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD)" >> $GITHUB_ENV - - # GhosttyKit is the framework that is built from Zig for our native - # Mac app to access. Build this in release mode. - - name: Build GhosttyKit - run: nix develop -c zig build -Doptimize=Debug -Demit-macos-app=false - - # The native app is built with native XCode tooling. This also does - # codesigning. IMPORTANT: this must NOT run in a Nix environment. - # Nix breaks xcodebuild so this has to be run outside. - - name: Build Ghostty.app - run: | - cd macos - sudo xcode-select -s /Applications/Xcode_26.0.app - xcodebuild -version - xcodebuild -target Ghostty -configuration Release - - # We inject the "build number" as simply the number of commits since HEAD. - # This will be a monotonically always increasing build number that we use. - - name: Update Info.plist - env: - SPARKLE_KEY_PUB: ${{ secrets.PROD_MACOS_SPARKLE_KEY_PUB }} - run: | - # Version Info - /usr/libexec/PlistBuddy -c "Set :GhosttyCommit $GHOSTTY_COMMIT" "macos/build/Release/Ghostty.app/Contents/Info.plist" - /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $GHOSTTY_BUILD" "macos/build/Release/Ghostty.app/Contents/Info.plist" - /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $GHOSTTY_COMMIT" "macos/build/Release/Ghostty.app/Contents/Info.plist" - - # Updater - /usr/libexec/PlistBuddy -c "Set :SUPublicEDKey $SPARKLE_KEY_PUB" "macos/build/Release/Ghostty.app/Contents/Info.plist" - /usr/libexec/PlistBuddy -c "Delete :SUEnableAutomaticChecks" "macos/build/Release/Ghostty.app/Contents/Info.plist" - - - name: Codesign app bundle - env: - MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }} - MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }} - MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} - run: | - # Turn our base64-encoded certificate back to a regular .p12 file - echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12 - - # We need to create a new keychain, otherwise using the certificate will prompt - # with a UI dialog asking for the certificate password, which we can't - # use in a headless CI environment - security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain - security default-keychain -s build.keychain - security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain - security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain - - # Codesign Sparkle. Some notes here: - # - The XPC services aren't used since we don't sandbox Ghostty, - # but since they're part of the build, they still need to be - # codesigned. - # - The binaries in the "Versions" folders need to NOT be symlinks. - /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Downloader.xpc" - /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Installer.xpc" - /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate" - /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Updater.app" - /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework" - - # Codesign the app bundle - /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime --entitlements "macos/Ghostty.entitlements" macos/build/Release/Ghostty.app - - - name: "Notarize app bundle" - env: - APPLE_NOTARIZATION_ISSUER: ${{ secrets.APPLE_NOTARIZATION_ISSUER }} - APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} - APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }} - run: | - # Store the notarization credentials so that we can prevent a UI password dialog - # from blocking the CI - echo "Create keychain profile" - echo "$APPLE_NOTARIZATION_KEY" > notarization_key.p8 - xcrun notarytool store-credentials "notarytool-profile" --key notarization_key.p8 --key-id "$APPLE_NOTARIZATION_KEY_ID" --issuer "$APPLE_NOTARIZATION_ISSUER" - rm notarization_key.p8 - - # We can't notarize an app bundle directly, but we need to compress it as an archive. - # Therefore, we create a zip file containing our app bundle, so that we can send it to the - # notarization service - echo "Creating temp notarization archive" - ditto -c -k --keepParent "macos/build/Release/Ghostty.app" "notarization.zip" - - # Here we send the notarization request to the Apple's Notarization service, waiting for the result. - # This typically takes a few seconds inside a CI environment, but it might take more depending on the App - # characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if - # you're curious - echo "Notarize app" - xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait - - # Finally, we need to "attach the staple" to our executable, which will allow our app to be - # validated by macOS even when an internet connection is not available. - echo "Attach staple" - xcrun stapler staple "macos/build/Release/Ghostty.app" - - # Zip up the app - - name: Zip App - run: | - cd macos/build/Release - zip -9 -r --symlinks ../../../ghostty-macos-universal-debug.zip Ghostty.app - zip -9 -r --symlinks ../../../ghostty-macos-universal-debug-dsym.zip Ghostty.app.dSYM/ - - # Update Blob Storage - - name: Prep R2 Storage - run: | - mkdir blob - mkdir -p blob/${GHOSTTY_COMMIT_LONG} - cp ghostty-macos-universal-debug.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug.zip - cp ghostty-macos-universal-debug-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-dsym.zip - - name: Upload to R2 - uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4 - with: - r2-account-id: ${{ secrets.CF_R2_PR_ACCOUNT_ID }} - r2-access-key-id: ${{ secrets.CF_R2_PR_AWS_KEY }} - r2-secret-access-key: ${{ secrets.CF_R2_PR_SECRET_KEY }} - r2-bucket: ghostty-pr - source-dir: blob - destination-dir: ./ diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 009708f56..f53e426e7 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -15,9 +15,57 @@ concurrency: cancel-in-progress: false jobs: + setup: + if: | + github.event_name == 'workflow_dispatch' || + ( + github.event.workflow_run.conclusion == 'success' && + github.repository_owner == 'ghostty-org' && + github.ref_name == 'main' + ) + runs-on: namespace-profile-ghostty-sm + outputs: + should_skip: ${{ steps.check.outputs.should_skip }} + build: ${{ steps.extract_build_info.outputs.build }} + commit: ${{ steps.extract_build_info.outputs.commit }} + commit_long: ${{ steps.extract_build_info.outputs.commit_long }} + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + # Important so that build number generation works + fetch-depth: 0 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + - name: Extract build info + id: extract_build_info + run: | + GHOSTTY_BUILD=$(git rev-list --count HEAD) + GHOSTTY_COMMIT=$(git rev-parse --short HEAD) + GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD) + echo "build=$GHOSTTY_BUILD" >> $GITHUB_OUTPUT + echo "commit=$GHOSTTY_COMMIT" >> $GITHUB_OUTPUT + echo "commit_long=$GHOSTTY_COMMIT_LONG" >> $GITHUB_OUTPUT + - name: Check if tip already exists + id: check + run: | + GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD) + if nix develop -c nu .github/scripts/ghostty-tip $GHOSTTY_COMMIT_LONG; then + echo "Tip release already exists for commit $GHOSTTY_COMMIT_LONG" + echo "should_skip=true" >> $GITHUB_OUTPUT + else + echo "No tip release found for commit $GHOSTTY_COMMIT_LONG" + echo "should_skip=false" >> $GITHUB_OUTPUT + fi + tag: runs-on: namespace-profile-ghostty-sm - needs: [build-macos] + needs: [setup, build-macos] + if: needs.setup.outputs.should_skip != 'true' steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Tip Tag @@ -29,7 +77,10 @@ jobs: sentry-dsym-debug-slow: runs-on: namespace-profile-ghostty-sm - needs: [build-macos-debug-slow] + needs: [setup, build-macos-debug-slow] + if: needs.setup.outputs.should_skip != 'true' + env: + GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -39,7 +90,6 @@ jobs: - name: Download dSYM run: | - GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD) curl -L https://tip.files.ghostty.dev/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-slow-dsym.zip > dsym.zip - name: Upload dSYM to Sentry @@ -50,7 +100,10 @@ jobs: sentry-dsym-debug-fast: runs-on: namespace-profile-ghostty-sm - needs: [build-macos-debug-fast] + needs: [setup, build-macos-debug-fast] + if: needs.setup.outputs.should_skip != 'true' + env: + GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -60,7 +113,6 @@ jobs: - name: Download dSYM run: | - GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD) curl -L https://tip.files.ghostty.dev/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-fast-dsym.zip > dsym.zip - name: Upload dSYM to Sentry @@ -71,7 +123,10 @@ jobs: sentry-dsym: runs-on: namespace-profile-ghostty-sm - needs: [build-macos] + needs: [setup, build-macos] + if: needs.setup.outputs.should_skip != 'true' + env: + GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -81,7 +136,6 @@ jobs: - name: Download dSYM run: | - GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD) curl -L https://tip.files.ghostty.dev/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-dsym.zip > dsym.zip - name: Upload dSYM to Sentry @@ -91,15 +145,17 @@ jobs: sentry-cli dif upload --project ghostty --wait dsym.zip source-tarball: + needs: [setup] if: | - ${{ + needs.setup.outputs.should_skip != 'true' && + ( github.event_name == 'workflow_dispatch' || ( github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' ) - }} + ) runs-on: namespace-profile-ghostty-md env: ZIG_LOCAL_CACHE_DIR: /zig/local-cache @@ -144,18 +200,24 @@ jobs: token: ${{ secrets.GH_RELEASE_TOKEN }} build-macos: + needs: [setup] if: | - ${{ + needs.setup.outputs.should_skip != 'true' && + ( github.event_name == 'workflow_dispatch' || ( github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' ) - }} + ) runs-on: namespace-profile-ghostty-macos-tahoe timeout-minutes: 90 + env: + GHOSTTY_BUILD: ${{ needs.setup.outputs.build }} + GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }} + GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} steps: - name: Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -189,13 +251,6 @@ jobs: unzip sparkle.zip echo "$(pwd)/bin" >> $GITHUB_PATH - # Load Build Number - - name: Build Number - run: | - echo "GHOSTTY_BUILD=$(git rev-list --count head)" >> $GITHUB_ENV - echo "GHOSTTY_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - echo "GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD)" >> $GITHUB_ENV - # GhosttyKit is the framework that is built from Zig for our native # Mac app to access. Build this in release mode. - name: Build GhosttyKit @@ -317,6 +372,10 @@ 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: SPARKLE_KEY: ${{ secrets.PROD_MACOS_SPARKLE_KEY }} run: | @@ -348,12 +407,20 @@ jobs: # Now upload our appcast. This ensures that the appcast never # 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: | rm -r blob mkdir blob cp appcast_new.xml blob/appcast.xml - 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 with: r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }} @@ -363,19 +430,32 @@ jobs: source-dir: blob destination-dir: ./ + - name: Echo Release URLs + run: | + echo "Release URLs:" + echo " App Bundle: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal.zip" + echo " Debug Symbols: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-dsym.zip" + echo " DMG: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/Ghostty.dmg" + build-macos-debug-slow: + needs: [setup] if: | - ${{ + needs.setup.outputs.should_skip != 'true' && + ( github.event_name == 'workflow_dispatch' || ( github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' ) - }} + ) runs-on: namespace-profile-ghostty-macos-tahoe timeout-minutes: 90 + env: + GHOSTTY_BUILD: ${{ needs.setup.outputs.build }} + GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }} + GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} steps: - name: Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -409,13 +489,6 @@ jobs: unzip sparkle.zip echo "$(pwd)/bin" >> $GITHUB_PATH - # Load Build Number - - name: Build Number - run: | - echo "GHOSTTY_BUILD=$(git rev-list --count head)" >> $GITHUB_ENV - echo "GHOSTTY_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - echo "GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD)" >> $GITHUB_ENV - # GhosttyKit is the framework that is built from Zig for our native # Mac app to access. Build this in release mode. - name: Build GhosttyKit @@ -543,19 +616,31 @@ jobs: source-dir: blob destination-dir: ./ + - name: Echo Release URLs + run: | + echo "Release URLs:" + echo " App Bundle: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-slow.zip" + echo " Debug Symbols: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-slow-dsym.zip" + build-macos-debug-fast: + needs: [setup] if: | - ${{ + needs.setup.outputs.should_skip != 'true' && + ( github.event_name == 'workflow_dispatch' || ( github.event.workflow_run.conclusion == 'success' && github.repository_owner == 'ghostty-org' && github.ref_name == 'main' ) - }} + ) runs-on: namespace-profile-ghostty-macos-tahoe timeout-minutes: 90 + env: + GHOSTTY_BUILD: ${{ needs.setup.outputs.build }} + GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }} + GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} steps: - name: Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -589,13 +674,6 @@ jobs: unzip sparkle.zip echo "$(pwd)/bin" >> $GITHUB_PATH - # Load Build Number - - name: Build Number - run: | - echo "GHOSTTY_BUILD=$(git rev-list --count head)" >> $GITHUB_ENV - echo "GHOSTTY_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - echo "GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD)" >> $GITHUB_ENV - # GhosttyKit is the framework that is built from Zig for our native # Mac app to access. Build this in release mode. - name: Build GhosttyKit @@ -722,3 +800,9 @@ jobs: r2-bucket: ghostty-tip source-dir: blob destination-dir: ./ + + - name: Echo Release URLs + run: | + echo "Release URLs:" + echo " App Bundle: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-fast.zip" + echo " Debug Symbols: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-fast-dsym.zip" diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml new file mode 100644 index 000000000..dbb13ae95 --- /dev/null +++ b/.github/workflows/snap.yml @@ -0,0 +1,59 @@ +on: + workflow_dispatch: + inputs: + source-run-id: + description: run id of the workflow that generated the artifact + required: true + type: string + source-artifact-id: + description: source tarball built during build-dist + required: true + type: string + +name: Snap + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: + [namespace-profile-ghostty-snap, namespace-profile-ghostty-snap-arm64] + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + env: + ZIG_LOCAL_CACHE_DIR: /zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /zig/global-cache + steps: + - name: Download Source Tarball Artifacts + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + with: + run-id: ${{ inputs.source-run-id }} + artifact-ids: ${{ inputs.source-artifact-id }} + github-token: ${{ github.token }} + + - name: Extract tarball + run: | + mkdir dist + tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz + + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + with: + path: | + /nix + /zig + + - run: sudo apt install -y udev + + - run: sudo systemctl start systemd-udevd + + # Workaround until this is fixed: https://github.com/canonical/lxd-pkg-snap/pull/789 + - run: | + _LXD_SNAP_DEVCGROUP_CONFIG="/var/lib/snapd/cgroup/snap.lxd.device" + sudo mkdir -p /var/lib/snapd/cgroup + echo 'self-managed=true' | sudo tee "${_LXD_SNAP_DEVCGROUP_CONFIG}" + + - uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0 + with: + path: dist diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a2b2a84aa..dfd81b522 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,6 @@ jobs: - build-linux - build-linux-libghostty - build-nix - - build-snap - build-macos - build-macos-matrix - build-windows @@ -234,6 +233,8 @@ jobs: build-dist: runs-on: namespace-profile-ghostty-md needs: test + outputs: + artifact-id: ${{ steps.upload-artifact.outputs.artifact-id }} env: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache @@ -264,12 +265,30 @@ jobs: cp zig-out/dist/*.tar.gz ghostty-source.tar.gz - name: Upload artifact + id: upload-artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: source-tarball path: |- ghostty-source.tar.gz + trigger-snap: + runs-on: namespace-profile-ghostty-xsm + needs: build-dist + steps: + - name: Checkout code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Trigger Snap workflow + run: | + gh workflow run \ + snap.yml \ + --ref ${{ github.ref_name || 'main' }} \ + --field source-run-id=${{ github.run_id }} \ + --field source-artifact-id=${{ needs.build-dist.outputs.artifact-id }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + build-macos: runs-on: namespace-profile-ghostty-macos-tahoe needs: test @@ -355,44 +374,6 @@ jobs: nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=coretext_harfbuzz nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=coretext_noshape - build-snap: - strategy: - fail-fast: false - matrix: - os: - [namespace-profile-ghostty-snap, namespace-profile-ghostty-snap-arm64] - runs-on: ${{ matrix.os }} - timeout-minutes: 45 - needs: [test, build-dist] - env: - ZIG_LOCAL_CACHE_DIR: /zig/local-cache - ZIG_GLOBAL_CACHE_DIR: /zig/global-cache - steps: - - name: Download Source Tarball Artifacts - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 - with: - name: source-tarball - - name: Extract tarball - run: | - mkdir dist - 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 - with: - path: | - /nix - /zig - - run: sudo apt install -y udev - - run: sudo systemctl start systemd-udevd - # Workaround until this is fixed: https://github.com/canonical/lxd-pkg-snap/pull/789 - - run: | - _LXD_SNAP_DEVCGROUP_CONFIG="/var/lib/snapd/cgroup/snap.lxd.device" - sudo mkdir -p /var/lib/snapd/cgroup - echo 'self-managed=true' | sudo tee "${_LXD_SNAP_DEVCGROUP_CONFIG}" - - uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0 - with: - path: dist - build-windows: runs-on: windows-2022 # this will not stop other jobs from running diff --git a/build.zig.zon b/build.zig.zon index 4b2ef813a..b850ed524 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -111,8 +111,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz", - .hash = "N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b314fc540434cc037c2811fc048d32854b5b78c3.tar.gz", + .hash = "N-V-__8AAGupuwFrRxb2dkqFqmEChLEa4J3e95GReqvomV1b", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index 0bdbb9a41..cf024956f 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -49,10 +49,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME": { + "N-V-__8AAGupuwFrRxb2dkqFqmEChLEa4J3e95GReqvomV1b": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz", - "hash": "sha256-NlUXcBOmaA8W+7RXuXcn9TIhm964dXO2Op4QCQxhDyc=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b314fc540434cc037c2811fc048d32854b5b78c3.tar.gz", + "hash": "sha256-3vPlDDjv6BCLyro1YytzPtF0FfBH20skYuA9laDWhac=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "name": "jetbrains_mono", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index c9d11db4b..7bd663818 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -163,11 +163,11 @@ in }; } { - name = "N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME"; + name = "N-V-__8AAGupuwFrRxb2dkqFqmEChLEa4J3e95GReqvomV1b"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz"; - hash = "sha256-NlUXcBOmaA8W+7RXuXcn9TIhm964dXO2Op4QCQxhDyc="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b314fc540434cc037c2811fc048d32854b5b78c3.tar.gz"; + hash = "sha256-3vPlDDjv6BCLyro1YytzPtF0FfBH20skYuA9laDWhac="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 2315f08d6..531986a34 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -28,7 +28,7 @@ https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21a https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/jacobsandlund/uucode/archive/69782fbe79e06a34ee177978d3479ed5801ce0af.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.15.1-2025-09-04-48-1/ghostty-gobject-0.15.1-2025-09-04-48-1.tar.zst -https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz +https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b314fc540434cc037c2811fc048d32854b5b78c3.tar.gz https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz diff --git a/flake.nix b/flake.nix index dd97744b6..23b07f734 100644 --- a/flake.nix +++ b/flake.nix @@ -30,7 +30,6 @@ # we are using for "normal" builds. # # nixpkgs.follows = "nixpkgs"; - flake-utils.follows = "flake-utils"; }; }; }; diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index b71c2af3a..9ff118bad 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -61,9 +61,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz", - "dest": "vendor/p/N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME", - "sha256": "3655177013a6680f16fbb457b97727f532219bdeb87573b63a9e10090c610f27" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b314fc540434cc037c2811fc048d32854b5b78c3.tar.gz", + "dest": "vendor/p/N-V-__8AAGupuwFrRxb2dkqFqmEChLEa4J3e95GReqvomV1b", + "sha256": "def3e50c38efe8108bcaba35632b733ed17415f047db4b2462e03d95a0d685a7" }, { "type": "archive", diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 6b3b9252b..229b6e100 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -391,6 +391,24 @@ pub fn print(self: *Terminal, c: u21) !void { const cell = self.screen.cursorCellLeft(prev.left - 1); cell.wide = .narrow; + // Back track the cursor so that we don't end up with + // an extra space after the character. Since xterm is + // not VS aware, it cannot be used as a reference for + // this behavior; but it does follow the principle of + // least surprise, and also matches the behavior that + // can be observed in Kitty, which is one of the only + // other VS aware terminals. + if (self.screen.cursor.x == right_limit - 1) { + // If we're already at the right edge, we stay + // here and set the pending wrap to false since + // when we pend a wrap, we only move our cursor once + // even for wide chars (tests verify). + self.screen.cursor.pending_wrap = false; + } else { + // Otherwise, move back. + self.screen.cursorLeft(1); + } + break :narrow; }, @@ -3348,13 +3366,57 @@ test "Terminal: VS15 to make narrow character" { // Enable grapheme clustering t.modes.set(.grapheme_cluster, true); - try t.print(0x26C8); // Thunder cloud and rain + try t.print(0x2614); // Umbrella with rain drops, width=2 + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + t.clearDirty(); + + // We should have 2 cells taken up. It is one character but "wide". + try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); + try testing.expectEqual(@as(usize, 2), t.screen.cursor.x); + + try t.print(0xFE0E); // VS15 to make narrow + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + t.clearDirty(); + + // VS15 should send us back a cell since our char is no longer wide. + try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); + try testing.expectEqual(@as(usize, 1), t.screen.cursor.x); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("☔︎", str); + } + + { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0x2614), cell.content.codepoint); + try testing.expect(cell.hasGrapheme()); + try testing.expectEqual(Cell.Wide.narrow, cell.wide); + const cps = list_cell.node.data.lookupGrapheme(cell).?; + try testing.expectEqual(@as(usize, 1), cps.len); + } +} + +test "Terminal: VS15 on already narrow emoji" { + var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); + defer t.deinit(testing.allocator); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + try t.print(0x26C8); // Thunder cloud and rain, width=1 try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); t.clearDirty(); try t.print(0xFE0E); // VS15 to make narrow try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); t.clearDirty(); + // Character takes up one cell + try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); + try testing.expectEqual(@as(usize, 1), t.screen.cursor.x); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -3372,6 +3434,48 @@ test "Terminal: VS15 to make narrow character" { } } +test "Terminal: VS15 to make narrow character with pending wrap" { + var t = try init(testing.allocator, .{ .rows = 5, .cols = 2 }); + defer t.deinit(testing.allocator); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + try t.print(0x2614); // Umbrella with rain drops, width=2 + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + t.clearDirty(); + + // We only move one because we're in a pending wrap state. + try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); + try testing.expectEqual(@as(usize, 1), t.screen.cursor.x); + try testing.expect(t.screen.cursor.pending_wrap); + + try t.print(0xFE0E); // VS15 to make narrow + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + t.clearDirty(); + + // VS15 should clear the pending wrap state + try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); + try testing.expectEqual(@as(usize, 1), t.screen.cursor.x); + try testing.expect(!t.screen.cursor.pending_wrap); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("☔︎", str); + } + + { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0x2614), cell.content.codepoint); + try testing.expect(cell.hasGrapheme()); + try testing.expectEqual(Cell.Wide.narrow, cell.wide); + const cps = list_cell.node.data.lookupGrapheme(cell).?; + try testing.expectEqual(@as(usize, 1), cps.len); + } +} + test "Terminal: VS16 to make wide character with mode 2027" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator);