diff --git a/.agents/commands/gh-issue b/.agents/commands/gh-issue deleted file mode 100755 index de2f3733509..00000000000 --- a/.agents/commands/gh-issue +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env nu - -# A command to generate an agent prompt to diagnose and formulate -# a plan for resolving a GitHub issue. -# -# IMPORTANT: This command is prompted to NOT write any code and to ONLY -# produce a plan. You should still be vigilant when running this but that -# is the expected behavior. -# -# The `` parameter can be either an issue number or a full GitHub -# issue URL. -def main [ - issue: any, # Ghostty issue number or URL - --repo: string = "ghostty-org/ghostty" # GitHub repository in the format "owner/repo" -] { - # TODO: This whole script doesn't handle errors very well. I actually - # don't know Nu well enough to know the proper way to handle it all. - - let issueData = gh issue view $issue --json author,title,number,body,comments | from json - let comments = $issueData.comments | each { |comment| - $" -### Comment by ($comment.author.login) -($comment.body) -" | str trim - } | str join "\n\n" - - $" -Deep-dive on this GitHub issue. Find the problem and generate a plan. -Do not write code. Explain the problem clearly and propose a comprehensive plan -to solve it. - -# ($issueData.title) \(($issueData.number)\) - -## Description -($issueData.body) - -## Comments -($comments) - -## Your Tasks - -You are an experienced software developer tasked with diagnosing issues. - -1. Review the issue context and details. -2. Examine the relevant parts of the codebase. Analyze the code thoroughly - until you have a solid understanding of how it works. -3. Explain the issue in detail, including the problem and its root cause. -4. Create a comprehensive plan to solve the issue. The plan should include: - - Required code changes - - Potential impacts on other parts of the system - - Necessary tests to be written or updated - - Documentation updates - - Performance considerations - - Security implications - - Backwards compatibility \(if applicable\) - - Include the reference link to the source issue and any related discussions -4. Think deeply about all aspects of the task. Consider edge cases, potential - challenges, and best practices for addressing the issue. Review the plan - with the oracle and adjust it based on its feedback. - -**ONLY CREATE A PLAN. DO NOT WRITE ANY CODE.** Your task is to create -a thorough, comprehensive strategy for understanding and resolving the issue. -" | str trim -} diff --git a/.agents/skills/writing-commit-messages/SKILL.md b/.agents/skills/writing-commit-messages/SKILL.md new file mode 100644 index 00000000000..dedadbe5e87 --- /dev/null +++ b/.agents/skills/writing-commit-messages/SKILL.md @@ -0,0 +1,62 @@ +--- +name: writing-commit-messages +description: >- + Writes Git commit messages. Activates when the user asks to write + a commit message, draft a commit message, or similar. +--- + +# Writing Commit Messages + +Write commit messages that follow commit style guidelines for the project. + +## Format + +``` +: + + + + +``` + +## Rules + +### Subject line + +- **Subsystem prefix**: Use a short, lowercase identifier for the + area of code changed (e.g., `terminal`, `vt`, `lib`, `config`, + `font`). Determine this from the file paths in the diff. If + changes span the macOS app, use `macos`. For GTK, use `gtk`. For + build system, use `build`. Use nested subsystems with `/` when + helpful and exclusive (e.g., `terminal/osc`). +- **Summary**: Lowercase start (not capitalized), imperative mood, + no trailing period. Keep it concise—ideally under 60 characters + total for the whole subject line. + +### References + +- If the change relates to a GitHub issue, PR, or discussion, list + the relevant numbers on their own lines after the subject, separated + by a blank line. E.g. `#1234` +- If there are no references, omit this section entirely (no blank + line). + +### Long form description + +- Describe **what changed**, **what the previous behavior was**, + and **how the new behavior works** at a high level. +- Use plain prose, not bullet points. Wrap lines at ~72 characters. +- Focus on the _why_ and _how_ rather than restating the diff. +- Keep the tone direct and technical without no filler phrases. +- Don't exceed a handful of paragraphs; less is more. + +## Workflow + +- If `.jj` is present, use `jj` instead of `git` for all commands. +- Run a diff to see what changes are present since the last commit. +- Identify the subsystem from the changed file paths. +- Identify any referenced issues/PRs from the diff context or + branch name. +- Draft the commit message following the format above. +- Apply the commit +- Don't push the commit; leave that to the user. diff --git a/.github/VOUCHED.td b/.github/VOUCHED.td index cbbbd40ae86..c9d1629d739 100644 --- a/.github/VOUCHED.td +++ b/.github/VOUCHED.td @@ -18,30 +18,159 @@ # Maintainers can vouch for new contributors by commenting "!vouch" on a # discussion by the author. Maintainers can denounce users by commenting # "!denounce" or "!denounce [username]" on a discussion. +00-kat +04cb +aalhendi +abdurrahmanski +abudvytis +adrum +aindriu80 +alaasdk +alanmoyano +alexfeijoo44 +alexjuca +amadeus +andrejdaskalov +anthonyzhoon +atomk +balazs-szucs bennettp123 +benodiwal bernsno +beryesa +bitigchi bkircher +bo2themax +brentschroeter +cadebrown +cespare +charliie-dev +chernetskyi +chronologos +cmwetherell +craziestowl +curtismoncoq +d-dudas daiimus +damyanbogoev +danulqua +dariogriffo +davidsanchez222 +dervedro +devsunb +diaaeddin +dmehala doprz +douglance +douglas +drepper +dzhlobo elias8 +ephemera eriksremess +faukah filip7 +flou +francescarpi +gagbo +ghokun +gmile +gordonbondon +gpanders +guilhermetk hakonhagland +halosatrio hqnna +hulet +icodesign +j0hnm4r5 +jacobsandlund jake-stewart jcollie +jesusvazquez +jguthmiller +jmcgover +johnslavik +josephmart +jparise juniqlim +kawarimidoll +kenvandine +khipp +kirwiisp +kjvdven +kloneets +koranir +kristina8888 +kristofersoler +laxystem +liby +linustalacko +lonsagisawa +mac0ne mahnokropotkinvich +marijagjorgjieva +markdorison +markhuot marrocco-simone +matkotiric +michielvk +miguelelgallo +mihi314 mikailmm +misairuzame +mischief mitchellh +miupa +molechowski +mrmage +mtak +natesmyth +neo773 +nicosuave +nmggithub +noib3 +nwehg +ocean6954 +oshdubh +pan93412 +pangoraw +pauley-unsaturated peilingjiang peterdavehello +phush0 +piedrahitac pluiedev pouwerkerk +poweruser64 +prakhar54-byte priyans-hu -prsweet +puzza007 qwerasd205 +reo101 +rgehan +rhodes-b +rmengelbrecht rmunn +rockorager +rpfaeffle +secrus +seruman +silveirapf +slsrepo +sunshine-syz +tdslot +ticclick +tnagatomi +trag1c +tristan957 tweedbeetle +uhojin +uzaaft +vaughanandrews +vlsi +wyounas yamshta +ydah +zenyr +zeshi09 diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 7c4256e0e86..d64ab829ac8 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -30,7 +30,7 @@ jobs: runs-on: ${{ matrix.variant.runner }} steps: - name: Download Source Tarball Artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: run-id: ${{ inputs.source-run-id }} artifact-ids: ${{ inputs.source-artifact-id }} diff --git a/.github/workflows/milestone.yml b/.github/workflows/milestone.yml index 49bba4e6ba6..05d1f83c86b 100644 --- a/.github/workflows/milestone.yml +++ b/.github/workflows/milestone.yml @@ -16,11 +16,10 @@ jobs: steps: - name: Set Milestone for PR uses: hustcer/milestone-action@ebed8d5daafd855a600d7e665c1b130f06d24130 # v3.1 - if: github.event.pull_request.merged == true + if: github.event.pull_request.merged == true && !contains(github.event.pull_request.title, 'VOUCHED') && !startsWith(github.event.pull_request.title, 'ci:') with: action: bind-pr # `bind-pr` is the default action - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} # Bind milestone to closed issue that has a merged PR fix - name: Set Milestone for Issue @@ -28,5 +27,4 @@ jobs: if: github.event.issue.state == 'closed' with: action: bind-issue - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index f12ba221149..c6535030c3b 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -41,13 +41,13 @@ jobs: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig - name: Setup Nix - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index a24e5a38968..a742b4d4b0e 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -83,13 +83,13 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable @@ -113,7 +113,7 @@ jobs: nix develop -c minisign -S -m "ghostty-source.tar.gz" -s minisign.key < minisign.password - name: Upload artifact - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: source-tarball path: |- @@ -130,10 +130,20 @@ jobs: GHOSTTY_VERSION: ${{ needs.setup.outputs.version }} GHOSTTY_BUILD: ${{ needs.setup.outputs.build }} GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }} + ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 + with: + cache: | + xcode + path: | + /Users/runner/zig + - uses: DeterminateSystems/nix-installer-action@main with: determinate: true @@ -150,7 +160,7 @@ jobs: - name: Setup Sparkle env: - SPARKLE_VERSION: 2.7.3 + SPARKLE_VERSION: 2.9.0 run: | mkdir -p .action/sparkle cd .action/sparkle @@ -174,7 +184,9 @@ jobs: - name: Build Ghostty.app run: | cd macos - xcodebuild -target Ghostty -configuration Release + xcodebuild -target Ghostty -configuration Release \ + COMPILATION_CACHE_CAS_PATH=/Users/runner/Library/Developer/Xcode/DerivedData/CompilationCache.noindex \ + COMPILATION_CACHE_KEEP_CAS_DIRECTORY=YES # Add all our metadata to Info.plist so we can reference it later. - name: Update Info.plist @@ -219,6 +231,7 @@ jobs: /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" + /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/PlugIns/DockTilePlugin.plugin" # Codesign the app bundle /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime --entitlements "macos/Ghostty.entitlements" macos/build/Release/Ghostty.app @@ -269,7 +282,7 @@ jobs: zip -9 -r --symlinks ../../../ghostty-macos-universal-dsym.zip Ghostty.app.dSYM/ - name: Upload artifact - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: macos path: |- @@ -286,7 +299,7 @@ jobs: curl -sL https://sentry.io/get-cli/ | bash - name: Download macOS Artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: macos @@ -309,13 +322,13 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Download macOS Artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: macos - name: Setup Sparkle env: - SPARKLE_VERSION: 2.7.3 + SPARKLE_VERSION: 2.9.0 run: | mkdir -p .action/sparkle cd .action/sparkle @@ -340,7 +353,7 @@ jobs: mv appcast_new.xml appcast.xml - name: Upload artifact - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: sparkle path: |- @@ -357,17 +370,17 @@ jobs: GHOSTTY_VERSION: ${{ needs.setup.outputs.version }} steps: - name: Download macOS Artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: macos - name: Download Sparkle Artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: sparkle - name: Download Source Tarball Artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: source-tarball diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 2227ae09c42..326b29439b1 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -37,7 +37,12 @@ jobs: with: # Important so that build number generation works fetch-depth: 0 - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 + with: + path: | + /nix + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -165,12 +170,12 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -219,6 +224,8 @@ jobs: GHOSTTY_BUILD: ${{ needs.setup.outputs.build }} GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }} GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} + ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -226,6 +233,14 @@ jobs: # Important so that build number generation works fetch-depth: 0 + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 + with: + cache: | + xcode + path: | + /Users/runner/zig + # TODO(tahoe): https://github.com/NixOS/nix/issues/13342 - uses: DeterminateSystems/nix-installer-action@main with: @@ -244,7 +259,7 @@ jobs: # Setup Sparkle - name: Setup Sparkle env: - SPARKLE_VERSION: 2.7.3 + SPARKLE_VERSION: 2.9.0 run: | mkdir -p .action/sparkle cd .action/sparkle @@ -263,7 +278,9 @@ jobs: - name: Build Ghostty.app run: | cd macos - xcodebuild -target Ghostty -configuration Release + xcodebuild -target Ghostty -configuration Release \ + COMPILATION_CACHE_CAS_PATH=/Users/runner/Library/Developer/Xcode/DerivedData/CompilationCache.noindex \ + COMPILATION_CACHE_KEEP_CAS_DIRECTORY=YES # 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. @@ -309,6 +326,7 @@ jobs: /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" + /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/PlugIns/DockTilePlugin.plugin" # Codesign the app bundle /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime --entitlements "macos/Ghostty.entitlements" macos/build/Release/Ghostty.app @@ -438,7 +456,7 @@ jobs: EOF - name: Upload Release URLs - uses: actions/upload-artifact@47309c993abb98030a35d55ef7ff34b7fa1074b5 # v6.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v6.0 with: name: release-urls-${{ inputs.pr || '0' }} path: release-urls.txt @@ -462,6 +480,8 @@ jobs: GHOSTTY_BUILD: ${{ needs.setup.outputs.build }} GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }} GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} + ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -469,6 +489,14 @@ jobs: # Important so that build number generation works fetch-depth: 0 + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 + with: + cache: | + xcode + path: | + /Users/runner/zig + # TODO(tahoe): https://github.com/NixOS/nix/issues/13342 - uses: DeterminateSystems/nix-installer-action@main with: @@ -487,7 +515,7 @@ jobs: # Setup Sparkle - name: Setup Sparkle env: - SPARKLE_VERSION: 2.7.3 + SPARKLE_VERSION: 2.9.0 run: | mkdir -p .action/sparkle cd .action/sparkle @@ -506,7 +534,9 @@ jobs: - name: Build Ghostty.app run: | cd macos - xcodebuild -target Ghostty -configuration Release + xcodebuild -target Ghostty -configuration Release \ + COMPILATION_CACHE_CAS_PATH=/Users/runner/Library/Developer/Xcode/DerivedData/CompilationCache.noindex \ + COMPILATION_CACHE_KEEP_CAS_DIRECTORY=YES # 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. @@ -552,6 +582,7 @@ jobs: /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" + /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/PlugIns/DockTilePlugin.plugin" # Codesign the app bundle /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime --entitlements "macos/Ghostty.entitlements" macos/build/Release/Ghostty.app @@ -646,6 +677,8 @@ jobs: GHOSTTY_BUILD: ${{ needs.setup.outputs.build }} GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }} GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} + ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -653,6 +686,14 @@ jobs: # Important so that build number generation works fetch-depth: 0 + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 + with: + cache: | + xcode + path: | + /Users/runner/zig + # TODO(tahoe): https://github.com/NixOS/nix/issues/13342 - uses: DeterminateSystems/nix-installer-action@main with: @@ -671,7 +712,7 @@ jobs: # Setup Sparkle - name: Setup Sparkle env: - SPARKLE_VERSION: 2.7.3 + SPARKLE_VERSION: 2.9.0 run: | mkdir -p .action/sparkle cd .action/sparkle @@ -690,7 +731,9 @@ jobs: - name: Build Ghostty.app run: | cd macos - xcodebuild -target Ghostty -configuration Release + xcodebuild -target Ghostty -configuration Release \ + COMPILATION_CACHE_CAS_PATH=/Users/runner/Library/Developer/Xcode/DerivedData/CompilationCache.noindex \ + COMPILATION_CACHE_KEEP_CAS_DIRECTORY=YES # 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. @@ -736,6 +779,7 @@ jobs: /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" + /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/PlugIns/DockTilePlugin.plugin" # Codesign the app bundle /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime --entitlements "macos/Ghostty.entitlements" macos/build/Release/Ghostty.app diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index 68ec9cacd28..67f291601d0 100644 --- a/.github/workflows/snap.yml +++ b/.github/workflows/snap.yml @@ -26,7 +26,7 @@ jobs: ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - name: Download Source Tarball Artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: run-id: ${{ inputs.source-run-id }} artifact-ids: ${{ inputs.source-artifact-id }} @@ -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@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 520fba403d5..2762427ac8b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,15 +11,85 @@ concurrency: cancel-in-progress: true jobs: + # Determines whether other jobs should be skipped. Modify this if there + # are other fast skip conditions, and add it as an output. Then modify + # other tests `needs/if` to check them. Document the outputs. + skip: + if: github.repository == 'ghostty-org/ghostty' + runs-on: namespace-profile-ghostty-xsm + outputs: + # 'true' when all changed files are non-code (e.g. only VOUCHED.td), + # signaling that all other jobs can be skipped entirely. + skip: ${{ steps.determine.outputs.skip }} + # Path-based filters to gate specific linter/formatter jobs. + actions_pins: ${{ steps.filter_any.outputs.actions_pins }} + blueprints: ${{ steps.filter_any.outputs.blueprints }} + macos: ${{ steps.filter_any.outputs.macos }} + nix: ${{ steps.filter_any.outputs.nix }} + shell: ${{ steps.filter_any.outputs.shell }} + zig: ${{ steps.filter_any.outputs.zig }} + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: dorny/paths-filter@9d7afb8d214ad99e78fbd4247752c4caed2b6e4c # v4.0.0 + id: filter_every + with: + token: ${{ secrets.GITHUB_TOKEN }} + predicate-quantifier: "every" + filters: | + code: + - '**' + - '!.github/VOUCHED.td' + - uses: dorny/paths-filter@9d7afb8d214ad99e78fbd4247752c4caed2b6e4c # v4.0.0 + id: filter_any + with: + token: ${{ secrets.GITHUB_TOKEN }} + filters: | + macos: + - '.swiftlint.yml' + - 'macos/**' + actions_pins: + - '.github/workflows/**' + - '.github/pinact.yml' + shell: + - '**/*.sh' + - '**/*.bash' + nix: + - 'nix/**' + - '*.nix' + - 'flake.nix' + - 'flake.lock' + - 'default.nix' + - 'shell.nix' + zig: + - '**/*.zig' + - 'build.zig*' + blueprints: + - 'src/apprt/gtk/**/*.blp' + - 'nix/build-support/check-blueprints.sh' + + - id: determine + name: Determine skip + run: | + if [ "${{ steps.filter_every.outputs.code }}" = "false" ]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + required: name: "Required Checks: Test" + if: always() runs-on: namespace-profile-ghostty-xsm needs: + - skip - build-bench - build-dist - build-examples - build-flatpak - build-libghostty-vt + - build-libghostty-vt-android + - build-libghostty-vt-macos - build-linux - build-linux-libghostty - build-nix @@ -32,9 +102,11 @@ jobs: - test-gtk - test-sentry-linux - test-i18n + - test-fuzz-libghostty - test-macos - pinact - prettier + - swiftlint - alejandra - typos - shellcheck @@ -77,14 +149,14 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -120,14 +192,14 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -153,14 +225,14 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -187,14 +259,14 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -231,14 +303,14 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -252,6 +324,93 @@ jobs: -Dtarget=${{ matrix.target }} \ -Dsimd=false + # lib-vt requires macOS runner for macOS/iOS builds becauase it requires the `apple_sdk` path + build-libghostty-vt-macos: + strategy: + matrix: + target: [aarch64-macos, x86_64-macos, aarch64-ios] + runs-on: namespace-profile-ghostty-macos-tahoe + needs: test + env: + ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 + with: + cache: | + xcode + path: | + /Users/runner/zig + + # TODO(tahoe): https://github.com/NixOS/nix/issues/13342 + - uses: DeterminateSystems/nix-installer-action@main + with: + determinate: true + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Xcode Select + run: sudo xcode-select -s /Applications/Xcode_26.2.app + + - name: Build + run: | + nix develop -c zig build lib-vt \ + -Dtarget=${{ matrix.target }} + + # lib-vt requires the Android NDK for Android builds + build-libghostty-vt-android: + strategy: + matrix: + target: + [aarch64-linux-android, x86_64-linux-android, arm-linux-androideabi] + runs-on: namespace-profile-ghostty-sm + needs: test + env: + ZIG_LOCAL_CACHE_DIR: /zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /zig/global-cache + ANDROID_NDK_VERSION: r29 + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 + with: + path: | + /nix + /zig + + # Install Nix and use that to run our tests so our environment matches exactly. + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Setup Android NDK + uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0 + id: setup-ndk + with: + ndk-version: r29 + add-to-path: false + link-to-sdk: false + local-cache: true + + - name: Build + run: | + nix develop -c zig build lib-vt \ + -Dtarget=${{ matrix.target }} + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + build-linux: strategy: fail-fast: false @@ -267,14 +426,14 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -296,14 +455,14 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -329,14 +488,14 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -375,14 +534,14 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -398,7 +557,7 @@ jobs: - name: Upload artifact id: upload-artifact - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: source-tarball path: |- @@ -443,10 +602,21 @@ jobs: build-macos: runs-on: namespace-profile-ghostty-macos-tahoe needs: test + env: + ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 + with: + cache: | + xcode + path: | + /Users/runner/zig + # TODO(tahoe): https://github.com/NixOS/nix/issues/13342 - uses: DeterminateSystems/nix-installer-action@main with: @@ -475,21 +645,38 @@ jobs: # codesigning. IMPORTANT: this must NOT run in a Nix environment. # Nix breaks xcodebuild so this has to be run outside. - name: Build Ghostty.app - run: cd macos && xcodebuild -target Ghostty + run: | + cd macos + xcodebuild -target Ghostty \ + COMPILATION_CACHE_CAS_PATH=/Users/runner/Library/Developer/Xcode/DerivedData/CompilationCache.noindex \ + COMPILATION_CACHE_KEEP_CAS_DIRECTORY=YES # Build the iOS target without code signing just to verify it works. - name: Build Ghostty iOS run: | cd macos - xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO" + xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO" \ + COMPILATION_CACHE_CAS_PATH=/Users/runner/Library/Developer/Xcode/DerivedData/CompilationCache.noindex \ + COMPILATION_CACHE_KEEP_CAS_DIRECTORY=YES build-macos-freetype: runs-on: namespace-profile-ghostty-macos-tahoe needs: test + env: + ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 + with: + cache: | + xcode + path: | + /Users/runner/zig + # TODO(tahoe): https://github.com/NixOS/nix/issues/13342 - uses: DeterminateSystems/nix-installer-action@main with: @@ -509,9 +696,11 @@ jobs: id: deps run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT + # We run tests with an empty test filter so it runs all unit tests + # but skips Xcode tests - name: Test All run: | - nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=coretext_freetype + nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=coretext_freetype -Dtest-filter="" - name: Build All run: | @@ -587,7 +776,8 @@ jobs: run: Get-Content -Path ".\build.log" test: - if: github.repository == 'ghostty-org/ghostty' + if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' + needs: skip runs-on: namespace-profile-ghostty-md outputs: zig_version: ${{ steps.zig.outputs.version }} @@ -604,14 +794,14 @@ jobs: 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@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -646,14 +836,14 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -694,14 +884,14 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -729,14 +919,14 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -751,10 +941,21 @@ jobs: test-macos: runs-on: namespace-profile-ghostty-macos-tahoe needs: test + env: + ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 + with: + cache: | + xcode + path: | + /Users/runner/zig + # TODO(tahoe): https://github.com/NixOS/nix/issues/13342 - uses: DeterminateSystems/nix-installer-action@main with: @@ -782,7 +983,7 @@ jobs: fail-fast: false matrix: i18n: ["true", "false"] - name: Build -Di18n=${{ matrix.simd }} + name: Build -Di18n=${{ matrix.i18n }} runs-on: namespace-profile-ghostty-sm needs: test env: @@ -793,14 +994,14 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -812,8 +1013,54 @@ jobs: run: | nix develop -c zig build -Di18n=${{ matrix.i18n }} + test-fuzz-libghostty: + name: Build test/fuzz-libghostty + runs-on: namespace-profile-ghostty-sm + needs: test + env: + ZIG_LOCAL_CACHE_DIR: /zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /zig/global-cache + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 + with: + path: | + /nix + /zig + + # Install Nix and use that to run our tests so our environment matches exactly. + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Install AFL++ and LLVM + run: | + sudo apt-get update + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y afl++ llvm + + - name: Verify AFL++ and LLVM are available + run: | + afl-cc --version + if command -v llvm-config >/dev/null 2>&1; then + llvm-config --version + else + llvm-config-18 --version + fi + + - name: Build fuzzer harness + run: | + nix develop -c sh -c 'cd test/fuzz-libghostty && zig build' + zig-fmt: - if: github.repository == 'ghostty-org/ghostty' + if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' && needs.skip.outputs.zig == 'true' + needs: skip runs-on: namespace-profile-ghostty-xsm timeout-minutes: 60 env: @@ -822,12 +1069,12 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -841,7 +1088,8 @@ jobs: pinact: name: "GitHub Actions Pins" - if: github.repository == 'ghostty-org/ghostty' + if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' && needs.skip.outputs.actions_pins == 'true' + needs: skip runs-on: namespace-profile-ghostty-xsm timeout-minutes: 60 permissions: @@ -852,12 +1100,12 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -872,7 +1120,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} prettier: - if: github.repository == 'ghostty-org/ghostty' + if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' + needs: skip runs-on: namespace-profile-ghostty-xsm timeout-minutes: 60 env: @@ -881,12 +1130,12 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -898,8 +1147,39 @@ jobs: - name: prettier check run: nix develop -c prettier --check . + swiftlint: + if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' && needs.skip.outputs.macos == 'true' + runs-on: namespace-profile-ghostty-macos-tahoe + needs: skip + timeout-minutes: 60 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 + with: + cache: | + xcode + path: | + /Users/runner/zig + + # TODO(tahoe): https://github.com/NixOS/nix/issues/13342 + - uses: DeterminateSystems/nix-installer-action@main + with: + determinate: true + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + skipPush: true + useDaemon: false # sometimes fails on short jobs + + - name: swiftlint check + run: nix develop -c swiftlint lint --strict + alejandra: - if: github.repository == 'ghostty-org/ghostty' + if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' && needs.skip.outputs.nix == 'true' + needs: skip runs-on: namespace-profile-ghostty-xsm timeout-minutes: 60 env: @@ -908,12 +1188,12 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -926,7 +1206,8 @@ jobs: run: nix develop -c alejandra --check . typos: - if: github.repository == 'ghostty-org/ghostty' + if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' + needs: skip runs-on: namespace-profile-ghostty-xsm timeout-minutes: 60 env: @@ -935,12 +1216,12 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -953,7 +1234,8 @@ jobs: run: nix develop -c typos shellcheck: - if: github.repository == 'ghostty-org/ghostty' + if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' && needs.skip.outputs.shell == 'true' + needs: skip runs-on: namespace-profile-ghostty-xsm timeout-minutes: 60 env: @@ -962,12 +1244,12 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -985,7 +1267,8 @@ jobs: $(find . \( -name "*.sh" -o -name "*.bash" \) -type f ! -path "./zig-out/*" ! -path "./macos/build/*" ! -path "./.git/*" | sort) translations: - if: github.repository == 'ghostty-org/ghostty' + if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' + needs: skip runs-on: namespace-profile-ghostty-xsm timeout-minutes: 60 env: @@ -994,12 +1277,12 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -1012,7 +1295,8 @@ jobs: run: nix develop -c .github/scripts/check-translations.sh blueprint-compiler: - if: github.repository == 'ghostty-org/ghostty' + if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' && needs.skip.outputs.blueprints == 'true' + needs: skip runs-on: namespace-profile-ghostty-xsm timeout-minutes: 60 env: @@ -1021,12 +1305,12 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -1056,14 +1340,14 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 @@ -1082,13 +1366,13 @@ jobs: needs: [test, build-dist] steps: - name: Install and configure Namespace CLI - uses: namespacelabs/nscloud-setup@d1c625762f7c926a54bd39252efff0705fd11c64 # v0.0.10 + uses: namespacelabs/nscloud-setup@f378676225212387f1283f4da878712af2c4cd60 # v0.0.11 - name: Configure Namespace powered Buildx uses: namespacelabs/nscloud-setup-buildx-action@f5814dcf37a16cce0624d5bec2ab879654294aa0 # v0.0.22 - name: Download Source Tarball Artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: source-tarball @@ -1098,7 +1382,7 @@ jobs: tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz - name: Build and push - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: dist file: dist/src/build/docker/debian/Dockerfile @@ -1118,14 +1402,14 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + - uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index 026b1e9df2a..4c159a81514 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@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix /zig - name: Setup Nix - uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1 + uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 diff --git a/.github/workflows/vouch-check-issue.yml b/.github/workflows/vouch-check-issue.yml index e0933aaf101..3fa3bb542f7 100644 --- a/.github/workflows/vouch-check-issue.yml +++ b/.github/workflows/vouch-check-issue.yml @@ -14,7 +14,7 @@ jobs: app-id: ${{ secrets.VOUCH_APP_ID }} private-key: ${{ secrets.VOUCH_APP_PRIVATE_KEY }} - - uses: mitchellh/vouch/action/check-issue@6803dde571265e13489c3f118200f60b6ab59e4d # v1.3.1 + - uses: mitchellh/vouch/action/check-issue@c6d80ead49839655b61b422700b7a3bc9d0804a9 # v1.4.2 with: issue-number: ${{ github.event.issue.number }} auto-close: true diff --git a/.github/workflows/vouch-check-pr.yml b/.github/workflows/vouch-check-pr.yml index eb5a7e6fb6b..0efb6208c4f 100644 --- a/.github/workflows/vouch-check-pr.yml +++ b/.github/workflows/vouch-check-pr.yml @@ -14,7 +14,7 @@ jobs: app-id: ${{ secrets.VOUCH_APP_ID }} private-key: ${{ secrets.VOUCH_APP_PRIVATE_KEY }} - - uses: mitchellh/vouch/action/check-pr@6803dde571265e13489c3f118200f60b6ab59e4d # v1.3.1 + - uses: mitchellh/vouch/action/check-pr@c6d80ead49839655b61b422700b7a3bc9d0804a9 # v1.4.2 with: pr-number: ${{ github.event.pull_request.number }} auto-close: true diff --git a/.github/workflows/vouch-manage-by-discussion.yml b/.github/workflows/vouch-manage-by-discussion.yml index 50e2a23f3c1..cf7c092e23a 100644 --- a/.github/workflows/vouch-manage-by-discussion.yml +++ b/.github/workflows/vouch-manage-by-discussion.yml @@ -22,7 +22,7 @@ jobs: with: token: ${{ steps.app-token.outputs.token }} - - uses: mitchellh/vouch/action/manage-by-discussion@6803dde571265e13489c3f118200f60b6ab59e4d # v1.3.1 + - uses: mitchellh/vouch/action/manage-by-discussion@c6d80ead49839655b61b422700b7a3bc9d0804a9 # v1.4.2 with: discussion-number: ${{ github.event.discussion.number }} comment-node-id: ${{ github.event.comment.node_id }} diff --git a/.github/workflows/vouch-manage-by-issue.yml b/.github/workflows/vouch-manage-by-issue.yml index f00270a0dda..6f85520bde1 100644 --- a/.github/workflows/vouch-manage-by-issue.yml +++ b/.github/workflows/vouch-manage-by-issue.yml @@ -22,7 +22,7 @@ jobs: with: token: ${{ steps.app-token.outputs.token }} - - uses: mitchellh/vouch/action/manage-by-issue@6803dde571265e13489c3f118200f60b6ab59e4d # v1.3.1 + - uses: mitchellh/vouch/action/manage-by-issue@c6d80ead49839655b61b422700b7a3bc9d0804a9 # v1.4.2 with: repo: ${{ github.repository }} issue-id: ${{ github.event.issue.number }} diff --git a/.github/workflows/vouch-sync-codeowners.yml b/.github/workflows/vouch-sync-codeowners.yml new file mode 100644 index 00000000000..fac06a37257 --- /dev/null +++ b/.github/workflows/vouch-sync-codeowners.yml @@ -0,0 +1,32 @@ +on: + schedule: + - cron: "0 0 * * 1" # Every Monday at midnight UTC + workflow_dispatch: + +name: "Vouch - Sync CODEOWNERS" + +concurrency: + group: vouch-manage + cancel-in-progress: false + +jobs: + sync: + runs-on: namespace-profile-ghostty-xsm + steps: + - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + id: app-token + with: + app-id: ${{ secrets.VOUCH_APP_ID }} + private-key: ${{ secrets.VOUCH_APP_PRIVATE_KEY }} + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + token: ${{ steps.app-token.outputs.token }} + + - uses: mitchellh/vouch/action/sync-codeowners@c6d80ead49839655b61b422700b7a3bc9d0804a9 # v1.4.2 + with: + repo: ${{ github.repository }} + pull-request: "true" + merge-immediately: "true" + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.gitignore b/.gitignore index e521f8851f0..74f3f85ebdc 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ zig-cache/ .zig-cache/ zig-out/ +/build.zig.zon.bak /result* /.nixos-test-history example/*.wasm @@ -24,3 +25,4 @@ glad.zip /ghostty.qcow2 vgcore.* + diff --git a/.prettierignore b/.prettierignore index f131a5edc40..2699f7e1058 100644 --- a/.prettierignore +++ b/.prettierignore @@ -11,6 +11,9 @@ zig-out/ # macos is managed by XCode GUI macos/ +# Xcode asset catalogs +**/*.xcassets/ + # produced by Icon Composer on macOS images/Ghostty.icon/icon.json @@ -19,3 +22,7 @@ website/.next # shaders *.frag + +# fuzz corpus files +test/fuzz-libghostty/corpus/ +test/fuzz-libghostty/afl-out/ diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 00000000000..7f1b56883fe --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,2 @@ +included: macos +child_config: macos/.swiftlint.yml diff --git a/AGENTS.md b/AGENTS.md index 04d3570a786..3298f216085 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,25 +5,23 @@ A file for [guiding coding agents](https://agents.md/). ## Commands - **Build:** `zig build` + - If you're on macOS and don't need to build the macOS app, use + `-Demit-macos-app=false` to skip building the app bundle and speed up + compilation. - **Test (Zig):** `zig build test` + - Prefer to run targeted tests with `-Dtest-filter` because the full + test suite is slow to run. - **Test filter (Zig)**: `zig build test -Dtest-filter=` - **Formatting (Zig)**: `zig fmt .` +- **Formatting (Swift)**: `swiftlint lint --strict --fix` - **Formatting (other)**: `prettier -w .` ## Directory Structure - Shared Zig core: `src/` -- C API: `include` - macOS app: `macos/` - GTK (Linux and FreeBSD) app: `src/apprt/gtk` -## macOS App - -- Do not use `xcodebuild` -- Use `zig build` to build the macOS app and any shared Zig code -- Use `zig build run` to build and run the macOS app -- Run Xcode tests using `zig build test` - ## Issue and PR Guidelines - Never create an issue. diff --git a/CODEOWNERS b/CODEOWNERS index f8efe9beb44..0e8aebe4e75 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -137,6 +137,7 @@ /dist/macos/ @ghostty-org/macos /pkg/apple-sdk/ @ghostty-org/macos /pkg/macos/ @ghostty-org/macos +/.swiftlint.yml @ghostty-org/macos # Renderer /src/renderer.zig @ghostty-org/renderer @@ -163,31 +164,35 @@ # Localization /po/README_TRANSLATORS.md @ghostty-org/localization /po/com.mitchellh.ghostty.pot @ghostty-org/localization -/po/ca_ES.UTF-8.po @ghostty-org/ca_ES -/po/de_DE.UTF-8.po @ghostty-org/de_DE -/po/es_BO.UTF-8.po @ghostty-org/es_BO -/po/es_AR.UTF-8.po @ghostty-org/es_AR -/po/fr_FR.UTF-8.po @ghostty-org/fr_FR -/po/hu_HU.UTF-8.po @ghostty-org/hu_HU -/po/id_ID.UTF-8.po @ghostty-org/id_ID -/po/ja_JP.UTF-8.po @ghostty-org/ja_JP -/po/mk_MK.UTF-8.po @ghostty-org/mk_MK -/po/nb_NO.UTF-8.po @ghostty-org/nb_NO -/po/nl_NL.UTF-8.po @ghostty-org/nl_NL -/po/pl_PL.UTF-8.po @ghostty-org/pl_PL -/po/pt_BR.UTF-8.po @ghostty-org/pt_BR -/po/ru_RU.UTF-8.po @ghostty-org/ru_RU -/po/tr_TR.UTF-8.po @ghostty-org/tr_TR -/po/uk_UA.UTF-8.po @ghostty-org/uk_UA -/po/zh_CN.UTF-8.po @ghostty-org/zh_CN -/po/ga_IE.UTF-8.po @ghostty-org/ga_IE -/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/lv_LV.UTF-8.po @ghostty-org/lv_LV -/po/zh_TW.UTF-8.po @ghostty-org/zh_TW -/po/hr_HR.UTF-8.po @ghostty-org/hr_HR +/po/bg.po @ghostty-org/bg_BG +/po/ca.po @ghostty-org/ca_ES +/po/de.po @ghostty-org/de_DE +/po/es_AR.po @ghostty-org/es_AR +/po/es_BO.po @ghostty-org/es_BO +/po/es_ES.po @ghostty-org/es_ES +/po/fr.po @ghostty-org/fr_FR +/po/ga.po @ghostty-org/ga_IE +/po/he.po @ghostty-org/he_IL +/po/hr.po @ghostty-org/hr_HR +/po/hu.po @ghostty-org/hu_HU +/po/id.po @ghostty-org/id_ID +/po/it.po @ghostty-org/it_IT +/po/ja.po @ghostty-org/ja_JP +/po/kk.po @ghostty-org/kk_KZ +/po/ko_KR.po @ghostty-org/ko_KR +/po/lt.po @ghostty-org/lt_LT +/po/lv.po @ghostty-org/lv_LV +/po/mk.po @ghostty-org/mk_MK +/po/nb.po @ghostty-org/nb_NO +/po/nl.po @ghostty-org/nl_NL +/po/pl.po @ghostty-org/pl_PL +/po/pt_BR.po @ghostty-org/pt_BR +/po/ru.po @ghostty-org/ru_RU +/po/tr.po @ghostty-org/tr_TR +/po/uk.po @ghostty-org/uk_UA +/po/vi.po @ghostty-org/vi_VN +/po/zh_CN.po @ghostty-org/zh_CN +/po/zh_TW.po @ghostty-org/zh_TW # Packaging - Snap /snap/ @ghostty-org/snap diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9633029c5cb..b6be800c078 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,6 +47,13 @@ on a system of trust, and AI has unfortunately made it so we can no longer trust-by-default because it makes it too trivial to generate plausible-looking but actually low-quality contributions. +## Contributors Prior to the Vouch System + +If you contributed to Ghostty prior to the introduction +of the vouch system and wish to continue contributing, you were not +automatically added to the [list of vouched users](.github/VOUCHED.td). You will need to follow the same +process as a first-time contributor to be vouched. + ## Denouncement System If you repeatedly break the rules of this document or repeatedly diff --git a/HACKING.md b/HACKING.md index 921ed71ff4d..7ba58488116 100644 --- a/HACKING.md +++ b/HACKING.md @@ -186,6 +186,31 @@ shellcheck \ $(find . \( -name "*.sh" -o -name "*.bash" \) -type f ! -path "./zig-out/*" ! -path "./macos/build/*" ! -path "./.git/*" | sort) ``` +### SwiftLint + +Swift code is linted using [SwiftLint](https://github.com/realm/SwiftLint). A +SwiftLint CI check will fail builds with improper formatting. Therefore, if you +are modifying Swift code, you may want to install it locally and run this from +the repo root before you commit: + +``` +swiftlint lint --fix +``` + +Make sure your SwiftLint version matches the version in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix). + +Nix users can use the following command to format with SwiftLint: + +``` +nix develop -c swiftlint lint --fix +``` + +To check for violations without auto-fixing: + +``` +nix develop -c swiftlint lint --strict +``` + ### Updating the Zig Cache Fixed-Output Derivation Hash The Nix package depends on a [fixed-output diff --git a/build.zig b/build.zig index fa68b91b484..c162d51f524 100644 --- a/build.zig +++ b/build.zig @@ -36,6 +36,7 @@ pub fn build(b: *std.Build) !void { // All our steps which we'll hook up later. The steps are shown // up here just so that they are more self-documenting. const libvt_step = b.step("lib-vt", "Build libghostty-vt"); + const cli_helper_step = b.step("cli-helper", "Build the Ghostty CLI helper"); const run_step = b.step("run", "Run the app"); const run_valgrind_step = b.step( "run-valgrind", @@ -61,6 +62,7 @@ pub fn build(b: *std.Build) !void { // Ghostty executable, the actual runnable Ghostty program. const exe = try buildpkg.GhosttyExe.init(b, &config, &deps); + cli_helper_step.dependOn(&exe.install_step.step); // Ghostty docs const docs = try buildpkg.GhosttyDocs.init(b, &deps); @@ -291,6 +293,14 @@ pub fn build(b: *std.Build) !void { if (config.emit_test_exe) b.installArtifact(test_exe); _ = try deps.add(test_exe); + // Verify our internal libghostty header. + const ghostty_h = b.addTranslateC(.{ + .root_source_file = b.path("include/ghostty.h"), + .target = config.baselineTarget(), + .optimize = .Debug, + }); + test_exe.root_module.addImport("ghostty.h", ghostty_h.createModule()); + // Normal test running const test_run = b.addRunArtifact(test_exe); test_step.dependOn(&test_run.step); diff --git a/build.zig.zon b/build.zig.zon index 497cef406e8..7a669a4a15f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .ghostty, - .version = "1.3.0-dev", + .version = "1.3.2-dev", .paths = .{""}, .fingerprint = 0x64407a2a0b4147e5, .minimum_zig_version = "0.15.2", @@ -21,7 +21,7 @@ }, .z2d = .{ // vancluever/z2d - .url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz", + .url = "https://deps.files.ghostty.org/z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ.tar.gz", .hash = "z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ", .lazy = true, }, @@ -39,8 +39,8 @@ }, .uucode = .{ // jacobsandlund/uucode - .url = "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz", - .hash = "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E", + .url = "https://deps.files.ghostty.org/uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9.tar.gz", + .hash = "uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9", }, .zig_wayland = .{ // codeberg ifreund/zig-wayland @@ -115,9 +115,10 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, + .android_ndk = .{ .path = "./pkg/android-ndk" }, .iterm2_themes = .{ - .url = "https://deps.files.ghostty.org/ghostty-themes-release-20260202-151632-49169e9.tgz", - .hash = "N-V-__8AAKlTAwClvdUfsAoiLgM6sb2NPZNnS6wzCiIs252Z", + .url = "https://deps.files.ghostty.org/ghostty-themes-release-20260216-151611-fc73ce3.tgz", + .hash = "N-V-__8AABVbAwBwDRyZONfx553tvMW8_A2OKUoLzPUSRiLF", .lazy = true, }, }, diff --git a/build.zig.zon.bak b/build.zig.zon.bak deleted file mode 100644 index 191ae7fa9c0..00000000000 --- a/build.zig.zon.bak +++ /dev/null @@ -1,124 +0,0 @@ -.{ - .name = .ghostty, - .version = "1.3.0-dev", - .paths = .{""}, - .fingerprint = 0x64407a2a0b4147e5, - .minimum_zig_version = "0.15.2", - .dependencies = .{ - // Zig libs - - .libxev = .{ - // mitchellh/libxev - .url = "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz", - .hash = "libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs", - .lazy = true, - }, - .vaxis = .{ - // rockorager/libvaxis - .url = "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz", - .hash = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS", - .lazy = true, - }, - .z2d = .{ - // vancluever/z2d - .url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz", - .hash = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T", - .lazy = true, - }, - .zig_objc = .{ - // mitchellh/zig-objc - .url = "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz", - .hash = "zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK", - .lazy = true, - }, - .zig_js = .{ - // mitchellh/zig-js - .url = "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz", - .hash = "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi", - .lazy = true, - }, - .uucode = .{ - // jacobsandlund/uucode - .url = "https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz", - .hash = "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E", - }, - .zig_wayland = .{ - // codeberg ifreund/zig-wayland - .url = "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz", - .hash = "wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe", - .lazy = true, - }, - .zf = .{ - // natecraddock/zf - .url = "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz", - .hash = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh", - .lazy = true, - }, - .gobject = .{ - // https://github.com/ghostty-org/zig-gobject based on zig_gobject - // Temporary until we generate them at build time automatically. - .url = "https://github.com/ghostty-org/zig-gobject/releases/download/0.7.0-2025-11-08-23-1/ghostty-gobject-0.7.0-2025-11-08-23-1.tar.zst", - .hash = "gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-", - .lazy = true, - }, - - // C libs - .cimgui = .{ .path = "./pkg/cimgui", .lazy = true }, - .fontconfig = .{ .path = "./pkg/fontconfig", .lazy = true }, - .freetype = .{ .path = "./pkg/freetype", .lazy = true }, - .gtk4_layer_shell = .{ .path = "./pkg/gtk4-layer-shell", .lazy = true }, - .harfbuzz = .{ .path = "./pkg/harfbuzz", .lazy = true }, - .highway = .{ .path = "./pkg/highway", .lazy = true }, - .libintl = .{ .path = "./pkg/libintl", .lazy = true }, - .libpng = .{ .path = "./pkg/libpng", .lazy = true }, - .macos = .{ .path = "./pkg/macos", .lazy = true }, - .oniguruma = .{ .path = "./pkg/oniguruma", .lazy = true }, - .opengl = .{ .path = "./pkg/opengl", .lazy = true }, - .sentry = .{ .path = "./pkg/sentry", .lazy = true }, - .simdutf = .{ .path = "./pkg/simdutf", .lazy = true }, - .utfcpp = .{ .path = "./pkg/utfcpp", .lazy = true }, - .wuffs = .{ .path = "./pkg/wuffs", .lazy = true }, - .zlib = .{ .path = "./pkg/zlib", .lazy = true }, - - // Shader translation - .glslang = .{ .path = "./pkg/glslang", .lazy = true }, - .spirv_cross = .{ .path = "./pkg/spirv-cross", .lazy = true }, - - // Wayland - .wayland = .{ - .url = "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz", - .hash = "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t", - .lazy = true, - }, - .wayland_protocols = .{ - .url = "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz", - .hash = "N-V-__8AAKw-DAAaV8bOAAGqA0-oD7o-HNIlPFYKRXSPT03S", - .lazy = true, - }, - .plasma_wayland_protocols = .{ - .url = "https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz", - .hash = "N-V-__8AAKYZBAB-CFHBKs3u4JkeiT4BMvyHu3Y5aaWF3Bbs", - .lazy = true, - }, - - // Fonts - .jetbrains_mono = .{ - .url = "https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz", - .hash = "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x", - .lazy = true, - }, - .nerd_fonts_symbols_only = .{ - .url = "https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz", - .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO26s", - .lazy = true, - }, - - // Other - .apple_sdk = .{ .path = "./pkg/apple-sdk" }, - .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz", - .hash = "N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-", - .lazy = true, - }, - }, -} diff --git a/build.zig.zon.json b/build.zig.zon.json index 5b557a49382..4a88e20174f 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -54,10 +54,10 @@ "url": "https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz", "hash": "sha256-yBbCDox18+Fa6Gc1DnmSVQLRpqhZOLsac7iSfl8x+cs=" }, - "N-V-__8AAKlTAwClvdUfsAoiLgM6sb2NPZNnS6wzCiIs252Z": { + "N-V-__8AABVbAwBwDRyZONfx553tvMW8_A2OKUoLzPUSRiLF": { "name": "iterm2_themes", - "url": "https://deps.files.ghostty.org/ghostty-themes-release-20260202-151632-49169e9.tgz", - "hash": "sha256-xN+3iQaN3uIJ/BzkgFxLojgHqeuz1htNcVjcjWR7Qjg=" + "url": "https://deps.files.ghostty.org/ghostty-themes-release-20260216-151611-fc73ce3.tgz", + "hash": "sha256-FCALuGoMgUq2lgnVALKAs5a20uuDXt8Gdt5KeJwKqP0=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "name": "jetbrains_mono", @@ -119,10 +119,10 @@ "url": "git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732", "hash": "sha256-sHPh+TQSdUGus/QTbj7KSJJkTuNTrK4VNmQDjS30Lf8=" }, - "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E": { + "uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9": { "name": "uucode", - "url": "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz", - "hash": "sha256-SzpYGhgG4B6Luf8eT35sKLobCxjmwEuo1Twk0jeu9g4=" + "url": "https://deps.files.ghostty.org/uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9.tar.gz", + "hash": "sha256-0KvuD0+L1urjwFF3fhbnxC2JZKqqAVWRxOVlcD9GX5U=" }, "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS": { "name": "vaxis", @@ -146,7 +146,7 @@ }, "z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ": { "name": "z2d", - "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz", + "url": "https://deps.files.ghostty.org/z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ.tar.gz", "hash": "sha256-afIdou/V7gk3/lXE0J5Ir8T7L5GgHvFnyMJ1rgRnl/c=" }, "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh": { diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 1cdecbb85b0..53e1b6c0267 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -171,11 +171,11 @@ in }; } { - name = "N-V-__8AAKlTAwClvdUfsAoiLgM6sb2NPZNnS6wzCiIs252Z"; + name = "N-V-__8AABVbAwBwDRyZONfx553tvMW8_A2OKUoLzPUSRiLF"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://deps.files.ghostty.org/ghostty-themes-release-20260202-151632-49169e9.tgz"; - hash = "sha256-xN+3iQaN3uIJ/BzkgFxLojgHqeuz1htNcVjcjWR7Qjg="; + url = "https://deps.files.ghostty.org/ghostty-themes-release-20260216-151611-fc73ce3.tgz"; + hash = "sha256-FCALuGoMgUq2lgnVALKAs5a20uuDXt8Gdt5KeJwKqP0="; }; } { @@ -275,11 +275,11 @@ in }; } { - name = "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E"; + name = "uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9"; path = fetchZigArtifact { name = "uucode"; - url = "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz"; - hash = "sha256-SzpYGhgG4B6Luf8eT35sKLobCxjmwEuo1Twk0jeu9g4="; + url = "https://deps.files.ghostty.org/uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9.tar.gz"; + hash = "sha256-0KvuD0+L1urjwFF3fhbnxC2JZKqqAVWRxOVlcD9GX5U="; }; } { @@ -318,7 +318,7 @@ in name = "z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ"; path = fetchZigArtifact { name = "z2d"; - url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz"; + url = "https://deps.files.ghostty.org/z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ.tar.gz"; hash = "sha256-afIdou/V7gk3/lXE0J5Ir8T7L5GgHvFnyMJ1rgRnl/c="; }; } diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 468208621b6..4ac9e659273 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -6,7 +6,7 @@ https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918 https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz https://deps.files.ghostty.org/gettext-0.24.tar.gz -https://deps.files.ghostty.org/ghostty-themes-release-20260202-151632-49169e9.tgz +https://deps.files.ghostty.org/ghostty-themes-release-20260216-151611-fc73ce3.tgz https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz @@ -21,11 +21,12 @@ https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz -https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz +https://deps.files.ghostty.org/uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9.tar.gz https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz +https://deps.files.ghostty.org/z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ.tar.gz https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz @@ -33,4 +34,3 @@ https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz -https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz diff --git a/dist/linux/com.mitchellh.ghostty.metainfo.xml.in b/dist/linux/com.mitchellh.ghostty.metainfo.xml.in index 42ccc27540c..4f23c35da68 100644 --- a/dist/linux/com.mitchellh.ghostty.metainfo.xml.in +++ b/dist/linux/com.mitchellh.ghostty.metainfo.xml.in @@ -52,6 +52,12 @@ + + https://ghostty.org/docs/install/release-notes/1-3-1 + + + https://ghostty.org/docs/install/release-notes/1-3-0 + https://ghostty.org/docs/install/release-notes/1-0-1 diff --git a/example/c-vt-formatter/README.md b/example/c-vt-formatter/README.md new file mode 100644 index 00000000000..f416c8dbdbe --- /dev/null +++ b/example/c-vt-formatter/README.md @@ -0,0 +1,18 @@ +# Example: `ghostty-vt` Terminal Formatter + +This contains a simple example of how to use the `ghostty-vt` terminal and +formatter APIs to create a terminal, write VT-encoded content into it, and +format the screen contents as plain text. + +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-formatter/build.zig b/example/c-vt-formatter/build.zig new file mode 100644 index 00000000000..637b48f13cb --- /dev/null +++ b/example/c-vt-formatter/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_formatter", + .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-formatter/build.zig.zon b/example/c-vt-formatter/build.zig.zon new file mode 100644 index 00000000000..a14f0aedbf7 --- /dev/null +++ b/example/c-vt-formatter/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt_formatter, + .version = "0.0.0", + .fingerprint = 0x9e3758265677a0c4, + .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-formatter/src/main.c b/example/c-vt-formatter/src/main.c new file mode 100644 index 00000000000..5d408b17238 --- /dev/null +++ b/example/c-vt-formatter/src/main.c @@ -0,0 +1,63 @@ +#include +#include +#include +#include +#include + +int main() { + // Create a terminal with a small grid + GhosttyTerminal terminal; + GhosttyTerminalOptions opts = { + .cols = 80, + .rows = 24, + .max_scrollback = 0, + }; + GhosttyResult result = ghostty_terminal_new(NULL, &terminal, opts); + assert(result == GHOSTTY_SUCCESS); + + // Write VT-encoded content into the terminal to exercise various + // cursor movement and styling sequences. + const char *commands[] = { + "Line 1: Hello World!\r\n", // Simple text on row 1 + "Line 2: \033[1mBold\033[0m and " // Bold text on row 2 + "\033[4mUnderline\033[0m\r\n", + "Line 3: placeholder\r\n", // Will be overwritten below + "\033[3;1H", // CUP: move cursor back to row 3, col 1 + "\033[2K", // EL: erase the entire line + "Line 3: Overwritten!\r\n", // Rewrite row 3 with new content + "\033[5;10H", // CUP: jump to row 5, col 10 + "Placed at (5,10)", // Write at that position + "\033[1;72H", // CUP: jump to row 1, col 72 + "RIGHT->", // Near the right edge of row 1 + }; + for (size_t i = 0; i < sizeof(commands) / sizeof(commands[0]); i++) { + ghostty_terminal_vt_write(terminal, (const uint8_t *)commands[i], + strlen(commands[i])); + } + + // Create a plain-text formatter for the terminal + GhosttyFormatterTerminalOptions fmt_opts = GHOSTTY_INIT_SIZED(GhosttyFormatterTerminalOptions); + fmt_opts.emit = GHOSTTY_FORMATTER_FORMAT_PLAIN; + fmt_opts.trim = true; + + GhosttyFormatter formatter; + result = ghostty_formatter_terminal_new(NULL, &formatter, terminal, fmt_opts); + assert(result == GHOSTTY_SUCCESS); + + // Format into an allocated buffer + uint8_t *buf = NULL; + size_t len = 0; + result = ghostty_formatter_format_alloc(formatter, NULL, &buf, &len); + assert(result == GHOSTTY_SUCCESS); + + // Print the formatted output + printf("Formatted output (%zu bytes):\n", len); + fwrite(buf, 1, len, stdout); + printf("\n"); + + // Clean up + free(buf); + ghostty_formatter_free(formatter); + ghostty_terminal_free(terminal); + return 0; +} diff --git a/example/zig-formatter/src/main.zig b/example/zig-formatter/src/main.zig index ad101dbf177..df21a20468f 100644 --- a/example/zig-formatter/src/main.zig +++ b/example/zig-formatter/src/main.zig @@ -23,8 +23,8 @@ pub fn main() !void { // Replace \n with \r\n for (buf[0..n]) |byte| { - if (byte == '\n') try stream.next('\r'); - try stream.next(byte); + if (byte == '\n') stream.next('\r'); + stream.next(byte); } } diff --git a/example/zig-vt-stream/src/main.zig b/example/zig-vt-stream/src/main.zig index 8fd438b7027..87d8857ddb1 100644 --- a/example/zig-vt-stream/src/main.zig +++ b/example/zig-vt-stream/src/main.zig @@ -14,24 +14,24 @@ pub fn main() !void { defer stream.deinit(); // Basic text with newline - try stream.nextSlice("Hello, World!\r\n"); + stream.nextSlice("Hello, World!\r\n"); // ANSI color codes: ESC[1;32m = bold green, ESC[0m = reset - try stream.nextSlice("\x1b[1;32mGreen Text\x1b[0m\r\n"); + stream.nextSlice("\x1b[1;32mGreen Text\x1b[0m\r\n"); // Cursor positioning: ESC[1;1H = move to row 1, column 1 - try stream.nextSlice("\x1b[1;1HTop-left corner\r\n"); + stream.nextSlice("\x1b[1;1HTop-left corner\r\n"); // Cursor movement: ESC[5B = move down 5 lines - try stream.nextSlice("\x1b[5B"); - try stream.nextSlice("Moved down!\r\n"); + stream.nextSlice("\x1b[5B"); + stream.nextSlice("Moved down!\r\n"); // Erase line: ESC[2K = clear entire line - try stream.nextSlice("\x1b[2K"); - try stream.nextSlice("New content\r\n"); + stream.nextSlice("\x1b[2K"); + stream.nextSlice("New content\r\n"); // Multiple lines - try stream.nextSlice("Line A\r\nLine B\r\nLine C\r\n"); + stream.nextSlice("Line A\r\nLine B\r\nLine C\r\n"); // Get the final terminal state as a plain string const str = try t.plainString(alloc); diff --git a/flake.lock b/flake.lock index 6f12f66b92d..b8e6d92634d 100644 --- a/flake.lock +++ b/flake.lock @@ -16,24 +16,6 @@ "type": "github" } }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, "home-manager": { "inputs": { "nixpkgs": [ @@ -70,7 +52,6 @@ "root": { "inputs": { "flake-compat": "flake-compat", - "flake-utils": "flake-utils", "home-manager": "home-manager", "nixpkgs": "nixpkgs", "zig": "zig", @@ -78,6 +59,7 @@ } }, "systems": { + "flake": false, "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", @@ -97,19 +79,17 @@ "flake-compat": [ "flake-compat" ], - "flake-utils": [ - "flake-utils" - ], "nixpkgs": [ "nixpkgs" - ] + ], + "systems": "systems" }, "locked": { - "lastModified": 1763295135, - "narHash": "sha256-sGv/NHCmEnJivguGwB5w8LRmVqr1P72OjS+NzcJsssE=", + "lastModified": 1773145353, + "narHash": "sha256-dE8zx8WA54TRmFFQBvA48x/sXGDTP7YaDmY6nNKMAYw=", "owner": "mitchellh", "repo": "zig-overlay", - "rev": "64f8b42cfc615b2cf99144adf2b7728c7847c72a", + "rev": "8666155d83bf792956a7c40915508e6d4b2b8716", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index d892dbd2fbf..61ca39ab1a4 100644 --- a/flake.nix +++ b/flake.nix @@ -10,7 +10,6 @@ # Gnome 49/Gtk 4.20. # nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"; - flake-utils.url = "github:numtide/flake-utils"; # Used for shell.nix flake-compat = { @@ -22,7 +21,6 @@ url = "github:mitchellh/zig-overlay"; inputs = { nixpkgs.follows = "nixpkgs"; - flake-utils.follows = "flake-utils"; flake-compat.follows = "flake-compat"; }; }; @@ -80,6 +78,7 @@ packageOverrides = pyfinal: pyprev: { blessed = pyfinal.callPackage ./nix/pkgs/blessed.nix {}; ucs-detect = pyfinal.callPackage ./nix/pkgs/ucs-detect.nix {}; + wcwidth = pyfinal.callPackage ./nix/pkgs/wcwidth.nix {}; }; }; }; diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index d5c96064d2b..e58ecd4488b 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -67,9 +67,9 @@ }, { "type": "archive", - "url": "https://deps.files.ghostty.org/ghostty-themes-release-20260202-151632-49169e9.tgz", - "dest": "vendor/p/N-V-__8AAKlTAwClvdUfsAoiLgM6sb2NPZNnS6wzCiIs252Z", - "sha256": "c4dfb789068ddee209fc1ce4805c4ba23807a9ebb3d61b4d7158dc8d647b4238" + "url": "https://deps.files.ghostty.org/ghostty-themes-release-20260216-151611-fc73ce3.tgz", + "dest": "vendor/p/N-V-__8AABVbAwBwDRyZONfx553tvMW8_A2OKUoLzPUSRiLF", + "sha256": "14200bb86a0c814ab69609d500b280b396b6d2eb835edf0676de4a789c0aa8fd" }, { "type": "archive", @@ -145,9 +145,9 @@ }, { "type": "archive", - "url": "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz", - "dest": "vendor/p/uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E", - "sha256": "4b3a581a1806e01e8bb9ff1e4f7e6c28ba1b0b18e6c04ba8d53c24d237aef60e" + "url": "https://deps.files.ghostty.org/uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9.tar.gz", + "dest": "vendor/p/uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9", + "sha256": "d0abee0f4f8bd6eae3c051777e16e7c42d8964aaaa015591c4e565703f465f95" }, { "type": "archive", @@ -175,7 +175,7 @@ }, { "type": "archive", - "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz", + "url": "https://deps.files.ghostty.org/z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ.tar.gz", "dest": "vendor/p/z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ", "sha256": "69f21da2efd5ee0937fe55c4d09e48afc4fb2f91a01ef167c8c275ae046797f7" }, diff --git a/include/ghostty.h b/include/ghostty.h index b32cc9856d1..40ff55c9b3d 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -463,6 +463,12 @@ typedef struct { // Config types +// config.Path +typedef struct { + const char* path; + bool optional; +} ghostty_config_path_s; + // config.Color typedef struct { uint8_t r; @@ -509,6 +515,15 @@ typedef struct { ghostty_quick_terminal_size_s secondary; } ghostty_config_quick_terminal_size_s; +// config.Fullscreen +typedef enum { + GHOSTTY_CONFIG_FULLSCREEN_FALSE, + GHOSTTY_CONFIG_FULLSCREEN_TRUE, + GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE, + GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE_VISIBLE_MENU, + GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE_PADDED_NOTCH, +} ghostty_config_fullscreen_e; + // apprt.Target.Key typedef enum { GHOSTTY_TARGET_APP, @@ -577,9 +592,9 @@ typedef enum { // apprt.action.Fullscreen typedef enum { GHOSTTY_FULLSCREEN_NATIVE, - GHOSTTY_FULLSCREEN_NON_NATIVE, - GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU, - GHOSTTY_FULLSCREEN_NON_NATIVE_PADDED_NOTCH, + GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE, + GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_VISIBLE_MENU, + GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_PADDED_NOTCH, } ghostty_action_fullscreen_e; // apprt.action.FloatWindow @@ -709,7 +724,7 @@ typedef struct { // renderer.Health typedef enum { - GHOSTTY_RENDERER_HEALTH_OK, + GHOSTTY_RENDERER_HEALTH_HEALTHY, GHOSTTY_RENDERER_HEALTH_UNHEALTHY, } ghostty_action_renderer_health_e; @@ -874,6 +889,7 @@ typedef enum { GHOSTTY_ACTION_RENDER_INSPECTOR, GHOSTTY_ACTION_DESKTOP_NOTIFICATION, GHOSTTY_ACTION_SET_TITLE, + GHOSTTY_ACTION_SET_TAB_TITLE, GHOSTTY_ACTION_PROMPT_TITLE, GHOSTTY_ACTION_PWD, GHOSTTY_ACTION_MOUSE_SHAPE, @@ -922,6 +938,7 @@ typedef union { ghostty_action_inspector_e inspector; ghostty_action_desktop_notification_s desktop_notification; ghostty_action_set_title_s set_title; + ghostty_action_set_title_s set_tab_title; ghostty_action_prompt_title_e prompt_title; ghostty_action_pwd_s pwd; ghostty_action_mouse_shape_e mouse_shape; @@ -953,7 +970,7 @@ typedef struct { } ghostty_action_s; typedef void (*ghostty_runtime_wakeup_cb)(void*); -typedef void (*ghostty_runtime_read_clipboard_cb)(void*, +typedef bool (*ghostty_runtime_read_clipboard_cb)(void*, ghostty_clipboard_e, void*); typedef void (*ghostty_runtime_confirm_read_clipboard_cb)( diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 4f8fef88ecc..dd5eda98937 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -28,6 +28,8 @@ * @section groups_sec API Reference * * The API is organized into the following groups: + * - @ref terminal "Terminal" - Complete terminal emulator state and rendering + * - @ref formatter "Formatter" - Format terminal content as plain text, VT sequences, or HTML * - @ref key "Key Encoding" - Encode key events into terminal sequences * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences * - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences @@ -42,6 +44,7 @@ * - @ref c-vt-key-encode/src/main.c - Key encoding example * - @ref c-vt-paste/src/main.c - Paste safety check example * - @ref c-vt-sgr/src/main.c - SGR parser example + * - @ref c-vt-formatter/src/main.c - Terminal formatter example * */ @@ -65,6 +68,12 @@ * styling sequences and extract text attributes like colors and underline styles. */ +/** @example c-vt-formatter/src/main.c + * This example demonstrates how to use the terminal and formatter APIs to + * create a terminal, write VT-encoded content into it, and format the screen + * contents as plain text. + */ + #ifndef GHOSTTY_VT_H #define GHOSTTY_VT_H @@ -72,8 +81,10 @@ extern "C" { #endif -#include +#include #include +#include +#include #include #include #include diff --git a/include/ghostty/vt/formatter.h b/include/ghostty/vt/formatter.h new file mode 100644 index 00000000000..4beb5fc771f --- /dev/null +++ b/include/ghostty/vt/formatter.h @@ -0,0 +1,226 @@ +/** + * @file formatter.h + * + * Format terminal content as plain text, VT sequences, or HTML. + */ + +#ifndef GHOSTTY_VT_FORMATTER_H +#define GHOSTTY_VT_FORMATTER_H + +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup formatter Formatter + * + * Format terminal content as plain text, VT sequences, or HTML. + * + * A formatter captures a reference to a terminal and formatting options. + * It can be used repeatedly to produce output that reflects the current + * terminal state at the time of each format call. + * + * The terminal must outlive the formatter. + * + * @{ + */ + +/** + * Output format. + * + * @ingroup formatter + */ +typedef enum { + /** Plain text (no escape sequences). */ + GHOSTTY_FORMATTER_FORMAT_PLAIN, + + /** VT sequences preserving colors, styles, URLs, etc. */ + GHOSTTY_FORMATTER_FORMAT_VT, + + /** HTML with inline styles. */ + GHOSTTY_FORMATTER_FORMAT_HTML, +} GhosttyFormatterFormat; + +/** + * Extra screen state to include in styled output. + * + * @ingroup formatter + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyFormatterScreenExtra). */ + size_t size; + + /** Emit cursor position using CUP (CSI H). */ + bool cursor; + + /** Emit current SGR style state based on the cursor's active style_id. */ + bool style; + + /** Emit current hyperlink state using OSC 8 sequences. */ + bool hyperlink; + + /** Emit character protection mode using DECSCA. */ + bool protection; + + /** Emit Kitty keyboard protocol state using CSI > u and CSI = sequences. */ + bool kitty_keyboard; + + /** Emit character set designations and invocations. */ + bool charsets; +} GhosttyFormatterScreenExtra; + +/** + * Extra terminal state to include in styled output. + * + * @ingroup formatter + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyFormatterTerminalExtra). */ + size_t size; + + /** Emit the palette using OSC 4 sequences. */ + bool palette; + + /** Emit terminal modes that differ from their defaults using CSI h/l. */ + bool modes; + + /** Emit scrolling region state using DECSTBM and DECSLRM sequences. */ + bool scrolling_region; + + /** Emit tabstop positions by clearing all tabs and setting each one. */ + bool tabstops; + + /** Emit the present working directory using OSC 7. */ + bool pwd; + + /** Emit keyboard modes such as ModifyOtherKeys. */ + bool keyboard; + + /** Screen-level extras. */ + GhosttyFormatterScreenExtra screen; +} GhosttyFormatterTerminalExtra; + +/** + * Opaque handle to a formatter instance. + * + * @ingroup formatter + */ +typedef struct GhosttyFormatter* GhosttyFormatter; + +/** + * Options for creating a terminal formatter. + * + * @ingroup formatter + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyFormatterTerminalOptions). */ + size_t size; + + /** Output format to emit. */ + GhosttyFormatterFormat emit; + + /** Whether to unwrap soft-wrapped lines. */ + bool unwrap; + + /** Whether to trim trailing whitespace on non-blank lines. */ + bool trim; + + /** Extra terminal state to include in styled output. */ + GhosttyFormatterTerminalExtra extra; +} GhosttyFormatterTerminalOptions; + +/** + * Create a formatter for a terminal's active screen. + * + * The terminal must outlive the formatter. The formatter stores a borrowed + * reference to the terminal and reads its current state on each format call. + * + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param formatter Pointer to store the created formatter handle + * @param terminal The terminal to format (must not be NULL) + * @param options Formatting options + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup formatter + */ +GhosttyResult ghostty_formatter_terminal_new( + const GhosttyAllocator* allocator, + GhosttyFormatter* formatter, + GhosttyTerminal terminal, + GhosttyFormatterTerminalOptions options); + +/** + * Run the formatter and produce output into the caller-provided buffer. + * + * Each call formats the current terminal state. Pass NULL for buf to + * query the required buffer size without writing any output; in that case + * out_written receives the required size and the return value is + * GHOSTTY_OUT_OF_SPACE. + * + * If the buffer is too small, returns GHOSTTY_OUT_OF_SPACE and sets + * out_written to the required size. The caller can then retry with a + * larger buffer. + * + * @param formatter The formatter handle (must not be NULL) + * @param buf Pointer to the output buffer, or NULL to query size + * @param buf_len Length of the output buffer in bytes + * @param out_written Pointer to receive the number of bytes written, + * or the required size on failure + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup formatter + */ +GhosttyResult ghostty_formatter_format_buf(GhosttyFormatter formatter, + uint8_t* buf, + size_t buf_len, + size_t* out_written); + +/** + * Run the formatter and return an allocated buffer with the output. + * + * Each call formats the current terminal state. The buffer is allocated + * using the provided allocator (or the default allocator if NULL). + * The caller is responsible for freeing the returned buffer. When using + * the default allocator (NULL), the buffer can be freed with `free()`. + * When using a custom allocator, the buffer must be freed using the + * same allocator. + * + * @param formatter The formatter handle (must not be NULL) + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param out_ptr Pointer to receive the allocated buffer + * @param out_len Pointer to receive the length of the output in bytes + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation + * failure + * + * @ingroup formatter + */ +GhosttyResult ghostty_formatter_format_alloc(GhosttyFormatter formatter, + const GhosttyAllocator* allocator, + uint8_t** out_ptr, + size_t* out_len); + +/** + * Free a formatter instance. + * + * Releases all resources associated with the formatter. After this call, + * the formatter handle becomes invalid. + * + * @param formatter The formatter handle to free (may be NULL) + * + * @ingroup formatter + */ +void ghostty_formatter_free(GhosttyFormatter formatter); + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_FORMATTER_H */ diff --git a/include/ghostty/vt/key.h b/include/ghostty/vt/key.h index 772b5d43bcf..e82a7596ca8 100644 --- a/include/ghostty/vt/key.h +++ b/include/ghostty/vt/key.h @@ -15,7 +15,9 @@ * ## Basic Usage * * 1. Create an encoder instance with ghostty_key_encoder_new() - * 2. Configure encoder options with ghostty_key_encoder_setopt(). + * 2. Configure encoder options with ghostty_key_encoder_setopt() + * or ghostty_key_encoder_setopt_from_terminal() if you have a + * GhosttyTerminal. * 3. For each key event: * - Create a key event with ghostty_key_event_new() * - Set event properties (action, key, modifiers, etc.) @@ -25,6 +27,9 @@ * changing its properties. * 4. Free the encoder with ghostty_key_encoder_free() when done * + * For a complete working example, see example/c-vt-key-encode in the + * repository. + * * ## Example * * @code{.c} @@ -66,8 +71,33 @@ * } * @endcode * - * For a complete working example, see example/c-vt-key-encode in the - * repository. + * ## Example: Encoding with Terminal State + * + * When you have a GhosttyTerminal, you can sync its modes (cursor key + * application, Kitty flags, etc.) into the encoder automatically: + * + * @code{.c} + * // Create a terminal and feed it some VT data that changes modes + * GhosttyTerminal terminal; + * ghostty_terminal_new(NULL, &terminal, + * (GhosttyTerminalOptions){.cols = 80, .rows = 24, .max_scrollback = 0}); + * + * // Application might write data that enables Kitty keyboard protocol, etc. + * ghostty_terminal_vt_write(terminal, vt_data, vt_len); + * + * // Create an encoder and sync its options from the terminal + * GhosttyKeyEncoder encoder; + * ghostty_key_encoder_new(NULL, &encoder); + * ghostty_key_encoder_setopt_from_terminal(encoder, terminal); + * + * // Encode a key event using the terminal-derived options + * char buf[128]; + * size_t written = 0; + * ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); + * + * ghostty_key_encoder_free(encoder); + * ghostty_terminal_free(terminal); + * @endcode * * @{ */ diff --git a/include/ghostty/vt/key/encoder.h b/include/ghostty/vt/key/encoder.h index 766a2942796..3053d73efee 100644 --- a/include/ghostty/vt/key/encoder.h +++ b/include/ghostty/vt/key/encoder.h @@ -9,8 +9,9 @@ #include #include -#include +#include #include +#include #include /** @@ -140,6 +141,10 @@ void ghostty_key_encoder_free(GhosttyKeyEncoder encoder); * protocol selection (Kitty keyboard protocol flags), and platform-specific * behaviors (macOS option-as-alt). * + * If you are using a terminal instance, you can set the key encoding + * options based on the active terminal state (e.g. legacy vs Kitty mode + * and associated flags) with ghostty_key_encoder_setopt_from_terminal(). + * * A null pointer value does nothing. It does not reset the value to the * default. The setopt call will do nothing. * @@ -151,6 +156,25 @@ void ghostty_key_encoder_free(GhosttyKeyEncoder encoder); */ void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOption option, const void *value); +/** + * Set encoder options from a terminal's current state. + * + * Reads the terminal's current modes and flags and applies them to the + * encoder's options. This sets cursor key application mode, keypad mode, + * alt escape prefix, modifyOtherKeys state, and Kitty keyboard protocol + * flags from the terminal state. + * + * Note that the `macos_option_as_alt` option cannot be determined from + * terminal state and is reset to `GHOSTTY_OPTION_AS_ALT_FALSE` by this + * call. Use ghostty_key_encoder_setopt() to set it afterward if needed. + * + * @param encoder The encoder handle, must not be NULL + * @param terminal The terminal handle, must not be NULL + * + * @ingroup key + */ +void ghostty_key_encoder_setopt_from_terminal(GhosttyKeyEncoder encoder, GhosttyTerminal terminal); + /** * Encode a key event into a terminal escape sequence. * diff --git a/include/ghostty/vt/key/event.h b/include/ghostty/vt/key/event.h index dbd2e9f841a..2fe45511200 100644 --- a/include/ghostty/vt/key/event.h +++ b/include/ghostty/vt/key/event.h @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include /** diff --git a/include/ghostty/vt/osc.h b/include/ghostty/vt/osc.h index f53077ab326..69f7d1e5521 100644 --- a/include/ghostty/vt/osc.h +++ b/include/ghostty/vt/osc.h @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include /** diff --git a/include/ghostty/vt/result.h b/include/ghostty/vt/result.h deleted file mode 100644 index 65938ee766f..00000000000 --- a/include/ghostty/vt/result.h +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @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, - /** Operation failed due to invalid value */ - GHOSTTY_INVALID_VALUE = -2, -} GhosttyResult; - -#endif /* GHOSTTY_VT_RESULT_H */ diff --git a/include/ghostty/vt/sgr.h b/include/ghostty/vt/sgr.h index 0c1afc309bd..c81c1c87a15 100644 --- a/include/ghostty/vt/sgr.h +++ b/include/ghostty/vt/sgr.h @@ -73,7 +73,7 @@ #include #include -#include +#include #include #include #include @@ -109,30 +109,29 @@ typedef enum { GHOSTTY_SGR_ATTR_RESET_ITALIC = 5, GHOSTTY_SGR_ATTR_FAINT = 6, GHOSTTY_SGR_ATTR_UNDERLINE = 7, - GHOSTTY_SGR_ATTR_RESET_UNDERLINE = 8, - GHOSTTY_SGR_ATTR_UNDERLINE_COLOR = 9, - GHOSTTY_SGR_ATTR_UNDERLINE_COLOR_256 = 10, - GHOSTTY_SGR_ATTR_RESET_UNDERLINE_COLOR = 11, - GHOSTTY_SGR_ATTR_OVERLINE = 12, - GHOSTTY_SGR_ATTR_RESET_OVERLINE = 13, - GHOSTTY_SGR_ATTR_BLINK = 14, - GHOSTTY_SGR_ATTR_RESET_BLINK = 15, - GHOSTTY_SGR_ATTR_INVERSE = 16, - GHOSTTY_SGR_ATTR_RESET_INVERSE = 17, - GHOSTTY_SGR_ATTR_INVISIBLE = 18, - GHOSTTY_SGR_ATTR_RESET_INVISIBLE = 19, - GHOSTTY_SGR_ATTR_STRIKETHROUGH = 20, - GHOSTTY_SGR_ATTR_RESET_STRIKETHROUGH = 21, - GHOSTTY_SGR_ATTR_DIRECT_COLOR_FG = 22, - GHOSTTY_SGR_ATTR_DIRECT_COLOR_BG = 23, - GHOSTTY_SGR_ATTR_BG_8 = 24, - GHOSTTY_SGR_ATTR_FG_8 = 25, - GHOSTTY_SGR_ATTR_RESET_FG = 26, - GHOSTTY_SGR_ATTR_RESET_BG = 27, - GHOSTTY_SGR_ATTR_BRIGHT_BG_8 = 28, - GHOSTTY_SGR_ATTR_BRIGHT_FG_8 = 29, - GHOSTTY_SGR_ATTR_BG_256 = 30, - GHOSTTY_SGR_ATTR_FG_256 = 31, + GHOSTTY_SGR_ATTR_UNDERLINE_COLOR = 8, + GHOSTTY_SGR_ATTR_UNDERLINE_COLOR_256 = 9, + GHOSTTY_SGR_ATTR_RESET_UNDERLINE_COLOR = 10, + GHOSTTY_SGR_ATTR_OVERLINE = 11, + GHOSTTY_SGR_ATTR_RESET_OVERLINE = 12, + GHOSTTY_SGR_ATTR_BLINK = 13, + GHOSTTY_SGR_ATTR_RESET_BLINK = 14, + GHOSTTY_SGR_ATTR_INVERSE = 15, + GHOSTTY_SGR_ATTR_RESET_INVERSE = 16, + GHOSTTY_SGR_ATTR_INVISIBLE = 17, + GHOSTTY_SGR_ATTR_RESET_INVISIBLE = 18, + GHOSTTY_SGR_ATTR_STRIKETHROUGH = 19, + GHOSTTY_SGR_ATTR_RESET_STRIKETHROUGH = 20, + GHOSTTY_SGR_ATTR_DIRECT_COLOR_FG = 21, + GHOSTTY_SGR_ATTR_DIRECT_COLOR_BG = 22, + GHOSTTY_SGR_ATTR_BG_8 = 23, + GHOSTTY_SGR_ATTR_FG_8 = 24, + GHOSTTY_SGR_ATTR_RESET_FG = 25, + GHOSTTY_SGR_ATTR_RESET_BG = 26, + GHOSTTY_SGR_ATTR_BRIGHT_BG_8 = 27, + GHOSTTY_SGR_ATTR_BRIGHT_FG_8 = 28, + GHOSTTY_SGR_ATTR_BG_256 = 29, + GHOSTTY_SGR_ATTR_FG_256 = 30, } GhosttySgrAttributeTag; /** diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h new file mode 100644 index 00000000000..8c91920a033 --- /dev/null +++ b/include/ghostty/vt/terminal.h @@ -0,0 +1,203 @@ +/** + * @file terminal.h + * + * Complete terminal emulator state and rendering. + */ + +#ifndef GHOSTTY_VT_TERMINAL_H +#define GHOSTTY_VT_TERMINAL_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup terminal Terminal + * + * Complete terminal emulator state and rendering. + * + * A terminal instance manages the full emulator state including the screen, + * scrollback, cursor, styles, modes, and VT stream processing. + * + * Once a terminal session is up and running, you can configure a key encoder + * to write keyboard input via ghostty_key_encoder_setopt_from_terminal(). + * + * @{ + */ + +/** + * Opaque handle to a terminal instance. + * + * @ingroup terminal + */ +typedef struct GhosttyTerminal* GhosttyTerminal; + +/** + * Terminal initialization options. + * + * @ingroup terminal + */ +typedef struct { + /** Terminal width in cells. Must be greater than zero. */ + uint16_t cols; + + /** Terminal height in cells. Must be greater than zero. */ + uint16_t rows; + + /** Maximum number of lines to keep in scrollback history. */ + size_t max_scrollback; + + // TODO: Consider ABI compatibility implications of this struct. + // We may want to artificially pad it significantly to support + // future options. +} GhosttyTerminalOptions; + +/** + * Scroll viewport behavior tag. + * + * @ingroup terminal + */ +typedef enum { + /** Scroll to the top of the scrollback. */ + GHOSTTY_SCROLL_VIEWPORT_TOP, + + /** Scroll to the bottom (active area). */ + GHOSTTY_SCROLL_VIEWPORT_BOTTOM, + + /** Scroll by a delta amount (up is negative). */ + GHOSTTY_SCROLL_VIEWPORT_DELTA, +} GhosttyTerminalScrollViewportTag; + +/** + * Scroll viewport value. + * + * @ingroup terminal + */ +typedef union { + /** Scroll delta (only used with GHOSTTY_SCROLL_VIEWPORT_DELTA). Up is negative. */ + intptr_t delta; + + /** Padding for ABI compatibility. Do not use. */ + uint64_t _padding[2]; +} GhosttyTerminalScrollViewportValue; + +/** + * Tagged union for scroll viewport behavior. + * + * @ingroup terminal + */ +typedef struct { + GhosttyTerminalScrollViewportTag tag; + GhosttyTerminalScrollViewportValue value; +} GhosttyTerminalScrollViewport; + +/** + * Create a new terminal instance. + * + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param terminal Pointer to store the created terminal handle + * @param options Terminal initialization options + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup terminal + */ +GhosttyResult ghostty_terminal_new(const GhosttyAllocator* allocator, + GhosttyTerminal* terminal, + GhosttyTerminalOptions options); + +/** + * Free a terminal instance. + * + * Releases all resources associated with the terminal. After this call, + * the terminal handle becomes invalid and must not be used. + * + * @param terminal The terminal handle to free (may be NULL) + * + * @ingroup terminal + */ +void ghostty_terminal_free(GhosttyTerminal terminal); + +/** + * Perform a full reset of the terminal (RIS). + * + * Resets all terminal state back to its initial configuration, including + * modes, scrollback, scrolling region, and screen contents. The terminal + * dimensions are preserved. + * + * @param terminal The terminal handle (may be NULL, in which case this is a no-op) + * + * @ingroup terminal + */ +void ghostty_terminal_reset(GhosttyTerminal terminal); + +/** + * Resize the terminal to the given dimensions. + * + * Changes the number of columns and rows in the terminal. The primary + * screen will reflow content if wraparound mode is enabled; the alternate + * screen does not reflow. If the dimensions are unchanged, this is a no-op. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param cols New width in cells (must be greater than zero) + * @param rows New height in cells (must be greater than zero) + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup terminal + */ +GhosttyResult ghostty_terminal_resize(GhosttyTerminal terminal, + uint16_t cols, + uint16_t rows); + +/** + * Write VT-encoded data to the terminal for processing. + * + * Feeds raw bytes through the terminal's VT stream parser, updating + * terminal state accordingly. Only read-only sequences are processed; + * sequences that require output (queries) are ignored. + * + * In the future, a callback-based API will be added to allow handling + * of output or side effect sequences. + * + * This never fails. Any erroneous input or errors in processing the + * input are logged internally but do not cause this function to fail + * because this input is assumed to be untrusted and from an external + * source; so the primary goal is to keep the terminal state consistent and + * not allow malformed input to corrupt or crash. + * + * @param terminal The terminal handle + * @param data Pointer to the data to write + * @param len Length of the data in bytes + * + * @ingroup terminal + */ +void ghostty_terminal_vt_write(GhosttyTerminal terminal, + const uint8_t* data, + size_t len); + +/** + * Scroll the terminal viewport. + * + * Scrolls the terminal's viewport according to the given behavior. + * When using GHOSTTY_SCROLL_VIEWPORT_DELTA, set the delta field in + * the value union to specify the number of rows to scroll (negative + * for up, positive for down). For other behaviors, the value is ignored. + * + * @param terminal The terminal handle (may be NULL, in which case this is a no-op) + * @param behavior The scroll behavior as a tagged union + * + * @ingroup terminal + */ +void ghostty_terminal_scroll_viewport(GhosttyTerminal terminal, + GhosttyTerminalScrollViewport behavior); + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_TERMINAL_H */ diff --git a/include/ghostty/vt/types.h b/include/ghostty/vt/types.h new file mode 100644 index 00000000000..12ace266e1b --- /dev/null +++ b/include/ghostty/vt/types.h @@ -0,0 +1,45 @@ +/** + * @file types.h + * + * Common types, macros, and utilities for libghostty-vt. + */ + +#ifndef GHOSTTY_VT_TYPES_H +#define GHOSTTY_VT_TYPES_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, + /** Operation failed due to invalid value */ + GHOSTTY_INVALID_VALUE = -2, + /** Operation failed because the provided buffer was too small */ + GHOSTTY_OUT_OF_SPACE = -3, +} GhosttyResult; + +/** + * Initialize a sized struct to zero and set its size field. + * + * Sized structs use a `size` field as the first member for ABI + * compatibility. This macro zero-initializes the struct and sets the + * size field to `sizeof(type)`, which allows the library to detect + * which version of the struct the caller was compiled against. + * + * @param type The struct type to initialize + * @return A zero-initialized struct with the size field set + * + * Example: + * @code + * GhosttyFormatterTerminalOptions opts = GHOSTTY_INIT_SIZED(GhosttyFormatterTerminalOptions); + * opts.emit = GHOSTTY_FORMATTER_FORMAT_PLAIN; + * opts.trim = true; + * @endcode + */ +#define GHOSTTY_INIT_SIZED(type) \ + ((type){ .size = sizeof(type) }) + +#endif /* GHOSTTY_VT_TYPES_H */ diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml new file mode 100644 index 00000000000..d2b371cc1d1 --- /dev/null +++ b/macos/.swiftlint.yml @@ -0,0 +1,36 @@ +# SwiftLint +# +check_for_updates: false + +excluded: + - build + +disabled_rules: + - cyclomatic_complexity + - file_length + - function_body_length + - line_length + - nesting + - no_fallthrough_only + - todo + - trailing_comma + - trailing_newline + - type_body_length + +identifier_name: + min_length: 1 + allowed_symbols: ["_"] + excluded: + - Core.* + +type_name: + min_length: 2 + allowed_symbols: ["_"] + excluded: + - iOS_.* + +function_parameter_count: + warning: 6 + +large_tuple: + warning: 3 diff --git a/macos/AGENTS.md b/macos/AGENTS.md new file mode 100644 index 00000000000..1a0c84c3280 --- /dev/null +++ b/macos/AGENTS.md @@ -0,0 +1,34 @@ +# macOS Ghostty Application + +- Use `swiftlint` for formatting and linting Swift code. +- If code outside of `macos/` directory is modified, use + `zig build -Demit-macos-app=false` before building the macOS app to update + the underlying Ghostty library. +- Use `macos/build.nu` to build the macOS app, do not use `zig build` + (except to build the underlying library as mentioned above). + - Build: `macos/build.nu [--scheme Ghostty] [--configuration Debug] [--action build]` + - Output: `macos/build//Ghostty.app` (e.g. `macos/build/Debug/Ghostty.app`) +- Run unit tests directly with `macos/build.nu --action test` + +## AppleScript + +- The AppleScript scripting definition is in `macos/Ghostty.sdef`. +- Guard AppleScript entry points and object accessors with the + `macos-applescript` configuration (use `NSApp.isAppleScriptEnabled` + and `NSApp.validateScript(command:)` where applicable). +- In `macos/Ghostty.sdef`, keep top-level definitions in this order: + 1. Classes + 2. Records + 3. Enums + 4. Commands +- Test AppleScript support: + (1) Build with `macos/build.nu` + (2) Launch and activate the app via osascript using the absolute path + to the built app bundle: + `osascript -e 'tell application "" to activate'` + (3) Wait a few seconds for the app to fully launch and open a terminal. + (4) Run test scripts with `osascript`, always targeting the app by + its absolute path (not by name) to avoid calling the wrong + application. + (5) When done, quit via: + `osascript -e 'tell application "" to quit'` diff --git a/macos/Ghostty-Info.plist b/macos/Ghostty-Info.plist index 5960dc0e77e..7ffe12c3944 100644 --- a/macos/Ghostty-Info.plist +++ b/macos/Ghostty-Info.plist @@ -2,6 +2,10 @@ + NSAutoFillRequiresTextContentTypeForOneTimeCodeOnMac + + NSDockTilePlugIn + DockTilePlugin.plugin CFBundleDocumentTypes @@ -53,8 +57,12 @@ MDItemKeywords Terminal + NSAppleScriptEnabled + NSHighResolutionCapable + OSAScriptingDefinition + Ghostty.sdef NSServices diff --git a/macos/Ghostty.sdef b/macos/Ghostty.sdef new file mode 100644 index 00000000000..bdfc501fb51 --- /dev/null +++ b/macos/Ghostty.sdef @@ -0,0 +1,318 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index ab6dde118cb..3758c325d94 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -10,6 +10,9 @@ 29C15B1D2CDC3B2900520DD4 /* bat in Resources */ = {isa = PBXBuildFile; fileRef = 29C15B1C2CDC3B2000520DD4 /* bat */; }; 55154BE02B33911F001622DC /* ghostty in Resources */ = {isa = PBXBuildFile; fileRef = 55154BDF2B33911F001622DC /* ghostty */; }; 552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; }; + 819324582F24E78800A9ED8F /* DockTilePlugin.plugin in Copy DockTilePlugin */ = {isa = PBXBuildFile; fileRef = 8193244D2F24E6C000A9ED8F /* DockTilePlugin.plugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 819324642F24FF2100A9ED8F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; + 8F3A9B4C2FA6B88000A18D13 /* Ghostty.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 8F3A9B4B2FA6B88000A18D13 /* Ghostty.sdef */; }; 9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; }; A51BFC272B30F1B800E92F16 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A51BFC262B30F1B800E92F16 /* Sparkle */; }; A53D0C8E2B53B0EA00305CE6 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; @@ -35,6 +38,13 @@ remoteGlobalIDString = A5B30530299BEAAA0047F10C; remoteInfo = Ghostty; }; + 819324672F2502FB00A9ED8F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A5B30529299BEAAA0047F10C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8193244C2F24E6C000A9ED8F; + remoteInfo = DockTilePlugin; + }; A54F45F72E1F047A0046BD5C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = A5B30529299BEAAA0047F10C /* Project object */; @@ -44,12 +54,28 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 819324572F24E74E00A9ED8F /* Copy DockTilePlugin */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 819324582F24E78800A9ED8F /* DockTilePlugin.plugin in Copy DockTilePlugin */, + ); + name = "Copy DockTilePlugin"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 29C15B1C2CDC3B2000520DD4 /* bat */ = {isa = PBXFileReference; lastKnownFileType = folder; name = bat; path = "../zig-out/share/bat"; sourceTree = ""; }; 3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyReleaseLocal.entitlements; sourceTree = ""; }; 55154BDF2B33911F001622DC /* ghostty */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ghostty; path = "../zig-out/share/ghostty"; sourceTree = ""; }; 552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = ""; }; 810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GhosttyUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 8193244D2F24E6C000A9ED8F /* DockTilePlugin.plugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DockTilePlugin.plugin; sourceTree = BUILT_PRODUCTS_DIR; }; + 8F3A9B4B2FA6B88000A18D13 /* Ghostty.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.sdef; path = Ghostty.sdef; sourceTree = ""; }; 9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = ""; }; A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyDebug.entitlements; sourceTree = ""; }; A546F1132D7B68D7003B11A0 /* locale */ = {isa = PBXFileReference; lastKnownFileType = folder; name = locale; path = "../zig-out/share/locale"; sourceTree = ""; }; @@ -70,6 +96,22 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 8193245D2F24E80800A9ED8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + "Features/Custom App Icon/AppIcon.swift", + "Features/Custom App Icon/ColorizedGhosttyIcon.swift", + "Features/Custom App Icon/DockTilePlugin.swift", + "Features/Custom App Icon/Extensions/Notification+AppIcon.swift", + "Features/Custom App Icon/Extensions/UserDefaults+AppIcon.swift", + Ghostty/Ghostty.ConfigTypes.swift, + Ghostty/GhosttyPackageMeta.swift, + Helpers/CrossKit.swift, + "Helpers/Extensions/NSImage+Extension.swift", + "Helpers/Extensions/OSColor+Extension.swift", + ); + target = 8193244C2F24E6C000A9ED8F /* DockTilePlugin */; + }; 81F82CB02E8281F5001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -80,6 +122,7 @@ Features/About/About.xib, Features/About/AboutController.swift, Features/About/AboutView.swift, + Features/About/AboutViewModel.swift, Features/About/CyclingIconView.swift, "Features/App Intents/CloseTerminalIntent.swift", "Features/App Intents/CommandPaletteIntent.swift", @@ -93,14 +136,30 @@ "Features/App Intents/KeybindIntent.swift", "Features/App Intents/NewTerminalIntent.swift", "Features/App Intents/QuickTerminalIntent.swift", + "Features/AppleScript/AppDelegate+AppleScript.swift", + "Features/AppleScript/Ghostty.Input.Mods+AppleScript.swift", + Features/AppleScript/ScriptInputTextCommand.swift, + Features/AppleScript/ScriptKeyEventCommand.swift, + Features/AppleScript/ScriptMouseButtonCommand.swift, + Features/AppleScript/ScriptMousePosCommand.swift, + Features/AppleScript/ScriptMouseScrollCommand.swift, + Features/AppleScript/ScriptRecord.swift, + Features/AppleScript/ScriptSurfaceConfiguration.swift, + Features/AppleScript/ScriptTab.swift, + Features/AppleScript/ScriptTerminal.swift, + Features/AppleScript/ScriptWindow.swift, Features/ClipboardConfirmation/ClipboardConfirmation.xib, Features/ClipboardConfirmation/ClipboardConfirmationController.swift, Features/ClipboardConfirmation/ClipboardConfirmationView.swift, - "Features/Colorized Ghostty Icon/ColorizedGhosttyIcon.swift", - "Features/Colorized Ghostty Icon/ColorizedGhosttyIconImage.swift", - "Features/Colorized Ghostty Icon/ColorizedGhosttyIconView.swift", "Features/Command Palette/CommandPalette.swift", "Features/Command Palette/TerminalCommandPalette.swift", + "Features/Custom App Icon/AppIcon.swift", + "Features/Custom App Icon/ColorizedGhosttyIcon.swift", + "Features/Custom App Icon/ColorizedGhosttyIconImage.swift", + "Features/Custom App Icon/ColorizedGhosttyIconView.swift", + "Features/Custom App Icon/DockTilePlugin.swift", + "Features/Custom App Icon/Extensions/Notification+AppIcon.swift", + "Features/Custom App Icon/Extensions/UserDefaults+AppIcon.swift", "Features/Global Keybinds/GlobalEventTap.swift", Features/QuickTerminal/QuickTerminal.xib, Features/QuickTerminal/QuickTerminalController.swift, @@ -186,12 +245,13 @@ Helpers/LastWindowPosition.swift, Helpers/MetalView.swift, Helpers/NonDraggableHostingView.swift, + Helpers/ObjCExceptionCatcher.m, Helpers/PermissionRequest.swift, Helpers/Private/CGS.swift, Helpers/Private/Dock.swift, Helpers/TabGroupCloseCoordinator.swift, + Helpers/TabTitleEditor.swift, Helpers/VibrantLayer.m, - Helpers/Weak.swift, ); target = A5D4499C2B53AE7B000F5B83 /* Ghostty-iOS */; }; @@ -199,6 +259,7 @@ isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( App/iOS/iOSApp.swift, + "Features/Custom App Icon/DockTilePlugin.swift", "Ghostty/Surface View/SurfaceView_UIKit.swift", ); target = A5B30530299BEAAA0047F10C /* Ghostty */; @@ -207,7 +268,7 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ 810ACCA02E9D3302004F8F92 /* GhosttyUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = GhosttyUITests; sourceTree = ""; }; - 81F82BC72E82815D001EDFA7 /* Sources */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (81F82CB12E8281F9001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 81F82CB02E8281F5001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Sources; sourceTree = ""; }; + 81F82BC72E82815D001EDFA7 /* Sources */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (81F82CB12E8281F9001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 81F82CB02E8281F5001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 8193245D2F24E80800A9ED8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Sources; sourceTree = ""; }; A54F45F42E1F047A0046BD5C /* Tests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Tests; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ @@ -219,6 +280,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 8193244A2F24E6C000A9ED8F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; A54F45F02E1F047A0046BD5C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -268,6 +336,7 @@ isa = PBXGroup; children = ( A571AB1C2A206FC600248498 /* Ghostty-Info.plist */, + 8F3A9B4B2FA6B88000A18D13 /* Ghostty.sdef */, A5B30538299BEAAB0047F10C /* Assets.xcassets */, A553F4122E06EB1600257779 /* Ghostty.icon */, A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */, @@ -289,6 +358,7 @@ A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */, A54F45F32E1F047A0046BD5C /* GhosttyTests.xctest */, 810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */, + 8193244D2F24E6C000A9ED8F /* DockTilePlugin.plugin */, ); name = Products; sourceTree = ""; @@ -328,6 +398,25 @@ productReference = 810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + 8193244C2F24E6C000A9ED8F /* DockTilePlugin */ = { + isa = PBXNativeTarget; + buildConfigurationList = 819324512F24E6C000A9ED8F /* Build configuration list for PBXNativeTarget "DockTilePlugin" */; + buildPhases = ( + 819324492F24E6C000A9ED8F /* Sources */, + 8193244A2F24E6C000A9ED8F /* Frameworks */, + 8193244B2F24E6C000A9ED8F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = DockTilePlugin; + packageProductDependencies = ( + ); + productName = DockTilePlugin; + productReference = 8193244D2F24E6C000A9ED8F /* DockTilePlugin.plugin */; + productType = "com.apple.product-type.bundle"; + }; A54F45F22E1F047A0046BD5C /* GhosttyTests */ = { isa = PBXNativeTarget; buildConfigurationList = A54F45FC2E1F047A0046BD5C /* Build configuration list for PBXNativeTarget "GhosttyTests" */; @@ -355,13 +444,16 @@ isa = PBXNativeTarget; buildConfigurationList = A5B30540299BEAAB0047F10C /* Build configuration list for PBXNativeTarget "Ghostty" */; buildPhases = ( + FC501E0B2F46B410007AE49D /* Run SwiftLint */, A5B3052D299BEAAA0047F10C /* Sources */, A5B3052E299BEAAA0047F10C /* Frameworks */, A5B3052F299BEAAA0047F10C /* Resources */, + 819324572F24E74E00A9ED8F /* Copy DockTilePlugin */, ); buildRules = ( ); dependencies = ( + 819324682F2502FB00A9ED8F /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 81F82BC72E82815D001EDFA7 /* Sources */, @@ -408,6 +500,9 @@ CreatedOnToolsVersion = 26.1; TestTargetID = A5B30530299BEAAA0047F10C; }; + 8193244C2F24E6C000A9ED8F = { + CreatedOnToolsVersion = 26.2; + }; A54F45F22E1F047A0046BD5C = { CreatedOnToolsVersion = 26.0; TestTargetID = A5B30530299BEAAA0047F10C; @@ -439,6 +534,7 @@ targets = ( A5B30530299BEAAA0047F10C /* Ghostty */, A5D4499C2B53AE7B000F5B83 /* Ghostty-iOS */, + 8193244C2F24E6C000A9ED8F /* DockTilePlugin */, A54F45F22E1F047A0046BD5C /* GhosttyTests */, 810ACC9E2E9D3301004F8F92 /* GhosttyUITests */, ); @@ -453,6 +549,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 8193244B2F24E6C000A9ED8F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 819324642F24FF2100A9ED8F /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; A54F45F12E1F047A0046BD5C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -468,6 +572,7 @@ A553F4142E06EB1600257779 /* Ghostty.icon in Resources */, 29C15B1D2CDC3B2900520DD4 /* bat in Resources */, A586167C2B7703CC009BDB1D /* fish in Resources */, + 8F3A9B4C2FA6B88000A18D13 /* Ghostty.sdef in Resources */, 55154BE02B33911F001622DC /* ghostty in Resources */, A546F1142D7B68D7003B11A0 /* locale in Resources */, A5985CE62C33060F00C57AD3 /* man in Resources */, @@ -490,6 +595,29 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + FC501E0B2F46B410007AE49D /* Run SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run SwiftLint"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "[[ -z \"$GITHUB_ACTIONS\" ]] || exit 0;\n\nSWIFTLINT=\"\"\nif command -v swiftlint >/dev/null 2>&1; then\n SWIFTLINT=\"$(command -v swiftlint)\"\nelif [[ -f \"/opt/homebrew/bin/swiftlint\" ]]; then\n SWIFTLINT=\"/opt/homebrew/bin/swiftlint\"\nfi\n\nif [[ -n \"$SWIFTLINT\" ]]; then\n \"$SWIFTLINT\" lint --quiet\nfi\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 810ACC9B2E9D3301004F8F92 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -498,6 +626,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 819324492F24E6C000A9ED8F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; A54F45EF2E1F047A0046BD5C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -527,6 +662,11 @@ target = A5B30530299BEAAA0047F10C /* Ghostty */; targetProxy = 810ACCA52E9D3302004F8F92 /* PBXContainerItemProxy */; }; + 819324682F2502FB00A9ED8F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8193244C2F24E6C000A9ED8F /* DockTilePlugin */; + targetProxy = 819324672F2502FB00A9ED8F /* PBXContainerItemProxy */; + }; A54F45F82E1F047A0046BD5C /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = A5B30530299BEAAA0047F10C /* Ghostty */; @@ -659,7 +799,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; @@ -682,6 +822,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; @@ -704,6 +845,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; @@ -712,6 +854,93 @@ }; name = ReleaseLocal; }; + 8193244E2F24E6C000A9ED8F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "Ghostty Dock Tile Plugin"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSPrincipalClass = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 0.1; + PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-dock-tile"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DOCK_TILE_PLUGIN DEBUG"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + WRAPPER_EXTENSION = plugin; + }; + name = Debug; + }; + 8193244F2F24E6C000A9ED8F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "Ghostty Dock Tile Plugin"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSPrincipalClass = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 0.1; + PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-dock-tile"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DOCK_TILE_PLUGIN; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + WRAPPER_EXTENSION = plugin; + }; + name = Release; + }; + 819324502F24E6C000A9ED8F /* ReleaseLocal */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "Ghostty Dock Tile Plugin"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSPrincipalClass = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 0.1; + PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-dock-tile"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DOCK_TILE_PLUGIN; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + WRAPPER_EXTENSION = plugin; + }; + name = ReleaseLocal; + }; A54F45F92E1F047A0046BD5C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1138,6 +1367,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = ReleaseLocal; }; + 819324512F24E6C000A9ED8F /* Build configuration list for PBXNativeTarget "DockTilePlugin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8193244E2F24E6C000A9ED8F /* Debug */, + 8193244F2F24E6C000A9ED8F /* Release */, + 819324502F24E6C000A9ED8F /* ReleaseLocal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseLocal; + }; A54F45FC2E1F047A0046BD5C /* Build configuration list for PBXNativeTarget "GhosttyTests" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 89573fb88e4..6e450d9bcef 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" : "9a1d2a19d3595fcf8d9c447173f9a1687b3dcadb", - "version" : "2.8.0" + "revision" : "21d8df80440b1ca3b65fa82e40782f1e5a9e6ba2", + "version" : "2.9.0" } } ], diff --git a/macos/GhosttyUITests/GhosttyCustomConfigCase.swift b/macos/GhosttyUITests/GhosttyCustomConfigCase.swift index 41993247ab7..ca3f5667782 100644 --- a/macos/GhosttyUITests/GhosttyCustomConfigCase.swift +++ b/macos/GhosttyUITests/GhosttyCustomConfigCase.swift @@ -27,6 +27,8 @@ class GhosttyCustomConfigCase: XCTestCase { true } + static let defaultsSuiteName: String = "GHOSTTY_UI_TESTS" + var configFile: URL? override func setUpWithError() throws { continueAfterFailure = false @@ -47,13 +49,14 @@ class GhosttyCustomConfigCase: XCTestCase { try newConfig.write(to: configFile!, atomically: true, encoding: .utf8) } - func ghosttyApplication() throws -> XCUIApplication { + func ghosttyApplication(defaultsSuite: String = GhosttyCustomConfigCase.defaultsSuiteName) throws -> XCUIApplication { let app = XCUIApplication() app.launchArguments.append(contentsOf: ["-ApplePersistenceIgnoreState", "YES"]) guard let configFile else { return app } app.launchEnvironment["GHOSTTY_CONFIG_PATH"] = configFile.path + app.launchEnvironment["GHOSTTY_USER_DEFAULTS_SUITE"] = defaultsSuite return app } } diff --git a/macos/GhosttyUITests/GhosttyWindowPositionUITests.swift b/macos/GhosttyUITests/GhosttyWindowPositionUITests.swift new file mode 100644 index 00000000000..399c2531a4d --- /dev/null +++ b/macos/GhosttyUITests/GhosttyWindowPositionUITests.swift @@ -0,0 +1,331 @@ +// +// GhosttyWindowPositionUITests.swift +// GhosttyUITests +// +// Created by Claude on 2026-03-11. +// + +import XCTest + +final class GhosttyWindowPositionUITests: GhosttyCustomConfigCase { + override static var runsForEachTargetApplicationUIConfiguration: Bool { false } + + // MARK: - Cascading + + @MainActor func testWindowCascading() async throws { + try updateConfig( + """ + window-width = 30 + window-height = 10 + title = "GhosttyWindowPositionUITests" + """ + ) + + let app = try ghosttyApplication() + // Suppress Restoration + app.launchArguments += ["-NSQuitAlwaysKeepsWindows", "NO"] + // Clean run + app.launchEnvironment["GHOSTTY_CLEAR_USER_DEFAULTS"] = "YES" + + app.launch() // window in the center + +// app.menuBarItems["Window"].firstMatch.click() +// app.menuItems["_zoomTopLeft:"].firstMatch.click() +// +// // wait for the animation to finish +// try await Task.sleep(for: .seconds(0.5)) + + let window = app.windows.firstMatch + let windowFrame = window.frame +// XCTAssertEqual(windowFrame.minX, 0, "Window should be on the left") + + app.typeKey("n", modifierFlags: [.command]) + + let window2 = app.windows.firstMatch + XCTAssertTrue(window2.waitForExistence(timeout: 5), "New window should appear") + let windowFrame2 = window2.frame + XCTAssertNotEqual(windowFrame, windowFrame2, "New window should have moved") + + XCTAssertEqual(windowFrame2.minX, windowFrame.minX + 30, accuracy: 5, "New window should be on the right") + + XCTAssertEqual(windowFrame2.minY, windowFrame.minY + 30, accuracy: 5, "New window should be on the bottom right") + + app.typeKey("n", modifierFlags: [.command]) + + let window3 = app.windows.firstMatch + XCTAssertTrue(window3.waitForExistence(timeout: 5), "New window should appear") + let windowFrame3 = window3.frame + XCTAssertNotEqual(windowFrame2, windowFrame3, "New window should have moved") + + XCTAssertEqual(windowFrame3.minX, windowFrame2.minX + 30, accuracy: 5, "New window should be on the right") + + XCTAssertEqual(windowFrame3.minY, windowFrame2.minY + 30, accuracy: 5, "New window should be on the bottom right") + + app.typeKey("n", modifierFlags: [.command]) + + let window4 = app.windows.firstMatch + XCTAssertTrue(window4.waitForExistence(timeout: 5), "New window should appear") + let windowFrame4 = window4.frame + XCTAssertNotEqual(windowFrame3, windowFrame4, "New window should have moved") + + XCTAssertEqual(windowFrame4.minX, windowFrame3.minX + 30, accuracy: 5, "New window should be on the right") + + XCTAssertEqual(windowFrame4.minY, windowFrame3.minY + 30, accuracy: 5, "New window should be on the bottom right") + } + + @MainActor func testDragSplitWindowPosition() async throws { + try updateConfig( + """ + window-width = 40 + window-height = 20 + title = "GhosttyWindowPositionUITests" + macos-titlebar-style = hidden + """ + ) + + let app = try ghosttyApplication() + // Suppress Restoration + app.launchArguments += ["-NSQuitAlwaysKeepsWindows", "NO"] + // Clean run + app.launchEnvironment["GHOSTTY_CLEAR_USER_DEFAULTS"] = "YES" + + app.launch() // window in the center + + let window = app.windows.firstMatch + XCTAssertTrue(window.waitForExistence(timeout: 5), "New window should appear") + + // remove fixed size + try updateConfig( + """ + title = "GhosttyWindowPositionUITests" + macos-titlebar-style = hidden + """ + ) + app.typeKey(",", modifierFlags: [.command, .shift]) + + app.typeKey("d", modifierFlags: [.command]) + + let rightSplit = app.groups["Right pane"] + let rightFrame = rightSplit.frame + + let sourcePos = rightSplit.coordinate(withNormalizedOffset: .zero) + .withOffset(.init(dx: rightFrame.size.width / 2, dy: 3)) + + let targetPos = rightSplit.coordinate(withNormalizedOffset: .zero) + .withOffset(.init(dx: rightFrame.size.width + 100, dy: 0)) + + sourcePos.click(forDuration: 0.2, thenDragTo: targetPos) + + let window2 = app.windows.firstMatch + XCTAssertTrue(window2.waitForExistence(timeout: 5), "New window should appear") + let windowFrame2 = window2.frame + + try await Task.sleep(for: .seconds(0.5)) + + XCTAssertEqual(windowFrame2.minX, rightFrame.maxX + 100, accuracy: 5, "New window should be target position") + XCTAssertEqual(windowFrame2.minY, rightFrame.minY, accuracy: 5, "New window should be target position") + XCTAssertEqual(windowFrame2.width, rightFrame.width, accuracy: 5, "New window should use size from config") + XCTAssertEqual(windowFrame2.height, rightFrame.height, accuracy: 5, "New window should use size from config") + } + + @MainActor func testDragSplitWindowPositionWithFixedSize() async throws { + try updateConfig( + """ + window-width = 40 + window-height = 20 + title = "GhosttyWindowPositionUITests" + macos-titlebar-style = hidden + """ + ) + + let app = try ghosttyApplication() + // Suppress Restoration + app.launchArguments += ["-NSQuitAlwaysKeepsWindows", "NO"] + // Clean run + app.launchEnvironment["GHOSTTY_CLEAR_USER_DEFAULTS"] = "YES" + + app.launch() // window in the center + + let window = app.windows.firstMatch + XCTAssertTrue(window.waitForExistence(timeout: 5), "New window should appear") + let windowFrame = window.frame + + app.typeKey("d", modifierFlags: [.command]) + + let rightSplit = app.groups["Right pane"] + let rightFrame = rightSplit.frame + + let sourcePos = rightSplit.coordinate(withNormalizedOffset: .zero) + .withOffset(.init(dx: rightFrame.size.width / 2, dy: 3)) + + let targetPos = rightSplit.coordinate(withNormalizedOffset: .zero) + .withOffset(.init(dx: rightFrame.size.width + 100, dy: 0)) + + sourcePos.click(forDuration: 0.2, thenDragTo: targetPos) + + let window2 = app.windows.firstMatch + XCTAssertTrue(window2.waitForExistence(timeout: 5), "New window should appear") + let windowFrame2 = window2.frame + + try await Task.sleep(for: .seconds(0.5)) + + XCTAssertEqual(windowFrame2.minX, rightFrame.maxX + 100, accuracy: 5, "New window should be target position") + XCTAssertEqual(windowFrame2.minY, rightFrame.minY, accuracy: 5, "New window should be target position") + XCTAssertEqual(windowFrame2.width, windowFrame.width, accuracy: 5, "New window should use size from config") + // We're still using right frame, because of the debug banner + XCTAssertEqual(windowFrame2.height, rightFrame.height, accuracy: 5, "New window should use size from config") + } + + // MARK: - Restore round-trip per titlebar style + + @MainActor func testRestoredNative() throws { try runRestoreTest(titlebarStyle: "native") } + @MainActor func testRestoredHidden() throws { try runRestoreTest(titlebarStyle: "hidden") } + @MainActor func testRestoredTransparent() throws { try runRestoreTest(titlebarStyle: "transparent") } + @MainActor func testRestoredTabs() throws { try runRestoreTest(titlebarStyle: "tabs") } + + // MARK: - Config overrides cached position/size + + @MainActor + func testConfigOverridesCachedPositionAndSize() async throws { + // Launch maximized so the cached frame is fullscreen-sized. + try updateConfig( + """ + maximize = true + title = "GhosttyWindowPositionUITests" + """ + ) + + let app = try ghosttyApplication() + app.launch() + + let window = app.windows.firstMatch + XCTAssertTrue(window.waitForExistence(timeout: 5), "Window should appear") + + let maximizedFrame = window.frame + + // Now update the config with a small explicit size and position, + // reload, and open a new window. It should respect the config, not the cache. + try updateConfig( + """ + window-position-x = 50 + window-position-y = 50 + window-width = 30 + window-height = 30 + title = "GhosttyWindowPositionUITests" + """ + ) + app.typeKey(",", modifierFlags: [.command, .shift]) + try await Task.sleep(for: .seconds(0.5)) + app.typeKey("n", modifierFlags: [.command]) + + XCTAssertEqual(app.windows.count, 2, "Should have 2 windows") + let newWindow = app.windows.element(boundBy: 0) + let newFrame = newWindow.frame + + // The new window should be smaller than the maximized one. + XCTAssertLessThan(newFrame.size.width, maximizedFrame.size.width, + "30 columns should be narrower than maximized") + XCTAssertLessThan(newFrame.size.height, maximizedFrame.size.height, + "30 rows should be shorter than maximized") + + app.terminate() + } + + // MARK: - Size-only config change preserves position + + @MainActor + func testSizeOnlyConfigPreservesPosition() async throws { + // Launch maximized so the window has a known position (top-left of visible frame). + try updateConfig( + """ + maximize = true + title = "GhosttyWindowPositionUITests" + """ + ) + + let app = try ghosttyApplication() + app.launch() + + let window = app.windows.firstMatch + XCTAssertTrue(window.waitForExistence(timeout: 5), "Window should appear") + + let initialFrame = window.frame + + // Reload with only size changed, close current window, open new one. + // Position should be restored from cache. + try updateConfig( + """ + window-width = 30 + window-height = 30 + title = "GhosttyWindowPositionUITests" + """ + ) + app.typeKey(",", modifierFlags: [.command, .shift]) + try await Task.sleep(for: .seconds(0.5)) + app.typeKey("w", modifierFlags: [.command]) + app.typeKey("n", modifierFlags: [.command]) + + let newWindow = app.windows.firstMatch + XCTAssertTrue(newWindow.waitForExistence(timeout: 5), "New window should appear") + + let newFrame = newWindow.frame + + // Position should be preserved from the cached value. + // Compare x and maxY since the window is anchored at the top-left + // but AppKit uses bottom-up coordinates (origin.y changes with height). + XCTAssertEqual(newFrame.origin.x, initialFrame.origin.x, accuracy: 2, + "x position should not change with size-only config") + XCTAssertEqual(newFrame.maxY, initialFrame.maxY, accuracy: 2, + "top edge (maxY) should not change with size-only config") + + app.terminate() + } + + // MARK: - Shared round-trip helper + + /// Opens a new window, records its frame, closes it, opens another, + /// and verifies the frame is restored consistently. + private func runRestoreTest(titlebarStyle: String) throws { + try updateConfig( + """ + macos-titlebar-style = \(titlebarStyle) + title = "GhosttyWindowPositionUITests" + """ + ) + + let app = try ghosttyApplication() + // Suppress Restoration + app.launchArguments += ["-NSQuitAlwaysKeepsWindows", "NO"] + // Clean run + app.launchEnvironment["GHOSTTY_CLEAR_USER_DEFAULTS"] = "YES" + app.launch() + + let window = app.windows.firstMatch + XCTAssertTrue(window.waitForExistence(timeout: 5), "Window should appear") + + let firstFrame = window.frame + let screenFrame = NSScreen.main?.frame ?? .zero + + XCTAssertEqual(firstFrame.midX, screenFrame.midX, accuracy: 5.0, "First window should be centered horizontally") + + // Close the window and open a new one — it should restore the same frame. + app.typeKey("w", modifierFlags: [.command]) + app.typeKey("n", modifierFlags: [.command]) + + let window2 = app.windows.firstMatch + XCTAssertTrue(window2.waitForExistence(timeout: 5), "New window should appear") + + let restoredFrame = window2.frame + + XCTAssertEqual(restoredFrame.origin.x, firstFrame.origin.x, accuracy: 2, + "[\(titlebarStyle)] x position should be restored") + XCTAssertEqual(restoredFrame.origin.y, firstFrame.origin.y, accuracy: 2, + "[\(titlebarStyle)] y position should be restored") + XCTAssertEqual(restoredFrame.size.width, firstFrame.size.width, accuracy: 2, + "[\(titlebarStyle)] width should be restored") + XCTAssertEqual(restoredFrame.size.height, firstFrame.size.height, accuracy: 2, + "[\(titlebarStyle)] height should be restored") + + app.terminate() + } +} diff --git a/macos/Sources/App/macOS/AppDelegate+Ghostty.swift b/macos/Sources/App/macOS/AppDelegate+Ghostty.swift index 4d798a1a5d9..fc9a4906746 100644 --- a/macos/Sources/App/macOS/AppDelegate+Ghostty.swift +++ b/macos/Sources/App/macOS/AppDelegate+Ghostty.swift @@ -10,14 +10,12 @@ extension AppDelegate: Ghostty.Delegate { guard let controller = window.windowController as? BaseTerminalController else { continue } - - for surface in controller.surfaceTree { - if surface.id == id { - return surface - } + + for surface in controller.surfaceTree where surface.id == id { + return surface } } - + return nil } } diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 0db39a09e69..b02337e4b20 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -9,8 +9,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, UNUserNotificationCenterDelegate, - GhosttyAppDelegate -{ + GhosttyAppDelegate { // The application logger. We should probably move this at some point to a dedicated // class/struct but for now it lives here! 🤷â€â™‚ï¸ static let logger = Logger( @@ -65,6 +64,7 @@ class AppDelegate: NSObject, @IBOutlet private var menuReturnToDefaultSize: NSMenuItem? @IBOutlet private var menuFloatOnTop: NSMenuItem? @IBOutlet private var menuUseAsDefault: NSMenuItem? + @IBOutlet private var menuSetAsDefaultTerminal: NSMenuItem? @IBOutlet private var menuIncreaseFontSize: NSMenuItem? @IBOutlet private var menuDecreaseFontSize: NSMenuItem? @@ -109,7 +109,7 @@ class AppDelegate: NSObject, switch quickTerminalControllerState { case .initialized(let controller): return controller - + case .pendingRestore(let state): let controller = QuickTerminalController( ghostty, @@ -119,7 +119,7 @@ class AppDelegate: NSObject, ) quickTerminalControllerState = .initialized(controller) return controller - + case .uninitialized: let controller = QuickTerminalController( ghostty, @@ -143,16 +143,23 @@ class AppDelegate: NSObject, } /// Tracks the windows that we hid for toggleVisibility. - private(set) var hiddenState: ToggleVisibilityState? = nil + private(set) var hiddenState: ToggleVisibilityState? /// The observer for the app appearance. - private var appearanceObserver: NSKeyValueObservation? = nil + private var appearanceObserver: NSKeyValueObservation? /// Signals private var signals: [DispatchSourceSignal] = [] /// The custom app icon image that is currently in use. - @Published private(set) var appIcon: NSImage? = nil + @Published private(set) var appIcon: NSImage? + + /// Ghostty menu items indexed by their normalized shortcut. This avoids traversing + /// the entire menu tree on every key equivalent event. + /// + /// We store a weak reference so this cache can never be the owner of menu items. + /// If multiple items map to the same shortcut, the most recent one wins. + private var menuItemsByShortcut: [MenuShortcutKey: Weak] = [:] override init() { #if DEBUG @@ -165,14 +172,22 @@ class AppDelegate: NSObject, ghostty.delegate = self } - //MARK: - NSApplicationDelegate + // MARK: - NSApplicationDelegate func applicationWillFinishLaunching(_ notification: Notification) { - UserDefaults.standard.register(defaults: [ + #if DEBUG + if + let suite = UserDefaults.ghosttySuite, + let clear = ProcessInfo.processInfo.environment["GHOSTTY_CLEAR_USER_DEFAULTS"], + (clear as NSString).boolValue { + UserDefaults.ghostty.removePersistentDomain(forName: suite) + } + #endif + UserDefaults.ghostty.register(defaults: [ // Disable the automatic full screen menu item because we handle // it manually. "NSFullScreenMenuItemEverywhere": false, - + // On macOS 26 RC1, the autofill heuristic controller causes unusable levels // of slowdowns and CPU usage in the terminal window under certain [unknown] // conditions. We don't know exactly why/how. This disables the full heuristic @@ -187,7 +202,7 @@ class AppDelegate: NSObject, func applicationDidFinishLaunching(_ notification: Notification) { // System settings overrides - UserDefaults.standard.register(defaults: [ + UserDefaults.ghostty.register(defaults: [ // Disable this so that repeated key events make it through to our terminal views. "ApplePressAndHoldEnabled": false, ]) @@ -196,7 +211,7 @@ class AppDelegate: NSObject, applicationLaunchTime = ProcessInfo.processInfo.systemUptime // Check if secure input was enabled when we last quit. - if (UserDefaults.standard.bool(forKey: "SecureInput") != SecureInput.shared.enabled) { + if UserDefaults.ghostty.bool(forKey: "SecureInput") != SecureInput.shared.enabled { toggleSecureInput(self) } @@ -243,6 +258,12 @@ class AppDelegate: NSObject, name: .ghosttyBellDidRing, object: nil ) + NotificationCenter.default.addObserver( + self, + selector: #selector(terminalWindowHasBell(_:)), + name: .terminalWindowBellDidChangeNotification, + object: nil + ) NotificationCenter.default.addObserver( self, selector: #selector(ghosttyNewWindow(_:)), @@ -279,7 +300,7 @@ class AppDelegate: NSObject, guard let appearance = change.newValue else { return } guard let app = self.ghostty.app else { return } let scheme: ghostty_color_scheme_e - if (appearance.isDark) { + if appearance.isDark { scheme = GHOSTTY_COLOR_SCHEME_DARK } else { scheme = GHOSTTY_COLOR_SCHEME_LIGHT @@ -298,12 +319,12 @@ class AppDelegate: NSObject, case .app: // Don't have to do anything. break - + case .zig_run, .cli: // Part of launch services (clicking an app, using `open`, etc.) activates // the application and brings it to the front. When using the CLI we don't // get this behavior, so we have to do it manually. - + // This never gets called until we click the dock icon. This forces it // activate immediately. applicationDidBecomeActive(.init(name: NSApplication.didBecomeActiveNotification)) @@ -327,11 +348,8 @@ class AppDelegate: NSObject, // If we're back manually then clear the hidden state because macOS handles it. self.hiddenState = nil - // Clear the dock badge when the app becomes active - self.setDockBadge(nil) - // First launch stuff - if (!applicationHasBecomeActive) { + if !applicationHasBecomeActive { applicationHasBecomeActive = true // Let's launch our first window. We only do this if we have no other windows. It @@ -352,8 +370,8 @@ class AppDelegate: NSObject, func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { let windows = NSApplication.shared.windows - if (windows.isEmpty) { return .terminateNow } - + 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 { @@ -379,14 +397,8 @@ class AppDelegate: NSObject, guard let keyword = AEKeyword("why?") else { break why } if let why = event.attributeDescriptor(forKeyword: keyword) { - switch (why.typeCodeValue) { - case kAEShutDown: - fallthrough - - case kAERestart: - fallthrough - - case kAEReallyLogOut: + switch why.typeCodeValue { + case kAEShutDown, kAERestart, kAEReallyLogOut: return .terminateNow default: @@ -396,7 +408,7 @@ class AppDelegate: NSObject, } // If our app says we don't need to confirm, we can exit now. - if (!ghostty.needsConfirmQuit) { return .terminateNow } + if !ghostty.needsConfirmQuit { return .terminateNow } // We have some visible window. Show an app-wide modal to confirm quitting. let alert = NSAlert() @@ -405,7 +417,7 @@ class AppDelegate: NSObject, alert.addButton(withTitle: "Close Ghostty") alert.addButton(withTitle: "Cancel") alert.alertStyle = .warning - switch (alert.runModal()) { + switch alert.runModal() { case .alertFirstButtonReturn: return .terminateNow @@ -448,18 +460,18 @@ class AppDelegate: NSObject, // Ghostty will validate as well but we can avoid creating an entirely new // surface by doing our own validation here. We can also show a useful error // this way. - + var isDirectory = ObjCBool(true) guard FileManager.default.fileExists(atPath: filename, isDirectory: &isDirectory) else { return false } - + // Set to true if confirmation is required before starting up the // new terminal. var requiresConfirm: Bool = false - + // Initialize the surface config which will be used to create the tab or window for the opened file. var config = Ghostty.SurfaceConfiguration() - - if (isDirectory.boolValue) { + + if isDirectory.boolValue { // When opening a directory, check the configuration to decide // whether to open in a new tab or new window. config.workingDirectory = filename @@ -470,24 +482,24 @@ class AppDelegate: NSObject, // because there is a sandbox escape possible if a sandboxed application // somehow is tricked into `open`-ing a non-sandboxed application. requiresConfirm = true - + // When opening a file, we want to execute the file. To do this, we // don't override the command directly, because it won't load the // profile/rc files for the shell, which is super important on macOS // due to things like Homebrew. Instead, we set the command to // `; exit` which is what Terminal and iTerm2 do. config.initialInput = "\(Ghostty.Shell.quote(filename)); exit\n" - + // For commands executed directly, we want to ensure we wait after exit // because in most cases scripts don't block on exit and we don't want // the window to just flash closed once complete. config.waitAfterCommand = true - + // Set the parent directory to our working directory so that relative // paths in scripts work. config.workingDirectory = (filename as NSString).deletingLastPathComponent } - + if requiresConfirm { // Confirmation required. We use an app-wide NSAlert for now. In the future we // may want to show this as a sheet on the focused window (especially if we're @@ -497,15 +509,15 @@ class AppDelegate: NSObject, alert.addButton(withTitle: "Allow") alert.addButton(withTitle: "Cancel") alert.alertStyle = .warning - switch (alert.runModal()) { + switch alert.runModal() { case .alertFirstButtonReturn: break - + default: return false } } - + switch ghostty.config.macosDockDropBehavior { case .new_tab: _ = TerminalController.newTab( @@ -515,13 +527,8 @@ class AppDelegate: NSObject, ) case .new_window: _ = TerminalController.newWindow(ghostty, withBaseConfig: config) } - - return true - } - /// This is called for the dock right-click menu. - func applicationDockMenu(_ sender: NSApplication) -> NSMenu? { - return dockMenu + return true } /// Setup signal handlers @@ -552,133 +559,6 @@ class AppDelegate: NSObject, signals.append(sigusr2) } - /// Setup all the images for our menu items. - private func setupMenuImages() { - // Note: This COULD Be done all in the xib file, but I find it easier to - // modify this stuff as code. - self.menuAbout?.setImageIfDesired(systemSymbolName: "info.circle") - self.menuCheckForUpdates?.setImageIfDesired(systemSymbolName: "square.and.arrow.down") - self.menuOpenConfig?.setImageIfDesired(systemSymbolName: "gear") - self.menuReloadConfig?.setImageIfDesired(systemSymbolName: "arrow.trianglehead.2.clockwise.rotate.90") - self.menuSecureInput?.setImageIfDesired(systemSymbolName: "lock.display") - self.menuNewWindow?.setImageIfDesired(systemSymbolName: "macwindow.badge.plus") - self.menuNewTab?.setImageIfDesired(systemSymbolName: "macwindow") - self.menuSplitRight?.setImageIfDesired(systemSymbolName: "rectangle.righthalf.inset.filled") - self.menuSplitLeft?.setImageIfDesired(systemSymbolName: "rectangle.leadinghalf.inset.filled") - self.menuSplitUp?.setImageIfDesired(systemSymbolName: "rectangle.tophalf.inset.filled") - self.menuSplitDown?.setImageIfDesired(systemSymbolName: "rectangle.bottomhalf.inset.filled") - self.menuClose?.setImageIfDesired(systemSymbolName: "xmark") - self.menuPasteSelection?.setImageIfDesired(systemSymbolName: "doc.on.clipboard.fill") - self.menuIncreaseFontSize?.setImageIfDesired(systemSymbolName: "textformat.size.larger") - self.menuResetFontSize?.setImageIfDesired(systemSymbolName: "textformat.size") - self.menuDecreaseFontSize?.setImageIfDesired(systemSymbolName: "textformat.size.smaller") - self.menuCommandPalette?.setImageIfDesired(systemSymbolName: "filemenu.and.selection") - self.menuQuickTerminal?.setImageIfDesired(systemSymbolName: "apple.terminal") - self.menuChangeTabTitle?.setImageIfDesired(systemSymbolName: "pencil.line") - self.menuTerminalInspector?.setImageIfDesired(systemSymbolName: "scope") - self.menuReadonly?.setImageIfDesired(systemSymbolName: "eye.fill") - self.menuToggleFullScreen?.setImageIfDesired(systemSymbolName: "square.arrowtriangle.4.outward") - self.menuToggleVisibility?.setImageIfDesired(systemSymbolName: "eye") - self.menuZoomSplit?.setImageIfDesired(systemSymbolName: "arrow.up.left.and.arrow.down.right") - self.menuPreviousSplit?.setImageIfDesired(systemSymbolName: "chevron.backward.2") - self.menuNextSplit?.setImageIfDesired(systemSymbolName: "chevron.forward.2") - self.menuEqualizeSplits?.setImageIfDesired(systemSymbolName: "inset.filled.topleft.topright.bottomleft.bottomright.rectangle") - self.menuSelectSplitLeft?.setImageIfDesired(systemSymbolName: "arrow.left") - self.menuSelectSplitRight?.setImageIfDesired(systemSymbolName: "arrow.right") - self.menuSelectSplitAbove?.setImageIfDesired(systemSymbolName: "arrow.up") - self.menuSelectSplitBelow?.setImageIfDesired(systemSymbolName: "arrow.down") - self.menuMoveSplitDividerUp?.setImageIfDesired(systemSymbolName: "arrow.up.to.line") - self.menuMoveSplitDividerDown?.setImageIfDesired(systemSymbolName: "arrow.down.to.line") - self.menuMoveSplitDividerLeft?.setImageIfDesired(systemSymbolName: "arrow.left.to.line") - self.menuMoveSplitDividerRight?.setImageIfDesired(systemSymbolName: "arrow.right.to.line") - self.menuFloatOnTop?.setImageIfDesired(systemSymbolName: "square.filled.on.square") - self.menuFindParent?.setImageIfDesired(systemSymbolName: "text.page.badge.magnifyingglass") - } - - /// Sync all of our menu item keyboard shortcuts with the Ghostty configuration. - private func syncMenuShortcuts(_ config: Ghostty.Config) { - guard ghostty.readiness == .ready else { return } - - syncMenuShortcut(config, action: "check_for_updates", menuItem: self.menuCheckForUpdates) - syncMenuShortcut(config, action: "open_config", menuItem: self.menuOpenConfig) - syncMenuShortcut(config, action: "reload_config", menuItem: self.menuReloadConfig) - syncMenuShortcut(config, action: "quit", menuItem: self.menuQuit) - - syncMenuShortcut(config, action: "new_window", menuItem: self.menuNewWindow) - syncMenuShortcut(config, action: "new_tab", menuItem: self.menuNewTab) - syncMenuShortcut(config, action: "close_surface", menuItem: self.menuClose) - syncMenuShortcut(config, action: "close_tab", menuItem: self.menuCloseTab) - syncMenuShortcut(config, action: "close_window", menuItem: self.menuCloseWindow) - syncMenuShortcut(config, action: "close_all_windows", menuItem: self.menuCloseAllWindows) - syncMenuShortcut(config, action: "new_split:right", menuItem: self.menuSplitRight) - syncMenuShortcut(config, action: "new_split:left", menuItem: self.menuSplitLeft) - syncMenuShortcut(config, action: "new_split:down", menuItem: self.menuSplitDown) - syncMenuShortcut(config, action: "new_split:up", menuItem: self.menuSplitUp) - - syncMenuShortcut(config, action: "undo", menuItem: self.menuUndo) - syncMenuShortcut(config, action: "redo", menuItem: self.menuRedo) - syncMenuShortcut(config, action: "copy_to_clipboard", menuItem: self.menuCopy) - syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste) - syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection) - syncMenuShortcut(config, action: "select_all", menuItem: self.menuSelectAll) - syncMenuShortcut(config, action: "start_search", menuItem: self.menuFind) - syncMenuShortcut(config, action: "search_selection", menuItem: self.menuSelectionForFind) - syncMenuShortcut(config, action: "scroll_to_selection", menuItem: self.menuScrollToSelection) - syncMenuShortcut(config, action: "search:next", menuItem: self.menuFindNext) - syncMenuShortcut(config, action: "search:previous", menuItem: self.menuFindPrevious) - - syncMenuShortcut(config, action: "toggle_split_zoom", menuItem: self.menuZoomSplit) - syncMenuShortcut(config, action: "goto_split:previous", menuItem: self.menuPreviousSplit) - syncMenuShortcut(config, action: "goto_split:next", menuItem: self.menuNextSplit) - syncMenuShortcut(config, action: "goto_split:up", menuItem: self.menuSelectSplitAbove) - syncMenuShortcut(config, action: "goto_split:down", menuItem: self.menuSelectSplitBelow) - syncMenuShortcut(config, action: "goto_split:left", menuItem: self.menuSelectSplitLeft) - syncMenuShortcut(config, action: "goto_split:right", menuItem: self.menuSelectSplitRight) - syncMenuShortcut(config, action: "resize_split:up,10", menuItem: self.menuMoveSplitDividerUp) - syncMenuShortcut(config, action: "resize_split:down,10", menuItem: self.menuMoveSplitDividerDown) - syncMenuShortcut(config, action: "resize_split:right,10", menuItem: self.menuMoveSplitDividerRight) - syncMenuShortcut(config, action: "resize_split:left,10", menuItem: self.menuMoveSplitDividerLeft) - syncMenuShortcut(config, action: "equalize_splits", menuItem: self.menuEqualizeSplits) - syncMenuShortcut(config, action: "reset_window_size", menuItem: self.menuReturnToDefaultSize) - - syncMenuShortcut(config, action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize) - syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize) - syncMenuShortcut(config, action: "reset_font_size", menuItem: self.menuResetFontSize) - syncMenuShortcut(config, action: "prompt_surface_title", menuItem: self.menuChangeTitle) - syncMenuShortcut(config, action: "prompt_tab_title", menuItem: self.menuChangeTabTitle) - syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal) - syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility) - syncMenuShortcut(config, action: "toggle_window_float_on_top", menuItem: self.menuFloatOnTop) - syncMenuShortcut(config, action: "inspector:toggle", menuItem: self.menuTerminalInspector) - syncMenuShortcut(config, action: "toggle_command_palette", menuItem: self.menuCommandPalette) - - syncMenuShortcut(config, action: "toggle_secure_input", menuItem: self.menuSecureInput) - - // This menu item is NOT synced with the configuration because it disables macOS - // global fullscreen keyboard shortcut. The shortcut in the Ghostty config will continue - // to work but it won't be reflected in the menu item. - // - // syncMenuShortcut(config, action: "toggle_fullscreen", menuItem: self.menuToggleFullScreen) - - // Dock menu - reloadDockMenu() - } - - /// Syncs a single menu shortcut for the given action. The action string is the same - /// action string used for the Ghostty configuration. - private func syncMenuShortcut(_ config: Ghostty.Config, action: String, menuItem: NSMenuItem?) { - guard let menu = menuItem else { return } - guard let shortcut = config.keyboardShortcut(for: action) else { - // No shortcut, clear the menu item - menu.keyEquivalent = "" - menu.keyEquivalentModifierMask = [] - return - } - - menu.keyEquivalent = shortcut.key.character.description - menu.keyEquivalentModifierMask = .init(swiftUIFlags: shortcut.modifiers) - } - // MARK: Notifications and Events /// This handles events from the NSEvent.addLocalEventMonitor. We use this so we can get @@ -744,7 +624,7 @@ class AppDelegate: NSObject, guard let ghostty = self.ghostty.app else { return event } // Build our event input and call ghostty - if (ghostty_app_key(ghostty, event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS))) { + if ghostty_app_key(ghostty, event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS)) { // The key was used so we want to stop it from going to our Mac app Ghostty.logger.debug("local key event handled event=\(event)") return nil @@ -759,7 +639,7 @@ class AppDelegate: NSObject, @objc private func quickTerminalDidChangeVisibility(_ notification: Notification) { guard let quickController = notification.object as? QuickTerminalController else { return } - self.menuQuickTerminal?.state = if (quickController.visible) { .on } else { .off } + self.menuQuickTerminal?.state = if quickController.visible { .on } else { .off } } @objc private func ghosttyConfigDidChange(_ notification: Notification) { @@ -775,25 +655,35 @@ class AppDelegate: NSObject, } @objc private func ghosttyBellDidRing(_ notification: Notification) { - if (ghostty.config.bellFeatures.contains(.system)) { + if ghostty.config.bellFeatures.contains(.system) { NSSound.beep() } - if (ghostty.config.bellFeatures.contains(.attention)) { + if ghostty.config.bellFeatures.contains(.audio) { + if let configPath = ghostty.config.bellAudioPath, + let sound = NSSound(contentsOfFile: configPath.path, byReference: false) { + sound.volume = ghostty.config.bellAudioVolume + sound.play() + } + } + + if ghostty.config.bellFeatures.contains(.attention) { // Bounce the dock icon if we're not focused. NSApp.requestUserAttention(.informationalRequest) - - // Handle setting the dock badge based on permissions - ghosttyUpdateBadgeForBell() } } - private func ghosttyUpdateBadgeForBell() { + @objc private func terminalWindowHasBell(_ notification: Notification) { + guard notification.object is BaseTerminalController else { return } + syncDockBadge() + } + + private func syncDockBadge() { let center = UNUserNotificationCenter.current() center.getNotificationSettings { settings in switch settings.authorizationStatus { case .authorized: - // Already authorized, check badge setting and set if enabled + // If we're authorized and allow badges, then set the badge. if settings.badgeSetting == .enabled { DispatchQueue.main.async { self.setDockBadge() @@ -847,7 +737,12 @@ class AppDelegate: NSObject, _ = TerminalController.newTab(ghostty, from: window, withBaseConfig: config) } - private func setDockBadge(_ label: String? = "•") { + private func setDockBadge() { + let bellCount = NSApp.windows + .compactMap { $0.windowController as? BaseTerminalController } + .reduce(0) { $0 + ($1.bell ? 1 : 0) } + let wantsBadge = ghostty.config.bellFeatures.contains(.attention) && bellCount > 0 + let label = wantsBadge ? (bellCount > 99 ? "99+" : String(bellCount)) : nil NSApp.dockTile.badgeLabel = label NSApp.dockTile.display() } @@ -859,11 +754,11 @@ class AppDelegate: NSObject, // Depending on the "window-save-state" setting we have to set the NSQuitAlwaysKeepsWindows // configuration. This is the only way to carefully control whether macOS invokes the // state restoration system. - switch (config.windowSaveState) { - case "never": UserDefaults.standard.setValue(false, forKey: "NSQuitAlwaysKeepsWindows") - case "always": UserDefaults.standard.setValue(true, forKey: "NSQuitAlwaysKeepsWindows") + switch config.windowSaveState { + case "never": UserDefaults.ghostty.setValue(false, forKey: "NSQuitAlwaysKeepsWindows") + case "always": UserDefaults.ghostty.setValue(true, forKey: "NSQuitAlwaysKeepsWindows") case "default": fallthrough - default: UserDefaults.standard.removeObject(forKey: "NSQuitAlwaysKeepsWindows") + default: UserDefaults.ghostty.removeObject(forKey: "NSQuitAlwaysKeepsWindows") } // Sync our auto-update settings. If SUEnableAutomaticChecks (in our Info.plist) is @@ -878,27 +773,30 @@ class AppDelegate: NSObject, autoUpdate == .check || autoUpdate == .download updateController.updater.automaticallyDownloadsUpdates = autoUpdate == .download - /** + /* To test `auto-update` easily, uncomment the line below and delete `SUEnableAutomaticChecks` in Ghostty-Info.plist. Note: When `auto-update = download`, you may need to `Clean Build Folder` if a background install has already begun. */ - //updateController.updater.checkForUpdatesInBackground() + // updateController.updater.checkForUpdatesInBackground() } // Config could change keybindings, so update everything that depends on that syncMenuShortcuts(config) TerminalController.all.forEach { $0.relabelTabs() } + // Update our badge since config can change what we show. + syncDockBadge() + // Config could change window appearance. We wrap this in an async queue because when // this is called as part of application launch it can deadlock with an internal // AppKit mutex on the appearance. DispatchQueue.main.async { self.syncAppearance(config: config) } // Decide whether to hide/unhide app from dock and app switcher - switch (config.macosHidden) { + switch config.macosHidden { case .never: NSApp.setActivationPolicy(.regular) @@ -909,16 +807,16 @@ class AppDelegate: NSObject, // If we have configuration errors, we need to show them. let c = ConfigurationErrorsController.sharedInstance c.errors = config.errors - if (c.errors.count > 0) { - if (c.window == nil || !c.window!.isVisible) { + if c.errors.count > 0 { + if c.window == nil || !c.window!.isVisible { c.showWindow(self) } } // We need to handle our global event tap depending on if there are global // events that we care about in Ghostty. - if (ghostty_app_has_global_keybinds(ghostty.app!)) { - if (timeSinceLaunch > 5) { + if ghostty_app_has_global_keybinds(ghostty.app!) { + if timeSinceLaunch > 5 { // If the process has been running for awhile we enable right away // because no windows are likely to pop up. GlobalEventTap.shared.enable() @@ -933,9 +831,8 @@ class AppDelegate: NSObject, } else { GlobalEventTap.shared.disable() } - Task { - await updateAppIcon(from: config) - } + + updateAppIcon(from: config) } /// Sync the appearance of our app with the theme specified in the config. @@ -943,84 +840,18 @@ class AppDelegate: NSObject, NSApplication.shared.appearance = .init(ghosttyConfig: config) } - // Using AppIconActor to ensure this work - // happens synchronously in the background - @AppIconActor - private func updateAppIcon(from config: Ghostty.Config) async { - var appIcon: NSImage? - var appIconName: String? = config.macosIcon.rawValue - - switch (config.macosIcon) { - case let icon where icon.assetName != nil: - appIcon = NSImage(named: icon.assetName!)! - - case .custom: - if let userIcon = NSImage(contentsOfFile: config.macosCustomIcon) { - appIcon = userIcon - appIconName = config.macosCustomIcon - } else { - appIcon = nil // Revert back to official icon if invalid location - appIconName = nil // Discard saved icon name - } - - case .customStyle: - // Discard saved icon name - // if no valid colours were found - appIconName = nil - guard let ghostColor = config.macosIconGhostColor else { break } - guard let screenColors = config.macosIconScreenColor else { break } - guard let icon = ColorizedGhosttyIcon( - screenColors: screenColors, - ghostColor: ghostColor, - frame: config.macosIconFrame - ).makeImage() else { break } - appIcon = icon - let colorStrings = ([ghostColor] + screenColors).compactMap(\.hexString) - appIconName = (colorStrings + [config.macosIconFrame.rawValue]) - .joined(separator: "_") - - default: - // Discard saved icon name - appIconName = nil + private func updateAppIcon(from config: Ghostty.Config) { + // Since this is called after `DockTilePlugin` has been running, + // clean it up here to trigger a correct update of the current config. + UserDefaults.ghostty.removeObject(forKey: "CustomGhosttyIcon") + DispatchQueue.global().async { + UserDefaults.ghostty.appIcon = AppIcon(config: config) + DistributedNotificationCenter.default() + .postNotificationName(.ghosttyIconDidChange, object: nil, userInfo: nil, deliverImmediately: true) } - - // Only change the icon if it has actually changed from the current one, - // or if the app build has changed (e.g. after an update that reset the icon) - let cachedIconName = UserDefaults.standard.string(forKey: "CustomGhosttyIcon") - let cachedIconBuild = UserDefaults.standard.string(forKey: "CustomGhosttyIconBuild") - let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String - let buildChanged = cachedIconBuild != currentBuild - - guard cachedIconName != appIconName || buildChanged else { -#if DEBUG - if appIcon == nil { - await MainActor.run { - // Changing the app bundle's icon will corrupt code signing. - // We only use the default blueprint icon for the dock, - // so developers don't need to clean and re-build every time. - NSApplication.shared.applicationIconImage = NSImage(named: "BlueprintImage") - } - } -#endif - return - } - // make it immutable, so Swift 6 won't complain - let newIcon = appIcon - - let appPath = Bundle.main.bundlePath - guard NSWorkspace.shared.setIcon(newIcon, forFile: appPath, options: []) else { return } - NSWorkspace.shared.noteFileSystemChanged(appPath) - - await MainActor.run { - self.appIcon = newIcon - NSApplication.shared.applicationIconImage = newIcon - } - - UserDefaults.standard.set(appIconName, forKey: "CustomGhosttyIcon") - UserDefaults.standard.set(currentBuild, forKey: "CustomGhosttyIconBuild") } - //MARK: - Restorable State + // MARK: - Restorable State /// We support NSSecureCoding for restorable state. Required as of macOS Sonoma (14) but a good idea anyways. func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { @@ -1029,18 +860,18 @@ class AppDelegate: NSObject, func application(_ app: NSApplication, willEncodeRestorableState coder: NSCoder) { Self.logger.debug("application will save window state") - + guard ghostty.config.windowSaveState != "never" else { return } - + // Encode our quick terminal state if we have it. switch quickTerminalControllerState { case .initialized(let controller) where controller.restorable: let data = QuickTerminalRestorableState(from: controller) data.encode(with: coder) - + case .pendingRestore(let state): state.encode(with: coder) - + default: break } @@ -1048,7 +879,7 @@ class AppDelegate: NSObject, func application(_ app: NSApplication, didDecodeRestorableState coder: NSCoder) { Self.logger.debug("application will restore window state") - + // Decode our quick terminal state. if ghostty.config.windowSaveState != "never", let state = QuickTerminalRestorableState(coder: coder) { @@ -1056,7 +887,7 @@ class AppDelegate: NSObject, } } - //MARK: - UNUserNotificationCenterDelegate + // MARK: - UNUserNotificationCenterDelegate func userNotificationCenter( _ center: UNUserNotificationCenter, @@ -1077,36 +908,23 @@ class AppDelegate: NSObject, withCompletionHandler(options) } - //MARK: - GhosttyAppDelegate + // MARK: - GhosttyAppDelegate func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? { for c in TerminalController.all { - for view in c.surfaceTree { - if view.id == uuid { - return view - } + for view in c.surfaceTree where view.id == uuid { + return view } } return nil } - //MARK: - Dock Menu - - private func reloadDockMenu() { - let newWindow = NSMenuItem(title: "New Window", action: #selector(newWindow), keyEquivalent: "") - let newTab = NSMenuItem(title: "New Tab", action: #selector(newTab), keyEquivalent: "") - - dockMenu.removeAllItems() - dockMenu.addItem(newWindow) - dockMenu.addItem(newTab) - } - - //MARK: - Global State + // MARK: - Global State func setSecureInput(_ mode: Ghostty.SetSecureInput) { let input = SecureInput.shared - switch (mode) { + switch mode { case .on: input.global = true @@ -1116,11 +934,11 @@ class AppDelegate: NSObject, case .toggle: input.global.toggle() } - self.menuSecureInput?.state = if (input.global) { .on } else { .off } - UserDefaults.standard.set(input.global, forKey: "SecureInput") + self.menuSecureInput?.state = if input.global { .on } else { .off } + UserDefaults.ghostty.set(input.global, forKey: "SecureInput") } - //MARK: - IB Actions + // MARK: - IB Actions @IBAction func openConfig(_ sender: Any?) { Ghostty.App.openConfig() @@ -1132,7 +950,7 @@ class AppDelegate: NSObject, @IBAction func checkForUpdates(_ sender: Any?) { updateController.checkForUpdates() - //UpdateSimulator.happyPath.simulate(with: updateViewModel) + // UpdateSimulator.happyPath.simulate(with: updateViewModel) } @IBAction func newWindow(_ sender: Any?) { @@ -1264,6 +1082,233 @@ class AppDelegate: NSObject, } } +// MARK: Menu + +extension AppDelegate { + /// This is called for the dock right-click menu. + func applicationDockMenu(_ sender: NSApplication) -> NSMenu? { + return dockMenu + } + + private func reloadDockMenu() { + let newWindow = NSMenuItem(title: "New Window", action: #selector(newWindow), keyEquivalent: "") + let newTab = NSMenuItem(title: "New Tab", action: #selector(newTab), keyEquivalent: "") + + dockMenu.removeAllItems() + dockMenu.addItem(newWindow) + dockMenu.addItem(newTab) + } + + /// Setup all the images for our menu items. + private func setupMenuImages() { + // Note: This COULD Be done all in the xib file, but I find it easier to + // modify this stuff as code. + self.menuAbout?.setImageIfDesired(systemSymbolName: "info.circle") + self.menuCheckForUpdates?.setImageIfDesired(systemSymbolName: "square.and.arrow.down") + self.menuOpenConfig?.setImageIfDesired(systemSymbolName: "gear") + self.menuReloadConfig?.setImageIfDesired(systemSymbolName: "arrow.trianglehead.2.clockwise.rotate.90") + self.menuSecureInput?.setImageIfDesired(systemSymbolName: "lock.display") + self.menuNewWindow?.setImageIfDesired(systemSymbolName: "macwindow.badge.plus") + self.menuNewTab?.setImageIfDesired(systemSymbolName: "macwindow") + self.menuSplitRight?.setImageIfDesired(systemSymbolName: "rectangle.righthalf.inset.filled") + self.menuSplitLeft?.setImageIfDesired(systemSymbolName: "rectangle.leadinghalf.inset.filled") + self.menuSplitUp?.setImageIfDesired(systemSymbolName: "rectangle.tophalf.inset.filled") + self.menuSplitDown?.setImageIfDesired(systemSymbolName: "rectangle.bottomhalf.inset.filled") + self.menuClose?.setImageIfDesired(systemSymbolName: "xmark") + self.menuPasteSelection?.setImageIfDesired(systemSymbolName: "doc.on.clipboard.fill") + self.menuIncreaseFontSize?.setImageIfDesired(systemSymbolName: "textformat.size.larger") + self.menuResetFontSize?.setImageIfDesired(systemSymbolName: "textformat.size") + self.menuDecreaseFontSize?.setImageIfDesired(systemSymbolName: "textformat.size.smaller") + self.menuCommandPalette?.setImageIfDesired(systemSymbolName: "filemenu.and.selection") + self.menuQuickTerminal?.setImageIfDesired(systemSymbolName: "apple.terminal") + self.menuChangeTabTitle?.setImageIfDesired(systemSymbolName: "pencil.line") + self.menuTerminalInspector?.setImageIfDesired(systemSymbolName: "scope") + self.menuReadonly?.setImageIfDesired(systemSymbolName: "eye.fill") + self.menuSetAsDefaultTerminal?.setImageIfDesired(systemSymbolName: "star.fill") + self.menuToggleFullScreen?.setImageIfDesired(systemSymbolName: "square.arrowtriangle.4.outward") + self.menuToggleVisibility?.setImageIfDesired(systemSymbolName: "eye") + self.menuZoomSplit?.setImageIfDesired(systemSymbolName: "arrow.up.left.and.arrow.down.right") + self.menuPreviousSplit?.setImageIfDesired(systemSymbolName: "chevron.backward.2") + self.menuNextSplit?.setImageIfDesired(systemSymbolName: "chevron.forward.2") + self.menuEqualizeSplits?.setImageIfDesired(systemSymbolName: "inset.filled.topleft.topright.bottomleft.bottomright.rectangle") + self.menuSelectSplitLeft?.setImageIfDesired(systemSymbolName: "arrow.left") + self.menuSelectSplitRight?.setImageIfDesired(systemSymbolName: "arrow.right") + self.menuSelectSplitAbove?.setImageIfDesired(systemSymbolName: "arrow.up") + self.menuSelectSplitBelow?.setImageIfDesired(systemSymbolName: "arrow.down") + self.menuMoveSplitDividerUp?.setImageIfDesired(systemSymbolName: "arrow.up.to.line") + self.menuMoveSplitDividerDown?.setImageIfDesired(systemSymbolName: "arrow.down.to.line") + self.menuMoveSplitDividerLeft?.setImageIfDesired(systemSymbolName: "arrow.left.to.line") + self.menuMoveSplitDividerRight?.setImageIfDesired(systemSymbolName: "arrow.right.to.line") + self.menuFloatOnTop?.setImageIfDesired(systemSymbolName: "square.filled.on.square") + self.menuFindParent?.setImageIfDesired(systemSymbolName: "text.page.badge.magnifyingglass") + } + + /// Sync all of our menu item keyboard shortcuts with the Ghostty configuration. + private func syncMenuShortcuts(_ config: Ghostty.Config) { + guard ghostty.readiness == .ready else { return } + + // Reset our shortcut index since we're about to rebuild all menu bindings. + menuItemsByShortcut.removeAll(keepingCapacity: true) + + syncMenuShortcut(config, action: "check_for_updates", menuItem: self.menuCheckForUpdates) + syncMenuShortcut(config, action: "open_config", menuItem: self.menuOpenConfig) + syncMenuShortcut(config, action: "reload_config", menuItem: self.menuReloadConfig) + syncMenuShortcut(config, action: "quit", menuItem: self.menuQuit) + + syncMenuShortcut(config, action: "new_window", menuItem: self.menuNewWindow) + syncMenuShortcut(config, action: "new_tab", menuItem: self.menuNewTab) + syncMenuShortcut(config, action: "close_surface", menuItem: self.menuClose) + syncMenuShortcut(config, action: "close_tab", menuItem: self.menuCloseTab) + syncMenuShortcut(config, action: "close_window", menuItem: self.menuCloseWindow) + syncMenuShortcut(config, action: "close_all_windows", menuItem: self.menuCloseAllWindows) + syncMenuShortcut(config, action: "new_split:right", menuItem: self.menuSplitRight) + syncMenuShortcut(config, action: "new_split:left", menuItem: self.menuSplitLeft) + syncMenuShortcut(config, action: "new_split:down", menuItem: self.menuSplitDown) + syncMenuShortcut(config, action: "new_split:up", menuItem: self.menuSplitUp) + + syncMenuShortcut(config, action: "undo", menuItem: self.menuUndo) + syncMenuShortcut(config, action: "redo", menuItem: self.menuRedo) + syncMenuShortcut(config, action: "copy_to_clipboard", menuItem: self.menuCopy) + syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste) + syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection) + syncMenuShortcut(config, action: "select_all", menuItem: self.menuSelectAll) + syncMenuShortcut(config, action: "start_search", menuItem: self.menuFind) + syncMenuShortcut(config, action: "search_selection", menuItem: self.menuSelectionForFind) + syncMenuShortcut(config, action: "scroll_to_selection", menuItem: self.menuScrollToSelection) + syncMenuShortcut(config, action: "search:next", menuItem: self.menuFindNext) + syncMenuShortcut(config, action: "search:previous", menuItem: self.menuFindPrevious) + + syncMenuShortcut(config, action: "toggle_split_zoom", menuItem: self.menuZoomSplit) + syncMenuShortcut(config, action: "goto_split:previous", menuItem: self.menuPreviousSplit) + syncMenuShortcut(config, action: "goto_split:next", menuItem: self.menuNextSplit) + syncMenuShortcut(config, action: "goto_split:up", menuItem: self.menuSelectSplitAbove) + syncMenuShortcut(config, action: "goto_split:down", menuItem: self.menuSelectSplitBelow) + syncMenuShortcut(config, action: "goto_split:left", menuItem: self.menuSelectSplitLeft) + syncMenuShortcut(config, action: "goto_split:right", menuItem: self.menuSelectSplitRight) + syncMenuShortcut(config, action: "resize_split:up,10", menuItem: self.menuMoveSplitDividerUp) + syncMenuShortcut(config, action: "resize_split:down,10", menuItem: self.menuMoveSplitDividerDown) + syncMenuShortcut(config, action: "resize_split:right,10", menuItem: self.menuMoveSplitDividerRight) + syncMenuShortcut(config, action: "resize_split:left,10", menuItem: self.menuMoveSplitDividerLeft) + syncMenuShortcut(config, action: "equalize_splits", menuItem: self.menuEqualizeSplits) + syncMenuShortcut(config, action: "reset_window_size", menuItem: self.menuReturnToDefaultSize) + + syncMenuShortcut(config, action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize) + syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize) + syncMenuShortcut(config, action: "reset_font_size", menuItem: self.menuResetFontSize) + syncMenuShortcut(config, action: "prompt_surface_title", menuItem: self.menuChangeTitle) + syncMenuShortcut(config, action: "prompt_tab_title", menuItem: self.menuChangeTabTitle) + syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal) + syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility) + syncMenuShortcut(config, action: "toggle_window_float_on_top", menuItem: self.menuFloatOnTop) + syncMenuShortcut(config, action: "inspector:toggle", menuItem: self.menuTerminalInspector) + syncMenuShortcut(config, action: "toggle_command_palette", menuItem: self.menuCommandPalette) + + syncMenuShortcut(config, action: "toggle_secure_input", menuItem: self.menuSecureInput) + + // This menu item is NOT synced with the configuration because it disables macOS + // global fullscreen keyboard shortcut. The shortcut in the Ghostty config will continue + // to work but it won't be reflected in the menu item. + // + // syncMenuShortcut(config, action: "toggle_fullscreen", menuItem: self.menuToggleFullScreen) + + // Dock menu + reloadDockMenu() + } + + /// Syncs a single menu shortcut for the given action. The action string is the same + /// action string used for the Ghostty configuration. + private func syncMenuShortcut(_ config: Ghostty.Config, action: String, menuItem: NSMenuItem?) { + guard let menu = menuItem else { return } + + guard let shortcut = config.keyboardShortcut(for: action) else { + // No shortcut, clear the menu item + menu.keyEquivalent = "" + menu.keyEquivalentModifierMask = [] + return + } + + let keyEquivalent = shortcut.key.character.description + let modifierMask = NSEvent.ModifierFlags(swiftUIFlags: shortcut.modifiers) + menu.keyEquivalent = keyEquivalent + menu.keyEquivalentModifierMask = modifierMask + + // Build a direct lookup for key-equivalent dispatch so we don't need to + // linearly walk the full menu hierarchy at event time. + guard let key = MenuShortcutKey( + keyEquivalent: keyEquivalent, + modifiers: modifierMask + ) else { + return + } + + // Later registrations intentionally override earlier ones for the same key. + menuItemsByShortcut[key] = .init(menu) + } + + /// Attempts to perform a menu key equivalent only for menu items that represent + /// Ghostty keybind actions. This is important because it lets our surface dispatch + /// bindings through the menu so they flash but also lets our surface override macOS built-ins + /// like Cmd+H. + func performGhosttyBindingMenuKeyEquivalent(with event: NSEvent) -> Bool { + // Convert this event into the same normalized lookup key we use when + // syncing menu shortcuts from configuration. + guard let key = MenuShortcutKey(event: event) else { + return false + } + + // If we don't have an entry for this key combo, no Ghostty-owned + // menu shortcut exists for this event. + guard let weakItem = menuItemsByShortcut[key] else { + return false + } + + // Weak references can be nil if a menu item was deallocated after sync. + guard let item = weakItem.value else { + menuItemsByShortcut.removeValue(forKey: key) + return false + } + + guard let parentMenu = item.menu else { + return false + } + + // Keep enablement state fresh in case menu validation hasn't run yet. + parentMenu.update() + guard item.isEnabled else { + return false + } + + let index = parentMenu.index(of: item) + guard index >= 0 else { + return false + } + + parentMenu.performActionForItem(at: index) + return true + } + + /// Hashable key for a menu shortcut match, normalized for quick lookup. + private struct MenuShortcutKey: Hashable { + private static let shortcutModifiers: NSEvent.ModifierFlags = [.shift, .control, .option, .command] + + private let keyEquivalent: String + private let modifiersRawValue: UInt + + init?(keyEquivalent: String, modifiers: NSEvent.ModifierFlags) { + let normalized = keyEquivalent.lowercased() + guard !normalized.isEmpty else { return nil } + + self.keyEquivalent = normalized + self.modifiersRawValue = modifiers.intersection(Self.shortcutModifiers).rawValue + } + + init?(event: NSEvent) { + guard let keyEquivalent = event.charactersIgnoringModifiers else { return nil } + self.init(keyEquivalent: keyEquivalent, modifiers: event.modifierFlags) + } + } +} + // MARK: Floating Windows extension AppDelegate { @@ -1284,14 +1329,31 @@ extension AppDelegate { } @IBAction func useAsDefault(_ sender: NSMenuItem) { - let ud = UserDefaults.standard + let ud = UserDefaults.ghostty let key = TerminalWindow.defaultLevelKey - if (menuFloatOnTop?.state == .on) { + if menuFloatOnTop?.state == .on { ud.set(NSWindow.Level.floating, forKey: key) } else { ud.removeObject(forKey: key) } } + + @IBAction func setAsDefaultTerminal(_ sender: NSMenuItem) { + NSWorkspace.shared.setDefaultApplication(at: Bundle.main.bundleURL, toOpen: .unixExecutable) { error in + guard let error else { return } + Task { @MainActor in + let alert = NSAlert() + alert.messageText = "Failed to Set Default Terminal" + alert.informativeText = """ + Ghostty could not be set as the default terminal application. + + Error: \(error.localizedDescription) + """ + alert.alertStyle = .warning + alert.runModal() + } + } + } } // MARK: NSMenuItemValidation @@ -1299,6 +1361,9 @@ extension AppDelegate { extension AppDelegate: NSMenuItemValidation { func validateMenuItem(_ item: NSMenuItem) -> Bool { switch item.action { + case #selector(setAsDefaultTerminal(_:)): + return NSWorkspace.shared.defaultTerminal != Bundle.main.bundleURL + case #selector(floatOnTop(_:)), #selector(useAsDefault(_:)): // Float on top items only active if the key window is a primary @@ -1336,8 +1401,3 @@ private enum QuickTerminalState { /// Controller has been initialized. case initialized(QuickTerminalController) } - -@globalActor -fileprivate actor AppIconActor: GlobalActor { - static let shared = AppIconActor() -} diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index e2834409827..28c2a09c424 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -60,6 +60,7 @@ + @@ -109,6 +110,12 @@ + + + + + + diff --git a/macos/Sources/App/macOS/ghostty-bridging-header.h b/macos/Sources/App/macOS/ghostty-bridging-header.h index fc654ad3f7e..44781cbe978 100644 --- a/macos/Sources/App/macOS/ghostty-bridging-header.h +++ b/macos/Sources/App/macOS/ghostty-bridging-header.h @@ -1,3 +1,4 @@ // C imports here are exposed to Swift. +#import "ObjCExceptionCatcher.h" #import "VibrantLayer.h" diff --git a/macos/Sources/App/macOS/main.swift b/macos/Sources/App/macOS/main.swift index ad32f4e705e..ade9bf3f050 100644 --- a/macos/Sources/App/macOS/main.swift +++ b/macos/Sources/App/macOS/main.swift @@ -7,7 +7,7 @@ import GhosttyKit // rest of the app. if ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv) != GHOSTTY_SUCCESS { Ghostty.logger.critical("ghostty_init failed") - + // We also write to stderr if this is executed from the CLI or zig run switch Ghostty.launchSource { case .cli, .zig_run: @@ -18,7 +18,7 @@ if ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv) != GHOSTTY_SUCCE "Actions start with the `+` character.\n\n" + "View all available actions by running `ghostty +help`.\n") exit(1) - + case .app: // For the app we exit immediately. We should handle this case more // gracefully in the future. @@ -28,6 +28,6 @@ if ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv) != GHOSTTY_SUCCE // This will run the CLI action and exit if one was specified. A CLI // action is a command starting with a `+`, such as `ghostty +boo`. -ghostty_cli_try_action(); +ghostty_cli_try_action() _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) diff --git a/macos/Sources/Features/About/AboutController.swift b/macos/Sources/Features/About/AboutController.swift index efd7a515a1f..6f4cccf6d87 100644 --- a/macos/Sources/Features/About/AboutController.swift +++ b/macos/Sources/Features/About/AboutController.swift @@ -5,26 +5,28 @@ import SwiftUI class AboutController: NSWindowController, NSWindowDelegate { static let shared: AboutController = AboutController() + private let viewModel = AboutViewModel() override var windowNibName: NSNib.Name? { "About" } override func windowDidLoad() { guard let window = window else { return } window.center() window.isMovableByWindowBackground = true - window.contentView = NSHostingView(rootView: AboutView()) + window.contentView = NSHostingView(rootView: AboutView().environmentObject(viewModel)) } // MARK: - Functions func show() { window?.makeKeyAndOrderFront(nil) + viewModel.startCyclingIcons() } func hide() { window?.close() } - //MARK: - First Responder + // MARK: - First Responder @IBAction func close(_ sender: Any) { self.window?.performClose(sender) @@ -38,4 +40,8 @@ class AboutController: NSWindowController, NSWindowDelegate { @objc func cancel(_ sender: Any?) { close() } + + func windowWillClose(_ notification: Notification) { + viewModel.stopCyclingIcons() + } } diff --git a/macos/Sources/Features/About/AboutView.swift b/macos/Sources/Features/About/AboutView.swift index 967eb16b040..d9a12e4dc98 100644 --- a/macos/Sources/Features/About/AboutView.swift +++ b/macos/Sources/Features/About/AboutView.swift @@ -21,8 +21,7 @@ struct AboutView: View { init(material: NSVisualEffectView.Material, blendingMode: NSVisualEffectView.BlendingMode = .behindWindow, - isEmphasized: Bool = false) - { + isEmphasized: Bool = false) { self.material = material self.blendingMode = blendingMode self.isEmphasized = isEmphasized diff --git a/macos/Sources/Features/About/AboutViewModel.swift b/macos/Sources/Features/About/AboutViewModel.swift new file mode 100644 index 00000000000..dc0d38c2194 --- /dev/null +++ b/macos/Sources/Features/About/AboutViewModel.swift @@ -0,0 +1,40 @@ +import Combine + +class AboutViewModel: ObservableObject { + @Published var currentIcon: Ghostty.MacOSIcon? + @Published var isHovering: Bool = false + + private var timerCancellable: AnyCancellable? + + private let icons: [Ghostty.MacOSIcon] = [ + .official, + .blueprint, + .chalkboard, + .microchip, + .glass, + .holographic, + .paper, + .retro, + .xray, + ] + + func startCyclingIcons() { + timerCancellable = Timer.publish(every: 3, on: .main, in: .common) + .autoconnect() + .sink { [weak self] _ in + guard let self, !isHovering else { return } + advanceToNextIcon() + } + } + + func stopCyclingIcons() { + timerCancellable = nil + currentIcon = nil + } + + func advanceToNextIcon() { + let currentIndex = currentIcon.flatMap(icons.firstIndex(of:)) ?? 0 + let nextIndex = icons.indexWrapping(after: currentIndex) + currentIcon = icons[nextIndex] + } +} diff --git a/macos/Sources/Features/About/CyclingIconView.swift b/macos/Sources/Features/About/CyclingIconView.swift index 4274278e0b8..c2a860ff7b5 100644 --- a/macos/Sources/Features/About/CyclingIconView.swift +++ b/macos/Sources/Features/About/CyclingIconView.swift @@ -1,50 +1,38 @@ import SwiftUI import GhosttyKit +import Combine /// A view that cycles through Ghostty's official icon variants. struct CyclingIconView: View { - @State private var currentIcon: Ghostty.MacOSIcon = .official - @State private var isHovering: Bool = false - - private let icons: [Ghostty.MacOSIcon] = [ - .official, - .blueprint, - .chalkboard, - .microchip, - .glass, - .holographic, - .paper, - .retro, - .xray, - ] - private let timerPublisher = Timer.publish(every: 3, on: .main, in: .common) + @EnvironmentObject var viewModel: AboutViewModel var body: some View { ZStack { - iconView(for: currentIcon) - .id(currentIcon) + iconView(for: viewModel.currentIcon) + .id(viewModel.currentIcon) } - .animation(.easeInOut(duration: 0.5), value: currentIcon) + .animation(.easeInOut(duration: 0.5), value: viewModel.currentIcon) .frame(height: 128) - .onReceive(timerPublisher.autoconnect()) { _ in - if !isHovering { - advanceToNextIcon() - } - } .onHover { hovering in - isHovering = hovering + viewModel.isHovering = hovering } .onTapGesture { - advanceToNextIcon() + viewModel.advanceToNextIcon() + } + .contextMenu { + if let currentIcon = viewModel.currentIcon { + Button("Copy Icon Config") { + NSPasteboard.general.setString("macos-icon = \(currentIcon.rawValue)", forType: .string) + } + } } - .help("macos-icon = \(currentIcon.rawValue)") .accessibilityLabel("Ghostty Application Icon") .accessibilityHint("Click to cycle through icon variants") } @ViewBuilder - private func iconView(for icon: Ghostty.MacOSIcon) -> some View { - let iconImage: Image = switch icon.assetName { + private func iconView(for icon: Ghostty.MacOSIcon?) -> some View { + let iconImage: Image = switch icon?.assetName { case let assetName?: Image(assetName) case nil: ghosttyIconImage() } @@ -53,10 +41,4 @@ struct CyclingIconView: View { .resizable() .aspectRatio(contentMode: .fit) } - - private func advanceToNextIcon() { - let currentIndex = icons.firstIndex(of: currentIcon) ?? 0 - let nextIndex = icons.indexWrapping(after: currentIndex) - currentIcon = icons[nextIndex] - } } diff --git a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift index 0155cf855c7..c3cca2514a6 100644 --- a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift @@ -22,7 +22,7 @@ struct CloseTerminalIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surfaceView = terminal.surfaceView else { throw GhosttyIntentError.surfaceNotFound } diff --git a/macos/Sources/Features/App Intents/CommandPaletteIntent.swift b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift index 2f07d78613e..de6063564e1 100644 --- a/macos/Sources/Features/App Intents/CommandPaletteIntent.swift +++ b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift @@ -29,7 +29,7 @@ struct CommandPaletteIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } diff --git a/macos/Sources/Features/App Intents/Entities/CommandEntity.swift b/macos/Sources/Features/App Intents/Entities/CommandEntity.swift index 3c7745e7c91..f05b5d9b9b5 100644 --- a/macos/Sources/Features/App Intents/Entities/CommandEntity.swift +++ b/macos/Sources/Features/App Intents/Entities/CommandEntity.swift @@ -25,11 +25,6 @@ struct CommandEntity: AppEntity { struct ID: Hashable { let terminalId: TerminalEntity.ID let actionKey: String - - init(terminalId: TerminalEntity.ID, actionKey: String) { - self.terminalId = terminalId - self.actionKey = actionKey - } } static var typeDisplayRepresentation: TypeDisplayRepresentation { @@ -79,7 +74,7 @@ extension CommandEntity.ID: EntityIdentifierConvertible { static func entityIdentifier(for entityIdentifierString: String) -> CommandEntity.ID? { .init(rawValue: entityIdentifierString) } - + var entityIdentifierString: String { rawValue } diff --git a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift index e805466a225..a2c4abea05a 100644 --- a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift +++ b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift @@ -52,7 +52,7 @@ struct TerminalEntity: AppEntity { if let nsImage = ImageRenderer(content: view.screenshot()).nsImage { self.screenshot = nsImage } - + // Determine the kind based on the window controller type if view.window?.windowController is QuickTerminalController { self.kind = .quick @@ -66,9 +66,9 @@ extension TerminalEntity { enum Kind: String, AppEnum { case normal case quick - + static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Terminal Kind") - + static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [ .normal: .init(title: "Normal"), .quick: .init(title: "Quick") @@ -112,7 +112,7 @@ struct TerminalQuery: EntityStringQuery, EnumerableEntityQuery { let controllers = NSApp.windows.compactMap { $0.windowController as? BaseTerminalController } - + // Get all our surfaces return controllers.flatMap { $0.surfaceTree.root?.leaves() ?? [] diff --git a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift index 563e3719b95..99d6e39ba4b 100644 --- a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift +++ b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift @@ -31,7 +31,7 @@ struct GetTerminalDetailsIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + switch detail { case .title: return .result(value: terminal.title) case .workingDirectory: return .result(value: terminal.workingDirectory) diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift index d169b3a8c90..b77945cccbb 100644 --- a/macos/Sources/Features/App Intents/InputIntent.swift +++ b/macos/Sources/Features/App Intents/InputIntent.swift @@ -34,7 +34,7 @@ struct InputTextIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -86,7 +86,7 @@ struct KeyEventIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -95,7 +95,7 @@ struct KeyEventIntent: AppIntent { let ghosttyMods = mods.reduce(Ghostty.Input.Mods()) { result, mod in result.union(mod.ghosttyMod) } - + let keyEvent = Ghostty.Input.KeyEvent( key: key, action: action, @@ -150,7 +150,7 @@ struct MouseButtonIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -159,7 +159,7 @@ struct MouseButtonIntent: AppIntent { let ghosttyMods = mods.reduce(Ghostty.Input.Mods()) { result, mod in result.union(mod.ghosttyMod) } - + let mouseEvent = Ghostty.Input.MouseButtonEvent( action: action, button: button, @@ -184,7 +184,7 @@ struct MousePosIntent: AppIntent { var x: Double @Parameter( - title: "Y Position", + title: "Y Position", description: "The vertical position of the mouse cursor in pixels.", default: 0 ) @@ -213,7 +213,7 @@ struct MousePosIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -222,7 +222,7 @@ struct MousePosIntent: AppIntent { let ghosttyMods = mods.reduce(Ghostty.Input.Mods()) { result, mod in result.union(mod.ghosttyMod) } - + let mousePosEvent = Ghostty.Input.MousePosEvent( x: x, y: y, @@ -283,7 +283,7 @@ struct MouseScrollIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -306,16 +306,16 @@ enum KeyEventMods: String, AppEnum, CaseIterable { case control case option case command - + static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Modifier Key") - - static var caseDisplayRepresentations: [KeyEventMods : DisplayRepresentation] = [ + + static var caseDisplayRepresentations: [KeyEventMods: DisplayRepresentation] = [ .shift: "Shift", .control: "Control", .option: "Option", .command: "Command" ] - + var ghosttyMod: Ghostty.Input.Mods { switch self { case .shift: .shift diff --git a/macos/Sources/Features/App Intents/IntentPermission.swift b/macos/Sources/Features/App Intents/IntentPermission.swift index 210d2cb2e21..26a21e70bf7 100644 --- a/macos/Sources/Features/App Intents/IntentPermission.swift +++ b/macos/Sources/Features/App Intents/IntentPermission.swift @@ -28,7 +28,7 @@ func requestIntentPermission() async -> Bool { await withCheckedContinuation { continuation in Task { @MainActor in if let delegate = NSApp.delegate as? AppDelegate { - switch (delegate.ghostty.config.macosShortcuts) { + switch delegate.ghostty.config.macosShortcuts { case .allow: continuation.resume(returning: true) return @@ -43,7 +43,6 @@ func requestIntentPermission() async -> Bool { } } - PermissionRequest.show( "com.mitchellh.ghostty.shortcutsPermission", message: "Allow Shortcuts to interact with Ghostty?", diff --git a/macos/Sources/Features/App Intents/KeybindIntent.swift b/macos/Sources/Features/App Intents/KeybindIntent.swift index a8cea8561bc..e4f41ebbd2f 100644 --- a/macos/Sources/Features/App Intents/KeybindIntent.swift +++ b/macos/Sources/Features/App Intents/KeybindIntent.swift @@ -26,7 +26,7 @@ struct KeybindIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift index 6de9e1e7e82..858d5ceb004 100644 --- a/macos/Sources/Features/App Intents/NewTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -152,7 +152,7 @@ enum NewTerminalLocation: String { case splitRight = "split:right" case splitUp = "split:up" case splitDown = "split:down" - + var splitDirection: SplitTree.NewDirection? { switch self { case .splitLeft: return .left diff --git a/macos/Sources/Features/App Intents/QuickTerminalIntent.swift b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift index 2048a3b8822..df0fe17a56e 100644 --- a/macos/Sources/Features/App Intents/QuickTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift @@ -15,7 +15,7 @@ struct QuickTerminalIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let delegate = NSApp.delegate as? AppDelegate else { throw GhosttyIntentError.appUnavailable } diff --git a/macos/Sources/Features/AppleScript/AppDelegate+AppleScript.swift b/macos/Sources/Features/AppleScript/AppDelegate+AppleScript.swift new file mode 100644 index 00000000000..983217d606c --- /dev/null +++ b/macos/Sources/Features/AppleScript/AppDelegate+AppleScript.swift @@ -0,0 +1,351 @@ +import AppKit + +// Application-level Cocoa scripting hooks for the Ghostty AppleScript dictionary. +// +// Cocoa scripting is mostly convention-based: we do not register handlers in +// code, we expose Objective-C selectors with names Cocoa derives from +// `Ghostty.sdef`. +// +// In practical terms: +// - An `` in `sdef` maps to an ObjC collection accessor. +// - Unique-ID element lookup maps to `valueIn...WithUniqueID:`. +// - Some `` declarations map to `handle...ScriptCommand:`. +// +// This file implements the selectors Cocoa expects on `NSApplication`, which is +// the runtime object behind the `application` class in `Ghostty.sdef`. + +// MARK: - Windows + +@MainActor +extension NSApplication { + /// Backing collection for `application.windows`. + /// + /// We expose one scripting window per native tab group so scripts see the + /// expected window/tab hierarchy instead of one AppKit window per tab. + /// + /// Required selector name from the `sdef` element key: `scriptWindows`. + /// + /// Cocoa scripting calls this whenever AppleScript evaluates a window list, + /// such as `windows`, `window 1`, or `every window whose ...`. + @objc(scriptWindows) + var scriptWindows: [ScriptWindow] { + guard isAppleScriptEnabled else { return [] } + + // AppKit exposes one NSWindow per tab. AppleScript users expect one + // top-level window object containing multiple tabs, so we dedupe tab + // siblings into a single ScriptWindow. + var seen: Set = [] + var result: [ScriptWindow] = [] + + for controller in orderedTerminalControllers { + // Collapse each controller to one canonical representative for the + // whole tab group. Standalone windows map to themselves. + guard let primary = primaryTerminalController(for: controller) else { + continue + } + + let primaryControllerID = ObjectIdentifier(primary) + guard seen.insert(primaryControllerID).inserted else { + // Another tab from this group already created the scripting + // window object. + continue + } + + result.append(ScriptWindow(primaryController: primary)) + } + + return result + } + + /// Exposed as the AppleScript `front window` property. + /// + /// `scriptWindows` is already ordered front-to-back, so the first item is + /// the frontmost logical Ghostty window. + @objc(frontWindow) + var frontWindow: ScriptWindow? { + guard isAppleScriptEnabled else { return nil } + return scriptWindows.first + } + + /// Enables AppleScript unique-ID lookup for window references. + /// + /// Required selector name pattern for element key `scriptWindows`: + /// `valueInScriptWindowsWithUniqueID:`. + /// + /// Cocoa calls this when a script resolves `window id "..."`. + /// Returning `nil` makes the object specifier fail naturally. + @objc(valueInScriptWindowsWithUniqueID:) + func valueInScriptWindows(uniqueID: String) -> ScriptWindow? { + guard isAppleScriptEnabled else { return nil } + return scriptWindows.first(where: { $0.stableID == uniqueID }) + } +} + +// MARK: - Terminals + +@MainActor +extension NSApplication { + /// Backing collection for `application.terminals`. + /// + /// Required selector name: `terminals`. + @objc(terminals) + var terminals: [ScriptTerminal] { + guard isAppleScriptEnabled else { return [] } + return allSurfaceViews.map(ScriptTerminal.init) + } + + /// Enables AppleScript unique-ID lookup for terminal references. + /// + /// Required selector name pattern for element `terminals`: + /// `valueInTerminalsWithUniqueID:`. + /// + /// This is what lets scripts do stable references like + /// `terminal id "..."` even as windows/tabs change. + @objc(valueInTerminalsWithUniqueID:) + func valueInTerminals(uniqueID: String) -> ScriptTerminal? { + guard isAppleScriptEnabled else { return nil } + return allSurfaceViews + .first(where: { $0.id.uuidString == uniqueID }) + .map(ScriptTerminal.init) + } +} + +// MARK: - Commands + +@MainActor +extension NSApplication { + /// Handler for the `perform action` AppleScript command. + /// + /// Required selector name from the command in `sdef`: + /// `handlePerformActionScriptCommand:`. + /// + /// Cocoa scripting parses script syntax and provides: + /// - `directParameter`: the command string (`perform action "..."`). + /// - `evaluatedArguments["on"]`: the target terminal (`... on terminal ...`). + /// + /// We return a Bool to match the command's declared result type. + @objc(handlePerformActionScriptCommand:) + func handlePerformActionScriptCommand(_ command: NSScriptCommand) -> NSNumber? { + guard validateScript(command: command) else { return nil } + + guard let action = command.directParameter as? String else { + command.scriptErrorNumber = errAEParamMissed + command.scriptErrorString = "Missing action string." + return nil + } + + guard let terminal = command.evaluatedArguments?["on"] as? ScriptTerminal else { + command.scriptErrorNumber = errAEParamMissed + command.scriptErrorString = "Missing terminal target." + return nil + } + + return NSNumber(value: terminal.perform(action: action)) + } + + /// Handler for creating a reusable AppleScript surface configuration object. + @objc(handleNewSurfaceConfigurationScriptCommand:) + func handleNewSurfaceConfigurationScriptCommand(_ command: NSScriptCommand) -> NSDictionary? { + guard validateScript(command: command) else { return nil } + + do { + let configuration = try Ghostty.SurfaceConfiguration( + scriptRecord: command.evaluatedArguments?["configuration"] as? NSDictionary + ) + return configuration.dictionaryRepresentation + } catch { + command.scriptErrorNumber = errAECoercionFail + command.scriptErrorString = error.localizedDescription + return nil + } + } + + /// Handler for the `new window` AppleScript command. + /// + /// Required selector name from the command in `sdef`: + /// `handleNewWindowScriptCommand:`. + /// + /// Accepts an optional reusable surface configuration object. + /// + /// Returns the newly created scripting window object. + @objc(handleNewWindowScriptCommand:) + func handleNewWindowScriptCommand(_ command: NSScriptCommand) -> ScriptWindow? { + guard validateScript(command: command) else { return nil } + + guard let appDelegate = delegate as? AppDelegate else { + command.scriptErrorNumber = errAEEventFailed + command.scriptErrorString = "Ghostty app delegate is unavailable." + return nil + } + + let baseConfig: Ghostty.SurfaceConfiguration? + if let scriptRecord = command.evaluatedArguments?["configuration"] as? NSDictionary { + do { + baseConfig = try Ghostty.SurfaceConfiguration(scriptRecord: scriptRecord) + } catch { + command.scriptErrorNumber = errAECoercionFail + command.scriptErrorString = error.localizedDescription + return nil + } + } else { + baseConfig = nil + } + + let controller = TerminalController.newWindow( + appDelegate.ghostty, + withBaseConfig: baseConfig + ) + let createdWindowID = ScriptWindow.stableID(primaryController: controller) + + if let scriptWindow = scriptWindows.first(where: { $0.stableID == createdWindowID }) { + return scriptWindow + } + + // Fall back to wrapping the created controller if AppKit window ordering + // has not refreshed yet in the current run loop. + return ScriptWindow(primaryController: controller) + } + + /// Handler for the `quit` AppleScript command. + /// + /// Required selector name from the command in `sdef`: + /// `handleQuitScriptCommand:`. + @objc(handleQuitScriptCommand:) + func handleQuitScriptCommand(_ command: NSScriptCommand) { + guard validateScript(command: command) else { return } + terminate(nil) + } + + /// Handler for the `new tab` AppleScript command. + /// + /// Required selector name from the command in `sdef`: + /// `handleNewTabScriptCommand:`. + /// + /// Accepts an optional target window and optional surface configuration. + /// If no window is provided, this mirrors App Intents and uses the + /// preferred parent window. + /// + /// Returns the newly created scripting tab object. + @objc(handleNewTabScriptCommand:) + func handleNewTabScriptCommand(_ command: NSScriptCommand) -> ScriptTab? { + guard validateScript(command: command) else { return nil } + + guard let appDelegate = delegate as? AppDelegate else { + command.scriptErrorNumber = errAEEventFailed + command.scriptErrorString = "Ghostty app delegate is unavailable." + return nil + } + + let baseConfig: Ghostty.SurfaceConfiguration? + if let scriptRecord = command.evaluatedArguments?["configuration"] as? NSDictionary { + do { + baseConfig = try Ghostty.SurfaceConfiguration(scriptRecord: scriptRecord) + } catch { + command.scriptErrorNumber = errAECoercionFail + command.scriptErrorString = error.localizedDescription + return nil + } + } else { + baseConfig = nil + } + + let targetWindow = command.evaluatedArguments?["window"] as? ScriptWindow + let parentWindow: NSWindow? + if let targetWindow { + guard let resolvedWindow = targetWindow.preferredParentWindow else { + command.scriptErrorNumber = errAEEventFailed + command.scriptErrorString = "Target window is no longer available." + return nil + } + + parentWindow = resolvedWindow + } else { + parentWindow = TerminalController.preferredParent?.window + } + + guard let createdController = TerminalController.newTab( + appDelegate.ghostty, + from: parentWindow, + withBaseConfig: baseConfig + ) else { + command.scriptErrorNumber = errAEEventFailed + command.scriptErrorString = "Failed to create tab." + return nil + } + + let createdTabID = ScriptTab.stableID(controller: createdController) + + if let targetWindow, + let scriptTab = targetWindow.valueInTabs(uniqueID: createdTabID) { + return scriptTab + } + + for scriptWindow in scriptWindows { + if let scriptTab = scriptWindow.valueInTabs(uniqueID: createdTabID) { + return scriptTab + } + } + + // Fall back to wrapping the created controller if AppKit tab-group + // bookkeeping has not fully refreshed in the current run loop. + let fallbackWindow = ScriptWindow(primaryController: createdController) + return ScriptTab(window: fallbackWindow, controller: createdController) + } +} + +// MARK: - Private Helpers + +@MainActor +extension NSApplication { + /// Whether Ghostty should currently accept AppleScript interactions. + var isAppleScriptEnabled: Bool { + guard let appDelegate = delegate as? AppDelegate else { return true } + return appDelegate.ghostty.config.macosAppleScript + } + + /// Applies a consistent error when scripting is disabled by configuration. + @discardableResult + func validateScript(command: NSScriptCommand) -> Bool { + guard isAppleScriptEnabled else { + command.scriptErrorNumber = errAEEventNotPermitted + command.scriptErrorString = "AppleScript is disabled by the macos-applescript configuration." + return false + } + + return true + } + + /// Discovers all currently alive terminal surfaces across normal and quick + /// terminal windows. This powers both terminal enumeration and ID lookup. + fileprivate var allSurfaceViews: [Ghostty.SurfaceView] { + allTerminalControllers + .flatMap { $0.surfaceTree.root?.leaves() ?? [] } + } + + /// All terminal controllers in undefined order. + fileprivate var allTerminalControllers: [BaseTerminalController] { + NSApp.windows.compactMap { $0.windowController as? BaseTerminalController } + } + + /// All terminal controllers in front-to-back order. + fileprivate var orderedTerminalControllers: [BaseTerminalController] { + NSApp.orderedWindows.compactMap { $0.windowController as? BaseTerminalController } + } + + /// Identifies the primary tab controller for a window's tab group. + /// + /// This gives us one stable representative for all tabs in the same native + /// AppKit tab group. + /// + /// For standalone windows this returns the window's controller directly. + /// For tabbed windows, "primary" is currently the first controller in the + /// tab group's ordered windows list. + fileprivate func primaryTerminalController(for controller: BaseTerminalController) -> BaseTerminalController? { + guard let window = controller.window else { return nil } + guard let tabGroup = window.tabGroup else { return controller } + + return tabGroup.windows + .compactMap { $0.windowController as? BaseTerminalController } + .first + } +} diff --git a/macos/Sources/Features/AppleScript/Ghostty.Input.Mods+AppleScript.swift b/macos/Sources/Features/AppleScript/Ghostty.Input.Mods+AppleScript.swift new file mode 100644 index 00000000000..72a274c08fc --- /dev/null +++ b/macos/Sources/Features/AppleScript/Ghostty.Input.Mods+AppleScript.swift @@ -0,0 +1,18 @@ +extension Ghostty.Input.Mods { + /// Parses a comma-separated modifier string into `Ghostty.Input.Mods`. + /// + /// Recognized names: `shift`, `control`, `option`, `command`. + /// Returns `nil` if any unrecognized modifier name is encountered. + init?(scriptModifiers string: String) { + self = [] + for part in string.split(separator: ",") { + switch part.trimmingCharacters(in: .whitespaces).lowercased() { + case "shift": insert(.shift) + case "control": insert(.ctrl) + case "option": insert(.alt) + case "command": insert(.super) + default: return nil + } + } + } +} diff --git a/macos/Sources/Features/AppleScript/ScriptInputTextCommand.swift b/macos/Sources/Features/AppleScript/ScriptInputTextCommand.swift new file mode 100644 index 00000000000..9662de343e2 --- /dev/null +++ b/macos/Sources/Features/AppleScript/ScriptInputTextCommand.swift @@ -0,0 +1,41 @@ +import AppKit + +/// Handler for the `input text` AppleScript command defined in `Ghostty.sdef`. +/// +/// Cocoa scripting instantiates this class because the command's `` element +/// specifies `class="GhosttyScriptInputTextCommand"`. The runtime calls +/// `performDefaultImplementation()` to execute the command. +@MainActor +@objc(GhosttyScriptInputTextCommand) +final class ScriptInputTextCommand: NSScriptCommand { + override func performDefaultImplementation() -> Any? { + guard NSApp.validateScript(command: self) else { return nil } + + guard let text = directParameter as? String else { + scriptErrorNumber = errAEParamMissed + scriptErrorString = "Missing text to input." + return nil + } + + guard let terminal = evaluatedArguments?["terminal"] as? ScriptTerminal else { + scriptErrorNumber = errAEParamMissed + scriptErrorString = "Missing terminal target." + return nil + } + + guard let surfaceView = terminal.surfaceView else { + scriptErrorNumber = errAEEventFailed + scriptErrorString = "Terminal surface is no longer available." + return nil + } + + guard let surface = surfaceView.surfaceModel else { + scriptErrorNumber = errAEEventFailed + scriptErrorString = "Terminal surface model is not available." + return nil + } + + surface.sendText(text) + return nil + } +} diff --git a/macos/Sources/Features/AppleScript/ScriptKeyEventCommand.swift b/macos/Sources/Features/AppleScript/ScriptKeyEventCommand.swift new file mode 100644 index 00000000000..0091098c553 --- /dev/null +++ b/macos/Sources/Features/AppleScript/ScriptKeyEventCommand.swift @@ -0,0 +1,76 @@ +import AppKit + +/// Handler for the `send key` AppleScript command defined in `Ghostty.sdef`. +/// +/// Cocoa scripting instantiates this class because the command's `` element +/// specifies `class="GhosttyScriptKeyEventCommand"`. The runtime calls +/// `performDefaultImplementation()` to execute the command. +@MainActor +@objc(GhosttyScriptKeyEventCommand) +final class ScriptKeyEventCommand: NSScriptCommand { + override func performDefaultImplementation() -> Any? { + guard NSApp.validateScript(command: self) else { return nil } + + guard let keyName = directParameter as? String else { + scriptErrorNumber = errAEParamMissed + scriptErrorString = "Missing key name." + return nil + } + + guard let terminal = evaluatedArguments?["terminal"] as? ScriptTerminal else { + scriptErrorNumber = errAEParamMissed + scriptErrorString = "Missing terminal target." + return nil + } + + guard let surfaceView = terminal.surfaceView else { + scriptErrorNumber = errAEEventFailed + scriptErrorString = "Terminal surface is no longer available." + return nil + } + + guard let surface = surfaceView.surfaceModel else { + scriptErrorNumber = errAEEventFailed + scriptErrorString = "Terminal surface model is not available." + return nil + } + + guard let key = Ghostty.Input.Key(rawValue: keyName) else { + scriptErrorNumber = errAECoercionFail + scriptErrorString = "Unknown key name: \(keyName)" + return nil + } + + let action: Ghostty.Input.Action + if let actionCode = evaluatedArguments?["action"] as? UInt32 { + switch actionCode { + case "GIpr".fourCharCode: action = .press + case "GIrl".fourCharCode: action = .release + default: action = .press + } + } else { + action = .press + } + + let mods: Ghostty.Input.Mods + if let modsString = evaluatedArguments?["modifiers"] as? String { + guard let parsed = Ghostty.Input.Mods(scriptModifiers: modsString) else { + scriptErrorNumber = errAECoercionFail + scriptErrorString = "Unknown modifier in: \(modsString)" + return nil + } + mods = parsed + } else { + mods = [] + } + + let keyEvent = Ghostty.Input.KeyEvent( + key: key, + action: action, + mods: mods + ) + surface.sendKeyEvent(keyEvent) + + return nil + } +} diff --git a/macos/Sources/Features/AppleScript/ScriptMouseButtonCommand.swift b/macos/Sources/Features/AppleScript/ScriptMouseButtonCommand.swift new file mode 100644 index 00000000000..15fe0fbce32 --- /dev/null +++ b/macos/Sources/Features/AppleScript/ScriptMouseButtonCommand.swift @@ -0,0 +1,95 @@ +import AppKit + +/// Handler for the `send mouse button` AppleScript command defined in `Ghostty.sdef`. +/// +/// Cocoa scripting instantiates this class because the command's `` element +/// specifies `class="GhosttyScriptMouseButtonCommand"`. The runtime calls +/// `performDefaultImplementation()` to execute the command. +@MainActor +@objc(GhosttyScriptMouseButtonCommand) +final class ScriptMouseButtonCommand: NSScriptCommand { + override func performDefaultImplementation() -> Any? { + guard NSApp.validateScript(command: self) else { return nil } + + guard let buttonCode = directParameter as? UInt32, + let button = ScriptMouseButtonValue(code: buttonCode) else { + scriptErrorNumber = errAEParamMissed + scriptErrorString = "Missing or unknown mouse button." + return nil + } + + guard let terminal = evaluatedArguments?["terminal"] as? ScriptTerminal else { + scriptErrorNumber = errAEParamMissed + scriptErrorString = "Missing terminal target." + return nil + } + + guard let surfaceView = terminal.surfaceView else { + scriptErrorNumber = errAEEventFailed + scriptErrorString = "Terminal surface is no longer available." + return nil + } + + guard let surface = surfaceView.surfaceModel else { + scriptErrorNumber = errAEEventFailed + scriptErrorString = "Terminal surface model is not available." + return nil + } + + let action: Ghostty.Input.MouseState + if let actionCode = evaluatedArguments?["action"] as? UInt32 { + switch actionCode { + case "GIpr".fourCharCode: action = .press + case "GIrl".fourCharCode: action = .release + default: action = .press + } + } else { + action = .press + } + + let mods: Ghostty.Input.Mods + if let modsString = evaluatedArguments?["modifiers"] as? String { + guard let parsed = Ghostty.Input.Mods(scriptModifiers: modsString) else { + scriptErrorNumber = errAECoercionFail + scriptErrorString = "Unknown modifier in: \(modsString)" + return nil + } + mods = parsed + } else { + mods = [] + } + + let mouseEvent = Ghostty.Input.MouseButtonEvent( + action: action, + button: button.ghosttyButton, + mods: mods + ) + surface.sendMouseButton(mouseEvent) + + return nil + } +} + +/// Four-character codes matching the `mouse button` enumeration in `Ghostty.sdef`. +private enum ScriptMouseButtonValue { + case left + case right + case middle + + init?(code: UInt32) { + switch code { + case "GMlf".fourCharCode: self = .left + case "GMrt".fourCharCode: self = .right + case "GMmd".fourCharCode: self = .middle + default: return nil + } + } + + var ghosttyButton: Ghostty.Input.MouseButton { + switch self { + case .left: .left + case .right: .right + case .middle: .middle + } + } +} diff --git a/macos/Sources/Features/AppleScript/ScriptMousePosCommand.swift b/macos/Sources/Features/AppleScript/ScriptMousePosCommand.swift new file mode 100644 index 00000000000..a044c3b2d6a --- /dev/null +++ b/macos/Sources/Features/AppleScript/ScriptMousePosCommand.swift @@ -0,0 +1,65 @@ +import AppKit + +/// Handler for the `send mouse position` AppleScript command defined in `Ghostty.sdef`. +/// +/// Cocoa scripting instantiates this class because the command's `` element +/// specifies `class="GhosttyScriptMousePosCommand"`. The runtime calls +/// `performDefaultImplementation()` to execute the command. +@MainActor +@objc(GhosttyScriptMousePosCommand) +final class ScriptMousePosCommand: NSScriptCommand { + override func performDefaultImplementation() -> Any? { + guard NSApp.validateScript(command: self) else { return nil } + + guard let x = evaluatedArguments?["x"] as? Double else { + scriptErrorNumber = errAEParamMissed + scriptErrorString = "Missing x position." + return nil + } + + guard let y = evaluatedArguments?["y"] as? Double else { + scriptErrorNumber = errAEParamMissed + scriptErrorString = "Missing y position." + return nil + } + + guard let terminal = evaluatedArguments?["terminal"] as? ScriptTerminal else { + scriptErrorNumber = errAEParamMissed + scriptErrorString = "Missing terminal target." + return nil + } + + guard let surfaceView = terminal.surfaceView else { + scriptErrorNumber = errAEEventFailed + scriptErrorString = "Terminal surface is no longer available." + return nil + } + + guard let surface = surfaceView.surfaceModel else { + scriptErrorNumber = errAEEventFailed + scriptErrorString = "Terminal surface model is not available." + return nil + } + + let mods: Ghostty.Input.Mods + if let modsString = evaluatedArguments?["modifiers"] as? String { + guard let parsed = Ghostty.Input.Mods(scriptModifiers: modsString) else { + scriptErrorNumber = errAECoercionFail + scriptErrorString = "Unknown modifier in: \(modsString)" + return nil + } + mods = parsed + } else { + mods = [] + } + + let mousePosEvent = Ghostty.Input.MousePosEvent( + x: x, + y: y, + mods: mods + ) + surface.sendMousePos(mousePosEvent) + + return nil + } +} diff --git a/macos/Sources/Features/AppleScript/ScriptMouseScrollCommand.swift b/macos/Sources/Features/AppleScript/ScriptMouseScrollCommand.swift new file mode 100644 index 00000000000..083937eaf9d --- /dev/null +++ b/macos/Sources/Features/AppleScript/ScriptMouseScrollCommand.swift @@ -0,0 +1,71 @@ +import AppKit + +/// Handler for the `send mouse scroll` AppleScript command defined in `Ghostty.sdef`. +/// +/// Cocoa scripting instantiates this class because the command's `` element +/// specifies `class="GhosttyScriptMouseScrollCommand"`. The runtime calls +/// `performDefaultImplementation()` to execute the command. +@MainActor +@objc(GhosttyScriptMouseScrollCommand) +final class ScriptMouseScrollCommand: NSScriptCommand { + override func performDefaultImplementation() -> Any? { + guard NSApp.validateScript(command: self) else { return nil } + + guard let x = evaluatedArguments?["x"] as? Double else { + scriptErrorNumber = errAEParamMissed + scriptErrorString = "Missing x scroll delta." + return nil + } + + guard let y = evaluatedArguments?["y"] as? Double else { + scriptErrorNumber = errAEParamMissed + scriptErrorString = "Missing y scroll delta." + return nil + } + + guard let terminal = evaluatedArguments?["terminal"] as? ScriptTerminal else { + scriptErrorNumber = errAEParamMissed + scriptErrorString = "Missing terminal target." + return nil + } + + guard let surfaceView = terminal.surfaceView else { + scriptErrorNumber = errAEEventFailed + scriptErrorString = "Terminal surface is no longer available." + return nil + } + + guard let surface = surfaceView.surfaceModel else { + scriptErrorNumber = errAEEventFailed + scriptErrorString = "Terminal surface model is not available." + return nil + } + + let precision = evaluatedArguments?["precision"] as? Bool ?? false + + let momentum: Ghostty.Input.Momentum + if let momentumCode = evaluatedArguments?["momentum"] as? UInt32 { + switch momentumCode { + case "SMno".fourCharCode: momentum = .none + case "SMbg".fourCharCode: momentum = .began + case "SMch".fourCharCode: momentum = .changed + case "SMen".fourCharCode: momentum = .ended + case "SMcn".fourCharCode: momentum = .cancelled + case "SMmb".fourCharCode: momentum = .mayBegin + case "SMst".fourCharCode: momentum = .stationary + default: momentum = .none + } + } else { + momentum = .none + } + + let scrollEvent = Ghostty.Input.MouseScrollEvent( + x: x, + y: y, + mods: .init(precision: precision, momentum: momentum) + ) + surface.sendMouseScroll(scrollEvent) + + return nil + } +} diff --git a/macos/Sources/Features/AppleScript/ScriptRecord.swift b/macos/Sources/Features/AppleScript/ScriptRecord.swift new file mode 100644 index 00000000000..7c81b8e2981 --- /dev/null +++ b/macos/Sources/Features/AppleScript/ScriptRecord.swift @@ -0,0 +1,29 @@ +import Cocoa + +/// Protocol to more easily implement AppleScript records in Swift. +protocol ScriptRecord { + /// Initialize a default record. + init() + + /// Initialize a record from the raw value from AppleScript. + init(scriptRecord: NSDictionary?) throws + + /// Encode into the dictionary form for AppleScript. + var dictionaryRepresentation: NSDictionary { get } +} + +/// An error that can be thrown by `ScriptRecord.init(scriptRecord:)`. Any localized error +/// can be thrown but this is a common one. +enum RecordParseError: LocalizedError { + case invalidType(parameter: String, expected: String) + case invalidValue(parameter: String, message: String) + + var errorDescription: String? { + switch self { + case .invalidType(let parameter, let expected): + return "\(parameter) must be \(expected)." + case .invalidValue(let parameter, let message): + return "\(parameter) \(message)." + } + } +} diff --git a/macos/Sources/Features/AppleScript/ScriptSurfaceConfiguration.swift b/macos/Sources/Features/AppleScript/ScriptSurfaceConfiguration.swift new file mode 100644 index 00000000000..dfa60da4169 --- /dev/null +++ b/macos/Sources/Features/AppleScript/ScriptSurfaceConfiguration.swift @@ -0,0 +1,140 @@ +import Foundation + +/// AppleScript record support for `Ghostty.SurfaceConfiguration`. +/// +/// This keeps scripting conversion at the data-structure boundary so AppleScript +/// can pass records by value (`new surface configuration`, assign, copy, mutate) +/// without introducing an additional wrapper type. +extension Ghostty.SurfaceConfiguration: ScriptRecord { + init(scriptRecord source: NSDictionary?) throws { + self.init() + + guard let source else { + return + } + + guard let raw = source as? [String: Any] else { + throw RecordParseError.invalidType(parameter: "configuration", expected: "a surface configuration record") + } + + if let rawFontSize = raw["fontSize"] { + guard let number = rawFontSize as? NSNumber else { + throw RecordParseError.invalidType(parameter: "font size", expected: "a number") + } + + let value = number.doubleValue + guard value.isFinite else { + throw RecordParseError.invalidValue(parameter: "font size", message: "must be a finite number") + } + + if value < 0 { + throw RecordParseError.invalidValue(parameter: "font size", message: "must be a positive number") + } + + if value > 0 { + fontSize = Float32(value) + } + } + + if let rawWorkingDirectory = raw["workingDirectory"] { + guard let workingDirectory = rawWorkingDirectory as? String else { + throw RecordParseError.invalidType(parameter: "initial working directory", expected: "text") + } + + if !workingDirectory.isEmpty { + self.workingDirectory = workingDirectory + } + } + + if let rawCommand = raw["command"] { + guard let command = rawCommand as? String else { + throw RecordParseError.invalidType(parameter: "command", expected: "text") + } + + if !command.isEmpty { + self.command = command + } + } + + if let rawInitialInput = raw["initialInput"] { + guard let initialInput = rawInitialInput as? String else { + throw RecordParseError.invalidType(parameter: "initial input", expected: "text") + } + + if !initialInput.isEmpty { + self.initialInput = initialInput + } + } + + if let rawWaitAfterCommand = raw["waitAfterCommand"] { + if let boolValue = rawWaitAfterCommand as? Bool { + waitAfterCommand = boolValue + } else if let numericValue = rawWaitAfterCommand as? NSNumber { + waitAfterCommand = numericValue.boolValue + } else { + throw RecordParseError.invalidType(parameter: "wait after command", expected: "boolean") + } + } + + if let assignments = raw["environmentVariables"] as? [String], !assignments.isEmpty { + environmentVariables = try Self.parseScriptEnvironmentAssignments(assignments) + } + } + + var dictionaryRepresentation: NSDictionary { + var record: [String: Any] = [ + "fontSize": 0, + "workingDirectory": "", + "command": "", + "initialInput": "", + "waitAfterCommand": false, + "environmentVariables": [String](), + ] + + if let fontSize { + record["fontSize"] = NSNumber(value: fontSize) + } + + if let workingDirectory { + record["workingDirectory"] = workingDirectory + } + + if let command { + record["command"] = command + } + + if let initialInput { + record["initialInput"] = initialInput + } + + if waitAfterCommand { + record["waitAfterCommand"] = true + } + + if !environmentVariables.isEmpty { + record["environmentVariables"] = environmentVariables.map { "\($0.key)=\($0.value)" } + } + + return record as NSDictionary + } + + private static func parseScriptEnvironmentAssignments(_ assignments: [String]) throws -> [String: String] { + var result: [String: String] = [:] + + for assignment in assignments { + guard let separator = assignment.firstIndex(of: "=") else { + throw RecordParseError.invalidValue( + parameter: "environment variables", + message: "expected KEY=VALUE, got \"\(assignment)\"" + ) + } + + let key = String(assignment[.. tab` without knowing anything about AppKit controllers. +@MainActor +@objc(GhosttyScriptTab) +final class ScriptTab: NSObject { + /// Stable identifier used by AppleScript `tab id "..."` references. + private let stableID: String + + /// Weak back-reference to the scripting window that owns this tab wrapper. + /// + /// We only need this for dynamic properties (`index`, `selected`) and for + /// building an object specifier path. + private weak var window: ScriptWindow? + + /// Live terminal controller for this tab. + /// + /// This can become `nil` if the tab closes while a script is running. + private weak var controller: BaseTerminalController? + + /// Called by `ScriptWindow.tabs` / `ScriptWindow.selectedTab`. + /// + /// The ID is computed once so object specifiers built from this instance keep + /// a consistent tab identity. + init(window: ScriptWindow, controller: BaseTerminalController) { + self.stableID = Self.stableID(controller: controller) + self.window = window + self.controller = controller + } + + /// Exposed as the AppleScript `id` property. + @objc(id) + var idValue: String { + guard NSApp.isAppleScriptEnabled else { return "" } + return stableID + } + + /// Exposed as the AppleScript `title` property. + /// + /// Returns the title of the tab's window. + @objc(title) + var title: String { + guard NSApp.isAppleScriptEnabled else { return "" } + return controller?.window?.title ?? "" + } + + /// Exposed as the AppleScript `index` property. + /// + /// Cocoa scripting expects this to be 1-based for user-facing collections. + @objc(index) + var index: Int { + guard NSApp.isAppleScriptEnabled else { return 0 } + guard let controller else { return 0 } + return window?.tabIndex(for: controller) ?? 0 + } + + /// Exposed as the AppleScript `selected` property. + /// + /// Powers script conditions such as `if selected of tab 1 then ...`. + @objc(selected) + var selected: Bool { + guard NSApp.isAppleScriptEnabled else { return false } + guard let controller else { return false } + return window?.tabIsSelected(controller) ?? false + } + + /// Exposed as the AppleScript `focused terminal` property. + /// + /// Uses the currently focused surface for this tab. + @objc(focusedTerminal) + var focusedTerminal: ScriptTerminal? { + guard NSApp.isAppleScriptEnabled else { return nil } + guard let controller else { return nil } + guard let surface = controller.focusedSurface, + controller.surfaceTree.contains(surface) + else { return nil } + + return ScriptTerminal(surfaceView: surface) + } + + /// Best-effort native window containing this tab. + var parentWindow: NSWindow? { + guard NSApp.isAppleScriptEnabled else { return nil } + return controller?.window + } + + /// Live controller backing this tab wrapper. + var parentController: BaseTerminalController? { + guard NSApp.isAppleScriptEnabled else { return nil } + return controller + } + + /// Exposed as the AppleScript `terminals` element on a tab. + /// + /// Returns all terminal surfaces (split panes) within this tab. + @objc(terminals) + var terminals: [ScriptTerminal] { + guard NSApp.isAppleScriptEnabled else { return [] } + guard let controller else { return [] } + return (controller.surfaceTree.root?.leaves() ?? []) + .map(ScriptTerminal.init) + } + + /// Enables unique-ID lookup for `terminals` references on a tab. + @objc(valueInTerminalsWithUniqueID:) + func valueInTerminals(uniqueID: String) -> ScriptTerminal? { + guard NSApp.isAppleScriptEnabled else { return nil } + guard let controller else { return nil } + return (controller.surfaceTree.root?.leaves() ?? []) + .first(where: { $0.id.uuidString == uniqueID }) + .map(ScriptTerminal.init) + } + + /// Handler for `select tab `. + @objc(handleSelectTabCommand:) + func handleSelectTab(_ command: NSScriptCommand) -> Any? { + guard NSApp.validateScript(command: command) else { return nil } + + guard let tabContainerWindow = parentWindow else { + command.scriptErrorNumber = errAEEventFailed + command.scriptErrorString = "Tab is no longer available." + return nil + } + + tabContainerWindow.makeKeyAndOrderFront(nil) + return nil + } + + /// Handler for `close tab `. + @objc(handleCloseTabCommand:) + func handleCloseTab(_ command: NSScriptCommand) -> Any? { + guard NSApp.validateScript(command: command) else { return nil } + + guard let tabController = parentController else { + command.scriptErrorNumber = errAEEventFailed + command.scriptErrorString = "Tab is no longer available." + return nil + } + + if let managedTerminalController = tabController as? TerminalController { + managedTerminalController.closeTabImmediately(registerRedo: false) + return nil + } + + guard let tabContainerWindow = parentWindow else { + command.scriptErrorNumber = errAEEventFailed + command.scriptErrorString = "Tab container window is no longer available." + return nil + } + + tabContainerWindow.close() + return nil + } + + /// Provides Cocoa scripting with a canonical "path" back to this object. + override var objectSpecifier: NSScriptObjectSpecifier? { + guard NSApp.isAppleScriptEnabled else { return nil } + guard let window else { return nil } + guard let windowClassDescription = window.classDescription as? NSScriptClassDescription else { + return nil + } + guard let windowSpecifier = window.objectSpecifier else { return nil } + + // This tells Cocoa how to re-find this tab later: + // application -> scriptWindows[id] -> tabs[id]. + return NSUniqueIDSpecifier( + containerClassDescription: windowClassDescription, + containerSpecifier: windowSpecifier, + key: "tabs", + uniqueID: stableID + ) + } +} + +extension ScriptTab { + /// Stable ID for one tab controller. + /// + /// Tab identity belongs to `ScriptTab`, so both tab creation and tab ID + /// lookups in `ScriptWindow` call this helper. + static func stableID(controller: BaseTerminalController) -> String { + "tab-\(ObjectIdentifier(controller).hexString)" + } +} diff --git a/macos/Sources/Features/AppleScript/ScriptTerminal.swift b/macos/Sources/Features/AppleScript/ScriptTerminal.swift new file mode 100644 index 00000000000..2cdde382e85 --- /dev/null +++ b/macos/Sources/Features/AppleScript/ScriptTerminal.swift @@ -0,0 +1,206 @@ +import AppKit + +/// AppleScript-facing wrapper around a live Ghostty terminal surface. +/// +/// This class is intentionally ObjC-visible because Cocoa scripting resolves +/// AppleScript objects through Objective-C runtime names/selectors, not Swift +/// protocol conformance. +/// +/// Mapping from `Ghostty.sdef`: +/// - `class terminal` -> this class (`@objc(GhosttyAppleScriptTerminal)`). +/// - `property id` -> `@objc(id)` getter below. +/// - `property title` -> `@objc(title)` getter below. +/// - `property working directory` -> `@objc(workingDirectory)` getter below. +/// +/// We keep only a weak reference to the underlying `SurfaceView` so this +/// wrapper never extends the terminal's lifetime. +@MainActor +@objc(GhosttyScriptTerminal) +final class ScriptTerminal: NSObject { + /// Weak reference to the underlying surface. Package-visible so that + /// other AppleScript command handlers (e.g. `ScriptSplitCommand`) can + /// access the live surface without exposing it to ObjC/AppleScript. + weak var surfaceView: Ghostty.SurfaceView? + + init(surfaceView: Ghostty.SurfaceView) { + self.surfaceView = surfaceView + } + + /// Exposed as the AppleScript `id` property. + /// + /// This is a stable UUID string for the life of a surface and is also used + /// by `NSUniqueIDSpecifier` to re-identify a terminal object in scripts. + @objc(id) + var stableID: String { + guard NSApp.isAppleScriptEnabled else { return "" } + return surfaceView?.id.uuidString ?? "" + } + + /// Exposed as the AppleScript `title` property. + @objc(title) + var title: String { + guard NSApp.isAppleScriptEnabled else { return "" } + return surfaceView?.title ?? "" + } + + /// Exposed as the AppleScript `working directory` property. + /// + /// The `sdef` uses a spaced name, but Cocoa scripting maps that to the + /// camel-cased selector name `workingDirectory`. + @objc(workingDirectory) + var workingDirectory: String { + guard NSApp.isAppleScriptEnabled else { return "" } + return surfaceView?.pwd ?? "" + } + + /// Used by command handling (`perform action ... on `). + func perform(action: String) -> Bool { + guard NSApp.isAppleScriptEnabled else { return false } + guard let surfaceModel = surfaceView?.surfaceModel else { return false } + return surfaceModel.perform(action: action) + } + + /// Handler for `split direction `. + @objc(handleSplitCommand:) + func handleSplit(_ command: NSScriptCommand) -> Any? { + guard NSApp.validateScript(command: command) else { return nil } + + guard let surfaceView else { + command.scriptErrorNumber = errAEEventFailed + command.scriptErrorString = "Terminal surface is no longer available." + return nil + } + + guard let directionCode = command.evaluatedArguments?["direction"] as? UInt32 else { + command.scriptErrorNumber = errAEParamMissed + command.scriptErrorString = "Missing or unknown split direction." + return nil + } + + guard let direction = ScriptSplitDirection(code: directionCode)?.splitDirection else { + command.scriptErrorNumber = errAEParamMissed + command.scriptErrorString = "Missing or unknown split direction." + return nil + } + + let baseConfig: Ghostty.SurfaceConfiguration? + if let scriptRecord = command.evaluatedArguments?["configuration"] as? NSDictionary { + do { + baseConfig = try Ghostty.SurfaceConfiguration(scriptRecord: scriptRecord) + } catch { + command.scriptErrorNumber = errAECoercionFail + command.scriptErrorString = error.localizedDescription + return nil + } + } else { + baseConfig = nil + } + + guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { + command.scriptErrorNumber = errAEEventFailed + command.scriptErrorString = "Terminal is not in a splittable window." + return nil + } + + guard let newView = controller.newSplit( + at: surfaceView, + direction: direction, + baseConfig: baseConfig + ) else { + command.scriptErrorNumber = errAEEventFailed + command.scriptErrorString = "Failed to create split." + return nil + } + + return ScriptTerminal(surfaceView: newView) + } + + /// Handler for `focus `. + @objc(handleFocusCommand:) + func handleFocus(_ command: NSScriptCommand) -> Any? { + guard NSApp.validateScript(command: command) else { return nil } + + guard let surfaceView else { + command.scriptErrorNumber = errAEEventFailed + command.scriptErrorString = "Terminal surface is no longer available." + return nil + } + + guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { + command.scriptErrorNumber = errAEEventFailed + command.scriptErrorString = "Terminal is not in a window." + return nil + } + + controller.focusSurface(surfaceView) + return nil + } + + /// Handler for `close `. + @objc(handleCloseCommand:) + func handleClose(_ command: NSScriptCommand) -> Any? { + guard NSApp.validateScript(command: command) else { return nil } + + guard let surfaceView else { + command.scriptErrorNumber = errAEEventFailed + command.scriptErrorString = "Terminal surface is no longer available." + return nil + } + + guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { + command.scriptErrorNumber = errAEEventFailed + command.scriptErrorString = "Terminal is not in a window." + return nil + } + + controller.closeSurface(surfaceView, withConfirmation: false) + return nil + } + + /// Provides Cocoa scripting with a canonical "path" back to this object. + /// + /// Without an object specifier, returned terminal objects can't be reliably + /// referenced in follow-up script statements because AppleScript cannot + /// express where the object came from (`application.terminals[id]`). + override var objectSpecifier: NSScriptObjectSpecifier? { + guard NSApp.isAppleScriptEnabled else { return nil } + guard let appClassDescription = NSApplication.shared.classDescription as? NSScriptClassDescription else { + return nil + } + + return NSUniqueIDSpecifier( + containerClassDescription: appClassDescription, + containerSpecifier: nil, + key: "terminals", + uniqueID: stableID + ) + } +} + +/// Converts four-character codes from the `split direction` enumeration in `Ghostty.sdef` +/// to `SplitTree.NewDirection` values. +enum ScriptSplitDirection { + case right + case left + case down + case up + + init?(code: UInt32) { + switch code { + case "GSrt".fourCharCode: self = .right + case "GSlf".fourCharCode: self = .left + case "GSdn".fourCharCode: self = .down + case "GSup".fourCharCode: self = .up + default: return nil + } + } + + var splitDirection: SplitTree.NewDirection { + switch self { + case .right: .right + case .left: .left + case .down: .down + case .up: .up + } + } +} diff --git a/macos/Sources/Features/AppleScript/ScriptWindow.swift b/macos/Sources/Features/AppleScript/ScriptWindow.swift new file mode 100644 index 00000000000..c8e4bc8e629 --- /dev/null +++ b/macos/Sources/Features/AppleScript/ScriptWindow.swift @@ -0,0 +1,260 @@ +import AppKit + +/// AppleScript-facing wrapper around a logical Ghostty window. +/// +/// In AppKit, each tab is often its own `NSWindow`. AppleScript users, however, +/// expect a single window object containing a list of tabs. +/// +/// `ScriptWindow` is that compatibility layer: +/// - It presents one object per tab group. +/// - It translates tab-group state into `tabs` and `selected tab`. +/// - It exposes stable IDs that Cocoa scripting can resolve later. +@MainActor +@objc(GhosttyScriptWindow) +final class ScriptWindow: NSObject { + /// Stable identifier used by AppleScript `window id "..."` references. + /// + /// We precompute this once so the object keeps a consistent ID for its whole + /// lifetime, even if AppKit window bookkeeping changes after creation. + let stableID: String + + /// Canonical representative for this scripting window's tab group. + /// + /// We intentionally keep only one controller reference; full tab membership + /// is derived lazily from current AppKit state whenever needed. + private weak var primaryController: BaseTerminalController? + + /// `scriptWindows` in `AppDelegate+AppleScript` constructs these objects. + /// + /// `stableID` must match the same identity scheme used by + /// `valueInScriptWindowsWithUniqueID:` so Cocoa can re-resolve object + /// specifiers produced earlier in a script. + init(primaryController: BaseTerminalController) { + self.stableID = Self.stableID(primaryController: primaryController) + self.primaryController = primaryController + } + + /// Exposed as the AppleScript `id` property. + /// + /// This is what scripts read with `id of window ...`. + @objc(id) + var idValue: String { + guard NSApp.isAppleScriptEnabled else { return "" } + return stableID + } + + /// Exposed as the AppleScript `title` property. + /// + /// Returns the title of the window (from the selected/primary controller's NSWindow). + @objc(title) + var title: String { + guard NSApp.isAppleScriptEnabled else { return "" } + return selectedController?.window?.title ?? "" + } + + /// Exposed as the AppleScript `tabs` element. + /// + /// Cocoa asks for this collection when a script evaluates `tabs of window ...` + /// or any tab-filter expression. We build wrappers from live controller state + /// so tab additions/removals are reflected immediately. + @objc(tabs) + var tabs: [ScriptTab] { + guard NSApp.isAppleScriptEnabled else { return [] } + return controllers.map { ScriptTab(window: self, controller: $0) } + } + + /// Exposed as the AppleScript `selected tab` property. + /// + /// This powers expressions like `selected tab of window 1`. + @objc(selectedTab) + var selectedTab: ScriptTab? { + guard NSApp.isAppleScriptEnabled else { return nil } + guard let selectedController else { return nil } + return ScriptTab(window: self, controller: selectedController) + } + + /// Enables unique-ID lookup for `tabs` references. + /// + /// Required selector pattern for the `tabs` element key: + /// `valueInTabsWithUniqueID:`. + /// + /// Cocoa uses this when a script resolves `tab id "..." of window ...`. + @objc(valueInTabsWithUniqueID:) + func valueInTabs(uniqueID: String) -> ScriptTab? { + guard NSApp.isAppleScriptEnabled else { return nil } + guard let controller = controller(tabID: uniqueID) else { return nil } + return ScriptTab(window: self, controller: controller) + } + + /// Exposed as the AppleScript `terminals` element on a window. + /// + /// Returns all terminal surfaces across every tab in this window. + @objc(terminals) + var terminals: [ScriptTerminal] { + guard NSApp.isAppleScriptEnabled else { return [] } + return controllers + .flatMap { $0.surfaceTree.root?.leaves() ?? [] } + .map(ScriptTerminal.init) + } + + /// Enables unique-ID lookup for `terminals` references on a window. + @objc(valueInTerminalsWithUniqueID:) + func valueInTerminals(uniqueID: String) -> ScriptTerminal? { + guard NSApp.isAppleScriptEnabled else { return nil } + return controllers + .flatMap { $0.surfaceTree.root?.leaves() ?? [] } + .first(where: { $0.id.uuidString == uniqueID }) + .map(ScriptTerminal.init) + } + + /// AppleScript tab indexes are 1-based, so we add one to Swift's 0-based + /// array index. + func tabIndex(for controller: BaseTerminalController) -> Int? { + guard NSApp.isAppleScriptEnabled else { return nil } + return controllers.firstIndex(where: { $0 === controller }).map { $0 + 1 } + } + + /// Reports whether a given controller maps to this window's selected tab. + func tabIsSelected(_ controller: BaseTerminalController) -> Bool { + guard NSApp.isAppleScriptEnabled else { return false } + return selectedController === controller + } + + /// Best-effort native window to use as a tab parent for AppleScript commands. + var preferredParentWindow: NSWindow? { + guard NSApp.isAppleScriptEnabled else { return nil } + return selectedController?.window ?? controllers.first?.window + } + + /// Best-effort controller to use for window-scoped AppleScript commands. + var preferredController: BaseTerminalController? { + guard NSApp.isAppleScriptEnabled else { return nil } + return selectedController ?? controllers.first + } + + /// Resolves a previously generated tab ID back to a live controller. + private func controller(tabID: String) -> BaseTerminalController? { + controllers.first(where: { ScriptTab.stableID(controller: $0) == tabID }) + } + + /// Live controller list for this scripting window. + /// + /// We recalculate on every access so AppleScript immediately sees tab-group + /// changes (new tabs, closed tabs, tab moves) without rebuilding all objects. + private var controllers: [BaseTerminalController] { + guard NSApp.isAppleScriptEnabled else { return [] } + guard let primaryController else { return [] } + guard let window = primaryController.window else { return [primaryController] } + + if let tabGroup = window.tabGroup { + let groupControllers = tabGroup.windows.compactMap { + $0.windowController as? BaseTerminalController + } + if !groupControllers.isEmpty { + return groupControllers + } + } + + return [primaryController] + } + + /// Live selected controller for this scripting window. + /// + /// AppKit tracks selected tab on `NSWindowTabGroup.selectedWindow`; for + /// non-tabbed windows we fall back to the primary controller. + private var selectedController: BaseTerminalController? { + guard let primaryController else { return nil } + guard let window = primaryController.window else { return primaryController } + + if let tabGroup = window.tabGroup, + let selectedController = tabGroup.selectedWindow?.windowController as? BaseTerminalController { + return selectedController + } + + return controllers.first + } + + /// Handler for `activate window `. + @objc(handleActivateWindowCommand:) + func handleActivateWindow(_ command: NSScriptCommand) -> Any? { + guard NSApp.validateScript(command: command) else { return nil } + + guard let windowContainer = preferredParentWindow else { + command.scriptErrorNumber = errAEEventFailed + command.scriptErrorString = "Window is no longer available." + return nil + } + + windowContainer.makeKeyAndOrderFront(nil) + NSApp.activate(ignoringOtherApps: true) + return nil + } + + /// Handler for `close window `. + @objc(handleCloseWindowCommand:) + func handleCloseWindow(_ command: NSScriptCommand) -> Any? { + guard NSApp.validateScript(command: command) else { return nil } + + if let managedTerminalController = preferredController as? TerminalController { + managedTerminalController.closeWindowImmediately() + return nil + } + + guard let windowContainer = preferredParentWindow else { + command.scriptErrorNumber = errAEEventFailed + command.scriptErrorString = "Window is no longer available." + return nil + } + + windowContainer.close() + return nil + } + + /// Provides Cocoa scripting with a canonical "path" back to this object. + /// + /// Without this, Cocoa can return data but cannot reliably build object + /// references for later script statements. This specifier encodes: + /// `application -> scriptWindows[id]`. + override var objectSpecifier: NSScriptObjectSpecifier? { + guard NSApp.isAppleScriptEnabled else { return nil } + guard let appClassDescription = NSApplication.shared.classDescription as? NSScriptClassDescription else { + return nil + } + + return NSUniqueIDSpecifier( + containerClassDescription: appClassDescription, + containerSpecifier: nil, + key: "scriptWindows", + uniqueID: stableID + ) + } +} + +extension ScriptWindow { + /// Produces the window-level stable ID from the primary controller. + /// + /// - Tabbed windows are keyed by tab-group identity. + /// - Standalone windows are keyed by window identity. + /// - Detached controllers fall back to controller identity. + static func stableID(primaryController: BaseTerminalController) -> String { + guard let window = primaryController.window else { + return "controller-\(ObjectIdentifier(primaryController).hexString)" + } + + if let tabGroup = window.tabGroup { + return stableID(tabGroup: tabGroup) + } + + return stableID(window: window) + } + + /// Stable ID for a standalone native window. + static func stableID(window: NSWindow) -> String { + "window-\(ObjectIdentifier(window).hexString)" + } + + /// Stable ID for a native AppKit tab group. + static func stableID(tabGroup: NSWindowTabGroup) -> String { + "tab-group-\(ObjectIdentifier(tabGroup).hexString)" + } +} diff --git a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift index 2040dcfae57..37b20afb02d 100644 --- a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift +++ b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift @@ -13,7 +13,7 @@ class ClipboardConfirmationController: NSWindowController { let contents: String let request: Ghostty.ClipboardRequest let state: UnsafeMutableRawPointer? - weak private var delegate: ClipboardConfirmationViewDelegate? = nil + weak private var delegate: ClipboardConfirmationViewDelegate? init(surface: ghostty_surface_t, contents: String, request: Ghostty.ClipboardRequest, state: UnsafeMutableRawPointer?, delegate: ClipboardConfirmationViewDelegate) { self.surface = surface @@ -28,12 +28,12 @@ class ClipboardConfirmationController: NSWindowController { fatalError("init(coder:) is not supported for this view") } - //MARK: - NSWindowController + // MARK: - NSWindowController override func windowDidLoad() { guard let window = window else { return } - switch (request) { + switch request { case .paste: window.title = "Warning: Potentially Unsafe Paste" case .osc_52_read, .osc_52_write: diff --git a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift index 6423e3cf6cc..17ab4aa24da 100644 --- a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift +++ b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift @@ -7,7 +7,7 @@ protocol ClipboardConfirmationViewDelegate: AnyObject { /// The SwiftUI view for showing a clipboard confirmation dialog. struct ClipboardConfirmationView: View { - enum Action : String { + enum Action: String { case cancel case confirm @@ -32,7 +32,7 @@ struct ClipboardConfirmationView: View { let request: Ghostty.ClipboardRequest /// Optional delegate to get results. If this is nil, then this view will never close on its own. - weak var delegate: ClipboardConfirmationViewDelegate? = nil + weak var delegate: ClipboardConfirmationViewDelegate? /// Used to track if we should rehide on disappear @State private var cursorHiddenCount: UInt = 0 @@ -45,16 +45,16 @@ struct ClipboardConfirmationView: View { .font(.system(size: 42)) .padding() .frame(alignment: .center) - + Text(request.text()) .frame(maxWidth: .infinity, alignment: .leading) .padding() } - + TextEditor(text: .constant(contents)) .focusable(false) .font(.system(.body, design: .monospaced)) - + HStack { Spacer() Button(Action.text(.cancel, request)) { onCancel() } @@ -74,7 +74,7 @@ struct ClipboardConfirmationView: View { // If we didn't unhide anything, we just send an unhide to be safe. // I don't think the count can go negative on NSCursor so this handles // scenarios cursor is hidden outside of our own NSCursor usage. - if (cursorHiddenCount == 0) { + if cursorHiddenCount == 0 { _ = Cursor.unhide() } } diff --git a/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIcon.swift b/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIcon.swift deleted file mode 100644 index 58de8f7710d..00000000000 --- a/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIcon.swift +++ /dev/null @@ -1,55 +0,0 @@ -import Cocoa - -struct ColorizedGhosttyIcon { - /// The colors that make up the gradient of the screen. - let screenColors: [NSColor] - - /// The color of the ghost. - let ghostColor: NSColor - - /// The frame type to use - let frame: Ghostty.MacOSIconFrame - - /// Make a custom colorized ghostty icon. - func makeImage() -> NSImage? { - // All of our layers (not in order) - guard let screen = NSImage(named: "CustomIconScreen") else { return nil } - guard let screenMask = NSImage(named: "CustomIconScreenMask") else { return nil } - guard let ghost = NSImage(named: "CustomIconGhost") else { return nil } - guard let crt = NSImage(named: "CustomIconCRT") else { return nil } - guard let gloss = NSImage(named: "CustomIconGloss") else { return nil } - - let baseName = switch (frame) { - case .aluminum: "CustomIconBaseAluminum" - case .beige: "CustomIconBaseBeige" - case .chrome: "CustomIconBaseChrome" - case .plastic: "CustomIconBasePlastic" - } - guard let base = NSImage(named: baseName) else { return nil } - - // Apply our color in various ways to our layers. - // NOTE: These functions are not built-in, they're implemented as an extension - // to NSImage in NSImage+Extension.swift. - guard let screenGradient = screenMask.gradient(colors: screenColors) else { return nil } - guard let tintedGhost = ghost.tint(color: ghostColor) else { return nil } - - // Combine our layers using the proper blending modes - return.combine(images: [ - base, - screen, - screenGradient, - ghost, - tintedGhost, - crt, - gloss, - ], blendingModes: [ - .normal, - .normal, - .color, - .normal, - .color, - .overlay, - .normal, - ]) - } -} diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 235881ddeb0..10c56f8dd2d 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -23,7 +23,7 @@ struct CommandOption: Identifiable, Hashable { let sortKey: AnySortKey? /// The action to perform when this option is selected. let action: () -> Void - + init( title: String, subtitle: String? = nil, @@ -78,7 +78,7 @@ struct CommandPaletteView: View { ($0.subtitle?.localizedCaseInsensitiveContains(query) ?? false) || colorMatchScore(for: $0.leadingColor, query: query) > 0 } - + // Sort by color match score (higher scores first), then maintain original order return filtered.sorted { a, b in let scoreA = colorMatchScore(for: a.leadingColor, query: query) @@ -106,7 +106,7 @@ struct CommandPaletteView: View { VStack(alignment: .leading, spacing: 0) { CommandPaletteQuery(query: $query, isTextFieldFocused: _isTextFieldFocused) { event in - switch (event) { + switch event { case .exit: isPresented = false @@ -128,7 +128,7 @@ struct CommandPaletteView: View { ? 0 : current + 1 - case .move(_): + case .move: // Unknown, ignore break } @@ -200,20 +200,20 @@ struct CommandPaletteView: View { isTextFieldFocused = isPresented } } - + /// Returns a score (0.0 to 1.0) indicating how well a color matches a search query color name. /// Returns 0 if no color name in the query matches, or if the color is nil. private func colorMatchScore(for color: Color?, query: String) -> Double { guard let color = color else { return 0 } - + let queryLower = query.lowercased() let nsColor = NSColor(color) - + var bestScore: Double = 0 for name in NSColor.colorNames { guard queryLower.contains(name), let systemColor = NSColor(named: name) else { continue } - + let distance = nsColor.distance(to: systemColor) // Max distance in weighted RGB space is ~3.0, so normalize and invert // Use a threshold to determine "close enough" matches @@ -223,15 +223,15 @@ struct CommandPaletteView: View { bestScore = max(bestScore, score) } } - + return bestScore } } /// The text field for building the query for the command palette. -fileprivate struct CommandPaletteQuery: View { +private struct CommandPaletteQuery: View { @Binding var query: String - var onEvent: ((KeyboardEvent) -> Void)? = nil + var onEvent: ((KeyboardEvent) -> Void)? @FocusState private var isTextFieldFocused: Bool init(query: Binding, isTextFieldFocused: FocusState, onEvent: ((KeyboardEvent) -> Void)? = nil) { @@ -284,7 +284,7 @@ fileprivate struct CommandPaletteQuery: View { } } -fileprivate struct CommandTable: View { +private struct CommandTable: View { var options: [CommandOption] @Binding var selectedIndex: UInt? @Binding var hoveredOptionID: UUID? @@ -332,7 +332,7 @@ fileprivate struct CommandTable: View { } /// A single row in the command palette. -fileprivate struct CommandRow: View { +private struct CommandRow: View { let option: CommandOption var isSelected: Bool @Binding var hoveredID: UUID? @@ -346,26 +346,26 @@ fileprivate struct CommandRow: View { .fill(color) .frame(width: 8, height: 8) } - + if let icon = option.leadingIcon { Image(systemName: icon) .foregroundStyle(option.emphasis ? Color.accentColor : .secondary) .font(.system(size: 14, weight: .medium)) } - + VStack(alignment: .leading, spacing: 2) { Text(option.title) .fontWeight(option.emphasis ? .medium : .regular) - + if let subtitle = option.subtitle { Text(subtitle) .font(.caption) .foregroundStyle(.secondary) } } - + Spacer() - + if let badge = option.badge, !badge.isEmpty { Text(badge) .font(.caption2.weight(.medium)) @@ -376,7 +376,7 @@ fileprivate struct CommandRow: View { ) .foregroundStyle(Color.accentColor) } - + if let symbols = option.symbols { ShortcutSymbolsView(symbols: symbols) .foregroundStyle(.secondary) @@ -406,7 +406,7 @@ fileprivate struct CommandRow: View { } /// A row of Text representing a shortcut. -fileprivate struct ShortcutSymbolsView: View { +private struct ShortcutSymbolsView: View { let symbols: [String] var body: some View { diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index 9bdf4b4ffa6..09e369d4a97 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -11,7 +11,7 @@ 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? @@ -54,13 +54,13 @@ struct TerminalCommandPaletteView: View { } } } - + /// All commands available in the command palette, combining update and terminal options. private var commandOptions: [CommandOption] { var options: [CommandOption] = [] // Updates always appear first options.append(contentsOf: updateOptions) - + // Sort the rest. We replace ":" with a character that sorts before space // so that "Foo:" sorts before "Foo Bar:". Use sortKey as a tie-breaker // for stable ordering when titles are equal. @@ -83,11 +83,11 @@ struct TerminalCommandPaletteView: View { /// Commands for installing or canceling available updates. private var updateOptions: [CommandOption] { var options: [CommandOption] = [] - + guard let updateViewModel, updateViewModel.state.isInstallable else { return options } - + // We override the update available one only because we want to properly // convey it'll go all the way through. let title: String @@ -96,7 +96,7 @@ struct TerminalCommandPaletteView: View { } else { title = updateViewModel.text } - + options.append(CommandOption( title: title, description: updateViewModel.description, @@ -106,14 +106,14 @@ struct TerminalCommandPaletteView: View { ) { (NSApp.delegate as? AppDelegate)?.updateController.installUpdate() }) - + options.append(CommandOption( title: "Cancel or Skip Update", description: "Dismiss the current update process" ) { updateViewModel.state.cancel() }) - + return options } @@ -143,8 +143,15 @@ struct TerminalCommandPaletteView: View { let displayColor = color != TerminalTabColor.none ? color : nil return controller.surfaceTree.map { surface in - let title = surface.title.isEmpty ? window.title : surface.title - let displayTitle = title.isEmpty ? "Untitled" : title + let terminalTitle = surface.title.isEmpty ? window.title : surface.title + let displayTitle: String + if let override = controller.titleOverride, !override.isEmpty { + displayTitle = override + } else if !terminalTitle.isEmpty { + displayTitle = terminalTitle + } else { + displayTitle = "Untitled" + } let pwd = surface.pwd?.abbreviatedPath let subtitle: String? = if let pwd, !displayTitle.contains(pwd) { pwd @@ -171,7 +178,7 @@ struct TerminalCommandPaletteView: View { } /// This is done to ensure that the given view is in the responder chain. -fileprivate struct ResponderChainInjector: NSViewRepresentable { +private struct ResponderChainInjector: NSViewRepresentable { let responder: NSResponder func makeNSView(context: Context) -> NSView { diff --git a/macos/Sources/Features/Custom App Icon/AppIcon.swift b/macos/Sources/Features/Custom App Icon/AppIcon.swift new file mode 100644 index 00000000000..13c6b83a1c7 --- /dev/null +++ b/macos/Sources/Features/Custom App Icon/AppIcon.swift @@ -0,0 +1,86 @@ +import AppKit +import System + +/// The icon style for the Ghostty App. +enum AppIcon: Equatable, Codable { + case official + case blueprint + case chalkboard + case glass + case holographic + case microchip + case paper + case retro + case xray + /// Save full image data to avoid sandboxing issues + case custom(_ iconFile: Data) + case customStyle(_ icon: ColorizedGhosttyIcon) + +#if !DOCK_TILE_PLUGIN + init?(config: Ghostty.Config) { + switch config.macosIcon { + case .official: + return nil + case .blueprint: + self = .blueprint + case .chalkboard: + self = .chalkboard + case .glass: + self = .glass + case .holographic: + self = .holographic + case .microchip: + self = .microchip + case .paper: + self = .paper + case .retro: + self = .retro + case .xray: + self = .xray + case .custom: + if let data = try? Data(contentsOf: URL(filePath: config.macosCustomIcon, relativeTo: nil)) { + self = .custom(data) + } else { + return nil + } + case .customStyle: + // Discard saved icon name + // if no valid colours were found + guard + let ghostColor = config.macosIconGhostColor, + let screenColors = config.macosIconScreenColor + else { + return nil + } + self = .customStyle(ColorizedGhosttyIcon(screenColors: screenColors, ghostColor: ghostColor, frame: config.macosIconFrame)) + } + } +#endif + + func image(in bundle: Bundle) -> NSImage? { + switch self { + case .official: + return nil + case .blueprint: + return bundle.image(forResource: "BlueprintImage")! + case .chalkboard: + return bundle.image(forResource: "ChalkboardImage")! + case .glass: + return bundle.image(forResource: "GlassImage")! + case .holographic: + return bundle.image(forResource: "HolographicImage")! + case .microchip: + return bundle.image(forResource: "MicrochipImage")! + case .paper: + return bundle.image(forResource: "PaperImage")! + case .retro: + return bundle.image(forResource: "RetroImage")! + case .xray: + return bundle.image(forResource: "XrayImage")! + case let .custom(file): + return NSImage(data: file) + case let .customStyle(customIcon): + return customIcon.makeImage(in: bundle) + } + } +} diff --git a/macos/Sources/Features/Custom App Icon/ColorizedGhosttyIcon.swift b/macos/Sources/Features/Custom App Icon/ColorizedGhosttyIcon.swift new file mode 100644 index 00000000000..99d6843693c --- /dev/null +++ b/macos/Sources/Features/Custom App Icon/ColorizedGhosttyIcon.swift @@ -0,0 +1,115 @@ +import Cocoa + +struct ColorizedGhosttyIcon { + /// The colors that make up the gradient of the screen. + let screenColors: [NSColor] + + /// The color of the ghost. + let ghostColor: NSColor + + /// The frame type to use + let frame: Ghostty.MacOSIconFrame + + /// Make a custom colorized ghostty icon. + func makeImage(in bundle: Bundle) -> NSImage? { + // All of our layers (not in order) + guard let screen = bundle.image(forResource: "CustomIconScreen") else { return nil } + guard let screenMask = bundle.image(forResource: "CustomIconScreenMask") else { return nil } + guard let ghost = bundle.image(forResource: "CustomIconGhost") else { return nil } + guard let crt = bundle.image(forResource: "CustomIconCRT") else { return nil } + guard let gloss = bundle.image(forResource: "CustomIconGloss") else { return nil } + + let baseName = switch frame { + case .aluminum: "CustomIconBaseAluminum" + case .beige: "CustomIconBaseBeige" + case .chrome: "CustomIconBaseChrome" + case .plastic: "CustomIconBasePlastic" + } + guard let base = bundle.image(forResource: baseName) else { return nil } + + // Apply our color in various ways to our layers. + // NOTE: These functions are not built-in, they're implemented as an extension + // to NSImage in NSImage+Extension.swift. + guard let screenGradient = screenMask.gradient(colors: screenColors) else { return nil } + guard let tintedGhost = ghost.tint(color: ghostColor) else { return nil } + + // Combine our layers using the proper blending modes + return.combine(images: [ + base, + screen, + screenGradient, + ghost, + tintedGhost, + crt, + gloss, + ], blendingModes: [ + .normal, + .normal, + .color, + .normal, + .color, + .overlay, + .normal, + ]) + } +} + +// MARK: Codable + +extension ColorizedGhosttyIcon: Codable { + private enum CodingKeys: String, CodingKey { + case version + case screenColors + case ghostColor + case frame + + static let currentVersion: Int = 1 + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + // If no version exists then this is the legacy v0 format. + let version = try container.decodeIfPresent(Int.self, forKey: .version) ?? 0 + guard version == 0 || version == CodingKeys.currentVersion else { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unsupported ColorizedGhosttyIcon version: \(version)" + ) + ) + } + + let screenColorHexes = try container.decode([String].self, forKey: .screenColors) + let screenColors = screenColorHexes.compactMap(NSColor.init(hex:)) + let ghostColorHex = try container.decode(String.self, forKey: .ghostColor) + guard let ghostColor = NSColor(hex: ghostColorHex) else { + throw DecodingError.dataCorruptedError( + forKey: .ghostColor, + in: container, + debugDescription: "Failed to decode ghost color from \(ghostColorHex)" + ) + } + let frame = try container.decode(Ghostty.MacOSIconFrame.self, forKey: .frame) + self.init(screenColors: screenColors, ghostColor: ghostColor, frame: frame) + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(CodingKeys.currentVersion, forKey: .version) + try container.encode(screenColors.compactMap(\.hexString), forKey: .screenColors) + try container.encode(ghostColor.hexString, forKey: .ghostColor) + try container.encode(frame, forKey: .frame) + } + +} + +// MARK: Equatable + +extension ColorizedGhosttyIcon: Equatable { + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.frame == rhs.frame && + lhs.screenColors.compactMap(\.hexString) == rhs.screenColors.compactMap(\.hexString) && + lhs.ghostColor.hexString == rhs.ghostColor.hexString + } +} diff --git a/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIconImage.swift b/macos/Sources/Features/Custom App Icon/ColorizedGhosttyIconImage.swift similarity index 100% rename from macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIconImage.swift rename to macos/Sources/Features/Custom App Icon/ColorizedGhosttyIconImage.swift diff --git a/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIconView.swift b/macos/Sources/Features/Custom App Icon/ColorizedGhosttyIconView.swift similarity index 89% rename from macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIconView.swift rename to macos/Sources/Features/Custom App Icon/ColorizedGhosttyIconView.swift index 8fbebfdc80e..7271c595fec 100644 --- a/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIconView.swift +++ b/macos/Sources/Features/Custom App Icon/ColorizedGhosttyIconView.swift @@ -8,6 +8,6 @@ struct ColorizedGhosttyIconView: View { screenColors: [.purple, .blue], ghostColor: .yellow, frame: .aluminum - ).makeImage()!) + ).makeImage(in: .main)!) } } diff --git a/macos/Sources/Features/Custom App Icon/DockTilePlugin.swift b/macos/Sources/Features/Custom App Icon/DockTilePlugin.swift new file mode 100644 index 00000000000..990cd8bb242 --- /dev/null +++ b/macos/Sources/Features/Custom App Icon/DockTilePlugin.swift @@ -0,0 +1,143 @@ +import AppKit + +class DockTilePlugin: NSObject, NSDockTilePlugIn { + // WARNING: An instance of this class is alive as long as Ghostty's icon is + // in the doc (running or not!), so keep any state and processing to a + // minimum to respect resource usage. + + private let pluginBundle = Bundle(for: DockTilePlugin.self) + + // Separate defaults based on debug vs release builds so we can test icons + // without messing up releases. + #if DEBUG + private let ghosttyUserDefaults = UserDefaults(suiteName: "com.mitchellh.ghostty.debug") + #else + private let ghosttyUserDefaults = UserDefaults(suiteName: "com.mitchellh.ghostty") + #endif + + private var iconChangeObserver: Any? + + /// The URL to the enclosing app bundle, determined from the plugin bundle path. + var ghosttyAppURL: URL? { + Self.appBundleURL(for: pluginBundle.bundleURL) + } + + /// Determine the enclosing app bundle for the dock tile plugin bundle. + /// + /// We intentionally avoid matching a specific bundle name (such as + /// "Ghostty.app") so renaming the app in Finder still works. + static func appBundleURL(for pluginBundleURL: URL) -> URL? { + var url = pluginBundleURL + while true { + if url.pathExtension.compare("app", options: .caseInsensitive) == .orderedSame { + return url + } + + let parent = url.deletingLastPathComponent() + if parent.path == url.path { + // Safety stop: this should only happen at filesystem root. + return nil + } + + url = parent + } + } + + /// The primary NSDockTilePlugin function. + func setDockTile(_ dockTile: NSDockTile?) { + // If no dock tile or no access to Ghostty defaults, we can't do anything. + guard let dockTile, let ghosttyUserDefaults else { + iconChangeObserver = nil + return + } + + // Try to restore the previous icon on launch. + iconDidChange(ghosttyUserDefaults.appIcon, dockTile: dockTile) + + // Setup a new observer for when the icon changes so we can update. This message + // is sent by the primary Ghostty app. + iconChangeObserver = DistributedNotificationCenter + .default() + .publisher(for: .ghosttyIconDidChange) + .map { [weak self] _ in self?.ghosttyUserDefaults?.appIcon } + .receive(on: DispatchQueue.global()) + .sink { [weak self] newIcon in self?.iconDidChange(newIcon, dockTile: dockTile) } + } + + private func iconDidChange(_ newIcon: AppIcon?, dockTile: NSDockTile) { + guard let appIcon = newIcon?.image(in: pluginBundle) else { + resetIcon(dockTile: dockTile) + return + } + + if let appBundleURL = self.ghosttyAppURL { + let appBundlePath = appBundleURL.path + NSWorkspace.shared.setIcon(appIcon, forFile: appBundlePath) + NSWorkspace.shared.noteFileSystemChanged(appBundlePath) + } + + dockTile.setIcon(appIcon) + } + + /// Reset the application icon and dock tile icon to the default. + private func resetIcon(dockTile: NSDockTile) { + let appBundlePath = self.ghosttyAppURL?.path + let appIcon: NSImage + if #available(macOS 26.0, *) { + // Reset to the default (glassy) icon. + if let appBundlePath { + NSWorkspace.shared.setIcon(nil, forFile: appBundlePath) + } + + #if DEBUG + // Use the `Blueprint` icon to distinguish Debug from Release builds. + appIcon = pluginBundle.image(forResource: "BlueprintImage")! + #else + // Get the composed icon from the app bundle. + if let appBundlePath, + let iconRep = NSWorkspace.shared.icon(forFile: appBundlePath) + .bestRepresentation( + for: CGRect(origin: .zero, size: dockTile.size), + context: nil, + hints: nil + ) { + appIcon = NSImage(size: dockTile.size) + appIcon.addRepresentation(iconRep) + } else { + // If something unexpected happens on macOS 26, + // fall back to a bundled icon. + appIcon = pluginBundle.image(forResource: "AppIconImage")! + } + #endif + } else { + // Use the bundled icon to keep the corner radius consistent with pre-Tahoe apps. + appIcon = pluginBundle.image(forResource: "AppIconImage")! + if let appBundlePath { + NSWorkspace.shared.setIcon(appIcon, forFile: appBundlePath) + } + } + + // Notify Finder/Dock so icon caches refresh immediately. + if let appBundlePath { + NSWorkspace.shared.noteFileSystemChanged(appBundlePath) + } + dockTile.setIcon(appIcon) + } +} + +private extension NSDockTile { + func setIcon(_ newIcon: NSImage) { + // Update the Dock tile on the main thread. + DispatchQueue.main.async { + let iconView = NSImageView(frame: CGRect(origin: .zero, size: self.size)) + iconView.wantsLayer = true + iconView.image = newIcon + self.contentView = iconView + self.display() + } + } +} + +// This is required because of the DispatchQueue call above. This doesn't +// feel right but I don't know a better way to solve this. +extension NSDockTile: @unchecked @retroactive Sendable {} diff --git a/macos/Sources/Features/Custom App Icon/Extensions/Notification+AppIcon.swift b/macos/Sources/Features/Custom App Icon/Extensions/Notification+AppIcon.swift new file mode 100644 index 00000000000..e492f1a7759 --- /dev/null +++ b/macos/Sources/Features/Custom App Icon/Extensions/Notification+AppIcon.swift @@ -0,0 +1,5 @@ +import AppKit + +extension Notification.Name { + static let ghosttyIconDidChange = Notification.Name("com.mitchellh.ghostty.iconDidChange") +} diff --git a/macos/Sources/Features/Custom App Icon/Extensions/UserDefaults+AppIcon.swift b/macos/Sources/Features/Custom App Icon/Extensions/UserDefaults+AppIcon.swift new file mode 100644 index 00000000000..d15644c9314 --- /dev/null +++ b/macos/Sources/Features/Custom App Icon/Extensions/UserDefaults+AppIcon.swift @@ -0,0 +1,29 @@ +import AppKit + +extension UserDefaults { + private static let customIconKeyOld = "CustomGhosttyIcon" + private static let customIconKeyNew = "CustomGhosttyIcon2" + + var appIcon: AppIcon? { + get { + // Always remove our old pre-docktileplugin values. + defer { + removeObject(forKey: Self.customIconKeyOld) + } + + // Check if we have the new key for our dock tile plugin format. + guard let data = data(forKey: Self.customIconKeyNew) else { + return nil + } + return try? JSONDecoder().decode(AppIcon.self, from: data) + } + + set { + guard let newData = try? JSONEncoder().encode(newValue) else { + return + } + + set(newData, forKey: Self.customIconKeyNew) + } + } +} diff --git a/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift b/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift index ae77535bea2..9d4023c2e38 100644 --- a/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift +++ b/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift @@ -16,11 +16,11 @@ class GlobalEventTap { // The event tap used for global event listening. This is non-nil if it is // created. - private var eventTap: CFMachPort? = nil + private var eventTap: CFMachPort? // This is the timer used to retry enabling the global event tap if we // don't have permissions. - private var enableTimer: Timer? = nil + private var enableTimer: Timer? // Private init so it can't be constructed outside of our singleton private init() {} @@ -33,7 +33,7 @@ class GlobalEventTap { // If enabling fails due to permissions, this will start a timer to retry since // accessibility permissions take affect immediately. func enable() { - if (eventTap != nil) { + if eventTap != nil { // Already enabled return } @@ -44,7 +44,7 @@ class GlobalEventTap { } // Try to enable the event tap immediately. If this succeeds then we're done! - if (tryEnable()) { + if tryEnable() { return } @@ -117,7 +117,7 @@ class GlobalEventTap { } } -fileprivate func cgEventFlagsChangedHandler( +private func cgEventFlagsChangedHandler( proxy: CGEventTapProxy, type: CGEventType, cgEvent: CGEvent, @@ -142,7 +142,7 @@ fileprivate func cgEventFlagsChangedHandler( // Build our event input and call ghostty let key_ev = event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS) - if (ghostty_app_key(ghostty, key_ev)) { + if ghostty_app_key(ghostty, key_ev) { GlobalEventTap.logger.info("global key event handled event=\(event)") return nil } diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 07c0c4c1987..214ff08d3d3 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -16,20 +16,20 @@ class QuickTerminalController: BaseTerminalController { /// The previously running application when the terminal is shown. This is NEVER Ghostty. /// If this is set then when the quick terminal is animated out then we will restore this /// application to the front. - private var previousApp: NSRunningApplication? = nil + private var previousApp: NSRunningApplication? // The active space when the quick terminal was last shown. - private var previousActiveSpace: CGSSpace? = nil + private var previousActiveSpace: CGSSpace? /// Cache for per-screen window state. let screenStateCache: QuickTerminalScreenStateCache /// Non-nil if we have hidden dock state. - private var hiddenDock: HiddenDock? = nil + private var hiddenDock: HiddenDock? /// The configuration derived from the Ghostty config so we don't need to rely on references. private var derivedConfig: DerivedConfig - + /// Tracks if we're currently handling a manual resize to prevent recursion private var isHandlingResize: Bool = false @@ -135,14 +135,12 @@ class QuickTerminalController: BaseTerminalController { if let qtWindow = window as? QuickTerminalWindow { qtWindow.initialFrame = window.frame } - + // Setup our content - window.contentView = TerminalViewContainer( - ghostty: self.ghostty, - viewModel: self, - delegate: self - ) - + window.contentView = TerminalViewContainer { + TerminalView(ghostty: ghostty, viewModel: self, delegate: self) + } + // Clear out our frame at this point, the fixup from above is complete. if let qtWindow = window as? QuickTerminalWindow { qtWindow.initialFrame = nil @@ -161,6 +159,8 @@ class QuickTerminalController: BaseTerminalController { // applies if we can be seen. guard visible else { return } + terminalViewContainer?.updateGlassTintOverlay(isKeyWindow: true) + // Re-hide the dock if we were hiding it before. hiddenDock?.hide() } @@ -174,6 +174,8 @@ class QuickTerminalController: BaseTerminalController { // ensures we don't run logic twice. guard visible else { return } + terminalViewContainer?.updateGlassTintOverlay(isKeyWindow: false) + // We don't animate out if there is a modal sheet being shown currently. // This lets us show alerts without causing the window to disappear. guard window?.attachedSheet == nil else { return } @@ -234,7 +236,7 @@ class QuickTerminalController: BaseTerminalController { // Prevent recursive loops isHandlingResize = true defer { isHandlingResize = false } - + switch position { case .top, .bottom, .center: // For centered positions (top, bottom, center), we need to recenter the window @@ -316,7 +318,7 @@ class QuickTerminalController: BaseTerminalController { // MARK: Methods func toggle() { - if (visible) { + if visible { animateOut() } else { animateIn() @@ -340,8 +342,7 @@ class QuickTerminalController: BaseTerminalController { // we want to store it so we can restore state later. if !NSApp.isActive { if let previousApp = NSWorkspace.shared.frontmostApplication, - previousApp.bundleIdentifier != Bundle.main.bundleIdentifier - { + previousApp.bundleIdentifier != Bundle.main.bundleIdentifier { self.previousApp = previousApp } } @@ -370,7 +371,7 @@ class QuickTerminalController: BaseTerminalController { } else { var config = Ghostty.SurfaceConfiguration() config.environmentVariables["GHOSTTY_QUICK_TERMINAL"] = "1" - + let view = Ghostty.SurfaceView(ghostty_app, baseConfig: config) surfaceTree = SplitTree(view: view) focusedSurface = view @@ -417,7 +418,7 @@ 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 from the cache. let closedFrame = screenStateCache.frame(for: screen) @@ -441,7 +442,7 @@ class QuickTerminalController: BaseTerminalController { // If our dock position would conflict with our target location then // we autohide the dock. if position.conflictsWithDock(on: screen) { - if (hiddenDock == nil) { + if hiddenDock == nil { hiddenDock = .init() } @@ -624,6 +625,8 @@ class QuickTerminalController: BaseTerminalController { window.isOpaque = true window.backgroundColor = .windowBackgroundColor } + + terminalViewContainer?.ghosttyConfigDidChange(ghostty.config, preferredBackgroundColor: nil) } private func showNoNewTabAlert() { @@ -675,10 +678,10 @@ class QuickTerminalController: BaseTerminalController { // We ignore the configured fullscreen style and always use non-native // because the way the quick terminal works doesn't support native. let mode: FullscreenMode - if (NSApp.isFrontmost) { + if NSApp.isFrontmost { // If we're frontmost and we have a notch then we keep padding // so all lines of the terminal are visible. - if (window?.screen?.hasNotch ?? false) { + if window?.screen?.hasNotch ?? false { mode = .nonNativePaddedNotch } else { mode = .nonNative @@ -707,6 +710,8 @@ class QuickTerminalController: BaseTerminalController { self.derivedConfig = DerivedConfig(config) syncAppearance() + + terminalViewContainer?.ghosttyConfigDidChange(config, preferredBackgroundColor: nil) } @objc private func onNewTab(notification: SwiftUI.Notification) { diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift index d7660f77ab2..8742a7836dd 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift @@ -1,6 +1,6 @@ import Cocoa -enum QuickTerminalPosition : String { +enum QuickTerminalPosition: String { case top case bottom case left @@ -64,7 +64,7 @@ enum QuickTerminalPosition : String { /// The initial point origin for this position. func initialOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { - switch (self) { + switch self { case .top: return .init( x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), @@ -86,13 +86,13 @@ enum QuickTerminalPosition : String { y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)) case .center: - return .init(x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: screen.visibleFrame.height - window.frame.width) + return .init(x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: screen.visibleFrame.height - window.frame.width) } } /// The final point origin for this position. func finalOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { - switch (self) { + switch self { case .top: return .init( x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), @@ -128,7 +128,7 @@ enum QuickTerminalPosition : String { // Depending on the orientation of the dock, we conflict if our quick terminal // would potentially "hit" the dock. In the future we should probably consider // the frame of the quick terminal. - return switch (orientation) { + return switch orientation { case .top: self == .top || self == .left || self == .right case .bottom: self == .bottom || self == .left || self == .right case .left: self == .top || self == .bottom @@ -144,25 +144,25 @@ enum QuickTerminalPosition : String { x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: window.frame.origin.y // Keep the same Y position ) - + case .bottom: return CGPoint( x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: window.frame.origin.y // Keep the same Y position ) - + case .center: return CGPoint( x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2) ) - + case .left, .right: // For left/right positions, only adjust horizontal centering if needed return window.frame.origin } } - + /// Calculate the vertically centered origin for side-positioned windows func verticallyCenteredOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { switch self { @@ -171,13 +171,13 @@ enum QuickTerminalPosition : String { x: window.frame.origin.x, // Keep the same X position y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2) ) - + case .right: return CGPoint( x: window.frame.origin.x, // Keep the same X position y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2) ) - + case .top, .bottom, .center: // These positions don't need vertical recentering during resize return window.frame.origin diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalScreen.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalScreen.swift index cd07a6f1205..70af0a5059a 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalScreen.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalScreen.swift @@ -6,23 +6,23 @@ enum QuickTerminalScreen { case menuBar init?(fromGhosttyConfig string: String) { - switch (string) { + switch string { case "main": self = .main case "mouse": self = .mouse - + case "macos-menu-bar": self = .menuBar - + default: return nil } } var screen: NSScreen? { - switch (self) { + switch self { case .main: return NSScreen.main diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift index a1c17abb922..301865561cc 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift @@ -8,15 +8,15 @@ import Cocoa /// to survive NSScreen garbage collection and automatically prunes stale entries. class QuickTerminalScreenStateCache { typealias Entries = [UUID: DisplayEntry] - + /// The maximum number of saved screen states we retain. This is to avoid some kind of /// pathological memory growth in case we get our screen state serializing wrong. I don't /// know anyone with more than 10 screens, so let's just arbitrarily go with that. 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(set) var stateByDisplay: Entries = [:] @@ -28,11 +28,11 @@ class QuickTerminalScreenStateCache { 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 } @@ -45,27 +45,27 @@ class QuickTerminalScreenStateCache { 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] { @@ -80,15 +80,15 @@ class QuickTerminalScreenStateCache { } } } - + // 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 @@ -98,13 +98,13 @@ class QuickTerminalScreenStateCache { stateByDisplay.removeValue(forKey: key) } } - + struct DisplayEntry: Codable { 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. diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift index 08bbcb8d9b2..2cd11e42e98 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift @@ -48,7 +48,6 @@ struct QuickTerminalSize { } } - /// This is an almost direct port of th Zig function QuickTerminalSize.calculate func calculate(position: QuickTerminalPosition, screenDimensions: CGSize) -> CGSize { let dims = CGSize(width: screenDimensions.width, height: screenDimensions.height) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalSpaceBehavior.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalSpaceBehavior.swift index 0561aaa1888..176cbf1606f 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalSpaceBehavior.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalSpaceBehavior.swift @@ -6,15 +6,15 @@ enum QuickTerminalSpaceBehavior { case move init?(fromGhosttyConfig string: String) { - switch (string) { - case "move": - self = .move + switch string { + case "move": + self = .move - case "remain": - self = .remain + case "remain": + self = .remain - default: - return nil + default: + return nil } } @@ -24,13 +24,13 @@ enum QuickTerminalSpaceBehavior { .fullScreenAuxiliary ] - switch (self) { - case .move: - // We want this to move the window to the active space. - return NSWindow.CollectionBehavior([.canJoinAllSpaces] + commonBehavior) - case .remain: - // We want this to remain the window in the current space. - return NSWindow.CollectionBehavior([.moveToActiveSpace] + commonBehavior) + switch self { + case .move: + // We want this to move the window to the active space. + return NSWindow.CollectionBehavior([.canJoinAllSpaces] + commonBehavior) + case .remain: + // We want this to remain the window in the current space. + return NSWindow.CollectionBehavior([.moveToActiveSpace] + commonBehavior) } } } diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift index 1a4170dbce4..507ec1baf0a 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift @@ -5,18 +5,18 @@ class QuickTerminalWindow: NSPanel { // still become key/main and receive events. override var canBecomeKey: Bool { return true } override var canBecomeMain: Bool { return true } - + override func awakeFromNib() { super.awakeFromNib() // Note: almost all of this stuff can be done in the nib/xib directly // but I prefer to do it programmatically because the properties we // care about are less hidden. - + // Add a custom identifier so third party apps can use the Accessibility // API to apply special rules to the quick terminal. self.identifier = .init(rawValue: "com.mitchellh.ghostty.quickTerminal") - + // Set the correct AXSubrole of kAXFloatingWindowSubrole (allows // AeroSpace to treat the Quick Terminal as a floating window) self.setAccessibilitySubrole(.floatingWindow) @@ -32,8 +32,8 @@ class QuickTerminalWindow: NSPanel { /// This is set to the frame prior to setting `contentView`. This is purely a hack to workaround /// bugs in older macOS versions (Ventura): https://github.com/ghostty-org/ghostty/pull/8026 - var initialFrame: NSRect? = nil - + var initialFrame: NSRect? + override func setFrame(_ frameRect: NSRect, display flag: Bool) { // Upon first adding this Window to its host view, older SwiftUI // seems to have a "hiccup" and corrupts the frameRect, diff --git a/macos/Sources/Features/Secure Input/SecureInput.swift b/macos/Sources/Features/Secure Input/SecureInput.swift index f999ce5caed..261a38e5c48 100644 --- a/macos/Sources/Features/Secure Input/SecureInput.swift +++ b/macos/Sources/Features/Secure Input/SecureInput.swift @@ -12,7 +12,7 @@ import OSLog // it. You have to yield secure input on application deactivation (because // it'll affect other apps) and reacquire on reactivation, and every enable // needs to be balanced with a disable. -class SecureInput : ObservableObject { +class SecureInput: ObservableObject { static let shared = SecureInput() private static let logger = Logger( @@ -90,12 +90,12 @@ class SecureInput : ObservableObject { guard enabled != desired else { return } let err: OSStatus - if (enabled) { + if enabled { err = DisableSecureEventInput() } else { err = EnableSecureEventInput() } - if (err == noErr) { + if err == noErr { enabled = desired Self.logger.debug("secure input state=\(self.enabled)") return @@ -111,7 +111,7 @@ class SecureInput : ObservableObject { // desire to be enabled. guard !enabled && desired else { return } let err = EnableSecureEventInput() - if (err == noErr) { + if err == noErr { enabled = true Self.logger.debug("secure input enabled on activation") return @@ -124,7 +124,7 @@ class SecureInput : ObservableObject { // We only want to disable if we're enabled. guard enabled else { return } let err = DisableSecureEventInput() - if (err == noErr) { + if err == noErr { enabled = false Self.logger.debug("secure input disabled on deactivation") return diff --git a/macos/Sources/Features/Secure Input/SecureInputOverlay.swift b/macos/Sources/Features/Secure Input/SecureInputOverlay.swift index 96f309de54a..ebf5b5138c1 100644 --- a/macos/Sources/Features/Secure Input/SecureInputOverlay.swift +++ b/macos/Sources/Features/Secure Input/SecureInputOverlay.swift @@ -2,8 +2,8 @@ import SwiftUI struct SecureInputOverlay: View { // Animations - @State private var shadowAngle: Angle = .degrees(0) - @State private var shadowWidth: CGFloat = 6 + @State private var gradientAngle: Angle = .degrees(0) + @State private var gradientOpacity: CGFloat = 0.5 // Popover explainer text @State private var isPopover = false @@ -20,18 +20,32 @@ struct SecureInputOverlay: View { .foregroundColor(.primary) .padding(5) .background( - RoundedRectangle(cornerRadius: 12) + Rectangle() .fill(.background) - .innerShadow( - using: RoundedRectangle(cornerRadius: 12), - stroke: AngularGradient( - gradient: Gradient(colors: [.cyan, .blue, .yellow, .blue, .cyan]), - center: .center, - angle: shadowAngle - ), - width: shadowWidth + .overlay( + Rectangle() + .fill( + AngularGradient( + gradient: Gradient( + colors: [.cyan, .blue, .yellow, .blue, .cyan] + ), + center: .center, + angle: gradientAngle + ) + ) + .blur(radius: 4, opaque: true) + .mask( + RadialGradient( + colors: [.clear, .black], + center: .center, + startRadius: 0, + endRadius: 25 + ) + ) + .opacity(gradientOpacity) ) ) + .mask(RoundedRectangle(cornerRadius: 12)) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(Color.gray, lineWidth: 1) @@ -44,9 +58,9 @@ struct SecureInputOverlay: View { .padding(.trailing, 10) .popover(isPresented: $isPopover, arrowEdge: .bottom) { Text(""" - Secure Input is active. Secure Input is a macOS security feature that - prevents applications from reading keyboard events. This is enabled - automatically whenever Ghostty detects a password prompt in the terminal, + Secure Input is active. Secure Input is a macOS security feature that + prevents applications from reading keyboard events. This is enabled + automatically whenever Ghostty detects a password prompt in the terminal, or at all times if `Ghostty > Secure Keyboard Entry` is active. """) .padding(.all) @@ -57,11 +71,11 @@ struct SecureInputOverlay: View { } .onAppear { withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) { - shadowAngle = .degrees(360) + gradientAngle = .degrees(360) } withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: true)) { - shadowWidth = 12 + gradientOpacity = 1 } } } diff --git a/macos/Sources/Features/Services/ServiceProvider.swift b/macos/Sources/Features/Services/ServiceProvider.swift index f165769a78d..9bf46fcf98a 100644 --- a/macos/Sources/Features/Services/ServiceProvider.swift +++ b/macos/Sources/Features/Services/ServiceProvider.swift @@ -50,7 +50,7 @@ class ServiceProvider: NSObject { var config = Ghostty.SurfaceConfiguration() config.workingDirectory = url.path(percentEncoded: false) - switch (target) { + switch target { case .window: _ = TerminalController.newWindow(delegate.ghostty, withBaseConfig: config) diff --git a/macos/Sources/Features/Settings/ConfigurationErrorsController.swift b/macos/Sources/Features/Settings/ConfigurationErrorsController.swift index b2550b94ed9..06fcebda397 100644 --- a/macos/Sources/Features/Settings/ConfigurationErrorsController.swift +++ b/macos/Sources/Features/Settings/ConfigurationErrorsController.swift @@ -12,13 +12,13 @@ class ConfigurationErrorsController: NSWindowController, NSWindowDelegate, Confi /// The data model for this view. Update this directly and the associated view will be updated, too. @Published var errors: [String] = [] { didSet { - if (errors.count == 0) { + if errors.count == 0 { self.window?.performClose(nil) } } } - //MARK: - NSWindowController + // MARK: - NSWindowController override func windowWillLoad() { shouldCascadeWindows = false diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index 2fb83e64c22..30caae0da62 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -1,4 +1,5 @@ import AppKit +import Combine /// SplitTree represents a tree of views that can be divided. struct SplitTree { @@ -222,7 +223,7 @@ extension SplitTree { case .split: // If the best candidate is a split node, use its the leaf/rightmost // depending on our spatial direction. - return switch (spatialDirection) { + return switch spatialDirection { case .up, .left: bestNode.node.leftmostLeaf() case .down, .right: bestNode.node.rightmostLeaf() } @@ -343,7 +344,7 @@ extension SplitTree { // MARK: SplitTree Codable -fileprivate enum CodingKeys: String, CodingKey { +private enum CodingKeys: String, CodingKey { case version case root case zoomed @@ -422,7 +423,7 @@ extension SplitTree.Node { /// Returns the node in the tree that contains the given view. func node(view: ViewType) -> Node? { - switch (self) { + switch self { case .leaf(view): return self @@ -728,7 +729,6 @@ extension SplitTree.Node { } } - /// Calculate the bounds of all views in this subtree based on split ratios func calculateViewBounds(in bounds: CGRect) -> [(view: ViewType, bounds: CGRect)] { switch self { @@ -1216,6 +1216,57 @@ extension SplitTree: Collection { } } +// MARK: SplitTree Combine + +extension SplitTree { + /// Builds a publisher that emits current values for all leaf views keyed by view ID. + /// + /// The returned publisher emits a full `[ViewType.ID: Value]` snapshot whenever any leaf view + /// publishes through the provided publisher key path. + func valuesPublisher( + valueKeyPath: KeyPath, + publisherKeyPath: KeyPath.Publisher> + ) -> AnyPublisher<[ViewType.ID: Value], Never> { + // Flatten the split tree into a list of current leaf views. + let views = map { $0 } + guard !views.isEmpty else { + // If there are no leaves, immediately publish an empty snapshot. + // `Just([:])` keeps the return type simple and makes downstream usage easy. + return Just([:]).eraseToAnyPublisher() + } + + // Capture each view's current value up front. + // We key by `ViewType.ID` so updates can replace the correct entry later. + // This avoids waiting for all views to emit before consumers see data. + let initial = Dictionary(uniqueKeysWithValues: views.map { view in + (view.id, view[keyPath: valueKeyPath]) + }) + + // Build one publisher per view from the requested key path. + // Each emission is mapped into `(id, value)` so we know which entry changed. + // `MergeMany` combines all per-view streams into a single update stream. + let updates = Publishers.MergeMany(views.map { view in + view[keyPath: publisherKeyPath] + .map { (view.id, $0) } + .eraseToAnyPublisher() + }) + + return updates + // Accumulate updates into a full "latest value per ID" dictionary. + // This turns incremental events into complete state snapshots. + .scan(initial) { state, update in + var state = state + state[update.0] = update.1 + return state + } + // Emit the initial snapshot first so subscribers always get a + // complete value dictionary immediately upon subscription. + .prepend(initial) + // Hide implementation details and expose a stable API type. + .eraseToAnyPublisher() + } +} + // MARK: Structural Identity extension SplitTree.Node { diff --git a/macos/Sources/Features/Splits/SplitView.Divider.swift b/macos/Sources/Features/Splits/SplitView.Divider.swift index a01175dce91..59a10ef60e2 100644 --- a/macos/Sources/Features/Splits/SplitView.Divider.swift +++ b/macos/Sources/Features/Splits/SplitView.Divider.swift @@ -10,7 +10,7 @@ extension SplitView { @Binding var split: CGFloat private var visibleWidth: CGFloat? { - switch (direction) { + switch direction { case .horizontal: return visibleSize case .vertical: @@ -19,7 +19,7 @@ extension SplitView { } private var visibleHeight: CGFloat? { - switch (direction) { + switch direction { case .horizontal: return nil case .vertical: @@ -28,7 +28,7 @@ extension SplitView { } private var invisibleWidth: CGFloat? { - switch (direction) { + switch direction { case .horizontal: return visibleSize + invisibleSize case .vertical: @@ -37,7 +37,7 @@ extension SplitView { } private var invisibleHeight: CGFloat? { - switch (direction) { + switch direction { case .horizontal: return nil case .vertical: @@ -46,7 +46,7 @@ extension SplitView { } private var pointerStyle: BackportPointerStyle { - return switch (direction) { + return switch direction { case .horizontal: .resizeLeftRight case .vertical: .resizeUpDown } @@ -69,8 +69,8 @@ extension SplitView { return } - if (isHovered) { - switch (direction) { + if isHovered { + switch direction { case .horizontal: NSCursor.resizeLeftRight.push() case .vertical: diff --git a/macos/Sources/Features/Splits/SplitView.swift b/macos/Sources/Features/Splits/SplitView.swift index 42de9759058..a19fdca6a85 100644 --- a/macos/Sources/Features/Splits/SplitView.swift +++ b/macos/Sources/Features/Splits/SplitView.swift @@ -90,7 +90,7 @@ struct SplitView: View { private func dragGesture(_ size: CGSize, splitterPoint: CGPoint) -> some Gesture { return DragGesture() .onChanged { gesture in - switch (direction) { + switch direction { case .horizontal: let new = min(max(minSize, gesture.location.x), size.width - minSize) split = new / size.width @@ -106,14 +106,14 @@ struct SplitView: View { private func leftRect(for size: CGSize) -> CGRect { // Initially the rect is the full size var result = CGRect(x: 0, y: 0, width: size.width, height: size.height) - switch (direction) { + switch direction { case .horizontal: - result.size.width = result.size.width * split + result.size.width *= split result.size.width -= splitterVisibleSize / 2 result.size.width -= result.size.width.truncatingRemainder(dividingBy: self.resizeIncrements.width) case .vertical: - result.size.height = result.size.height * split + result.size.height *= split result.size.height -= splitterVisibleSize / 2 result.size.height -= result.size.height.truncatingRemainder(dividingBy: self.resizeIncrements.height) } @@ -125,7 +125,7 @@ struct SplitView: View { private func rightRect(for size: CGSize, leftRect: CGRect) -> CGRect { // Initially the rect is the full size var result = CGRect(x: 0, y: 0, width: size.width, height: size.height) - switch (direction) { + switch direction { case .horizontal: // For horizontal layouts we offset the starting X by the left rect // and make the width fit the remaining space. @@ -144,7 +144,7 @@ struct SplitView: View { /// Calculates the point at which the splitter should be rendered. private func splitterPoint(for size: CGSize, leftRect: CGRect) -> CGPoint { - switch (direction) { + switch direction { case .horizontal: return CGPoint(x: leftRect.size.width, y: size.height / 2) @@ -152,9 +152,9 @@ struct SplitView: View { return CGPoint(x: size.width / 2, y: leftRect.size.height) } } - + // MARK: Accessibility - + private var splitViewLabel: String { switch direction { case .horizontal: @@ -163,7 +163,7 @@ struct SplitView: View { return "Vertical split view" } } - + private var leftPaneLabel: String { switch direction { case .horizontal: @@ -172,7 +172,7 @@ struct SplitView: View { return "Top pane" } } - + private var rightPaneLabel: String { switch direction { case .horizontal: diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index 2a42dc599fd..5fa12edebea 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -7,19 +7,19 @@ import SwiftUI enum TerminalSplitOperation { case resize(Resize) case drop(Drop) - + struct Resize { let node: SplitTree.Node let ratio: Double } - + struct Drop { /// The surface being dragged. let payload: Ghostty.SurfaceView - + /// The surface it was dragged onto let destination: Ghostty.SurfaceView - + /// The zone it was dropped to determine how to split the destination. let zone: TerminalSplitDropZone } @@ -44,7 +44,7 @@ struct TerminalSplitTreeView: View { } } -fileprivate struct TerminalSplitSubtreeView: View { +private struct TerminalSplitSubtreeView: View { @EnvironmentObject var ghostty: Ghostty.App let node: SplitTree.Node @@ -52,12 +52,12 @@ fileprivate struct TerminalSplitSubtreeView: View { let action: (TerminalSplitOperation) -> Void var body: some View { - switch (node) { + switch node { case .leaf(let leafView): TerminalSplitLeaf(surfaceView: leafView, isSplit: !isRoot, action: action) case .split(let split): - let splitViewDirection: SplitViewDirection = switch (split.direction) { + let splitViewDirection: SplitViewDirection = switch split.direction { case .horizontal: .horizontal case .vertical: .vertical } @@ -86,14 +86,14 @@ fileprivate struct TerminalSplitSubtreeView: View { } } -fileprivate struct TerminalSplitLeaf: View { +private struct TerminalSplitLeaf: View { let surfaceView: Ghostty.SurfaceView let isSplit: Bool let action: (TerminalSplitOperation) -> Void - + @State private var dropState: DropState = .idle @State private var isSelfDragging: Bool = false - + var body: some View { GeometryReader { geometry in Ghostty.InspectableSurface( @@ -129,26 +129,26 @@ fileprivate struct TerminalSplitLeaf: View { .accessibilityLabel("Terminal pane") } } - + private enum DropState: Equatable { case idle case dropping(TerminalSplitDropZone) } - + private struct SplitDropDelegate: DropDelegate { @Binding var dropState: DropState let viewSize: CGSize let destinationSurface: Ghostty.SurfaceView let action: (TerminalSplitOperation) -> Void - + func validateDrop(info: DropInfo) -> Bool { info.hasItemsConforming(to: [.ghosttySurfaceId]) } - + func dropEntered(info: DropInfo) { dropState = .dropping(.calculate(at: info.location, in: viewSize)) } - + func dropUpdated(info: DropInfo) -> DropProposal? { // For some reason dropUpdated is sent after performDrop is called // and we don't want to reset our drop zone to show it so we have @@ -157,11 +157,11 @@ fileprivate struct TerminalSplitLeaf: View { dropState = .dropping(.calculate(at: info.location, in: viewSize)) return DropProposal(operation: .move) } - + func dropExited(info: DropInfo) { dropState = .idle } - + func performDrop(info: DropInfo) -> Bool { let zone = TerminalSplitDropZone.calculate(at: info.location, in: viewSize) dropState = .idle @@ -169,7 +169,7 @@ fileprivate struct TerminalSplitLeaf: View { // Load the dropped surface asynchronously using Transferable let providers = info.itemProviders(for: [.ghosttySurfaceId]) guard let provider = providers.first else { return false } - + // Capture action before the async closure _ = provider.loadTransferable(type: Ghostty.SurfaceView.self) { [weak destinationSurface] result in switch result { @@ -180,12 +180,12 @@ fileprivate struct TerminalSplitLeaf: View { guard sourceSurface !== destinationSurface else { return } action(.drop(.init(payload: sourceSurface, destination: destinationSurface, zone: zone))) } - + case .failure: break } } - + return true } } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index b739e9ed1c9..d4b0ac0800e 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -31,13 +31,12 @@ class BaseTerminalController: NSWindowController, TerminalViewDelegate, TerminalViewModel, ClipboardConfirmationViewDelegate, - FullscreenDelegate -{ + FullscreenDelegate { /// The app instance that this terminal view will represent. let ghostty: Ghostty.App /// The currently focused surface. - var focusedSurface: Ghostty.SurfaceView? = nil { + var focusedSurface: Ghostty.SurfaceView? { didSet { syncFocusToSurfaceTree() } } @@ -48,29 +47,32 @@ 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 + /// True when any surface in this controller currently has an active bell. + @Published private(set) var bell: Bool = false + /// Whether the terminal surface should focus when the mouse is over it. var focusFollowsMouse: Bool { self.derivedConfig.focusFollowsMouse } /// Non-nil when an alert is active so we don't overlap multiple. - private var alert: NSAlert? = nil + private var alert: NSAlert? /// The clipboard confirmation window, if shown. - private var clipboardConfirmation: ClipboardConfirmationController? = nil + private var clipboardConfirmation: ClipboardConfirmationController? /// Fullscreen state management. private(set) var fullscreenStyle: FullscreenStyle? /// Event monitor (see individual events for why) - private var eventMonitor: Any? = nil + private var eventMonitor: Any? /// The previous frame information from the window - private var savedFrame: SavedFrame? = nil + private var savedFrame: SavedFrame? /// Cache previously applied appearance to avoid unnecessary updates private var appliedColorScheme: ghostty_color_scheme_e? @@ -84,9 +86,12 @@ class BaseTerminalController: NSWindowController, /// The cancellables related to our focused surface. private var focusedSurfaceCancellables: Set = [] + /// Cancellable for aggregating bell state across all surfaces in this controller. + private var bellStateCancellable: AnyCancellable? + /// An override title for the tab/window set by the user via prompt_tab_title. /// When set, this takes precedence over the computed title from the terminal. - var titleOverride: String? = nil { + var titleOverride: String? { didSet { applyTitleToWindow() } } @@ -136,6 +141,9 @@ class BaseTerminalController: NSWindowController, guard let ghostty_app = ghostty.app else { preconditionFailure("app must be loaded") } self.surfaceTree = tree ?? .init(view: Ghostty.SurfaceView(ghostty_app, baseConfig: base)) + // Setup our bell state for the window + setupBellNotificationPublisher() + // Setup our notifications for behaviors let center = NotificationCenter.default center.addObserver( @@ -281,7 +289,7 @@ class BaseTerminalController: NSWindowController, /// Subclasses should call super first. func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { // If our surface tree becomes empty then we have no focused surface. - if (to.isEmpty) { + if to.isEmpty { focusedSurface = nil } } @@ -424,7 +432,7 @@ class BaseTerminalController: NSWindowController, /// Goes to previous split unless we're the leftmost leaf, then goes to next. private func findNextFocusTargetAfterClosing(node: SplitTree.Node) -> Ghostty.SurfaceView? { guard let root = surfaceTree.root else { return nil } - + // If we're the leftmost, then we move to the next surface after closing. // Otherwise, we move to the previous. if root.leftmostLeaf() == node.leftmostLeaf() { @@ -433,7 +441,7 @@ class BaseTerminalController: NSWindowController, return surfaceTree.focusTarget(for: .previous, from: node) } } - + /// Remove a node from the surface tree and move focus appropriately. /// /// This also updates the undo manager to support restoring this node. @@ -471,13 +479,13 @@ class BaseTerminalController: NSWindowController, Ghostty.moveFocus(to: newView, from: oldView) } } - + // Setup our undo guard let undoManager else { return } if let undoAction { undoManager.setActionName(undoAction) } - + undoManager.registerUndo( withTarget: self, expiresAfter: undoExpiration @@ -488,7 +496,7 @@ class BaseTerminalController: NSWindowController, Ghostty.moveFocus(to: oldView, from: target.focusedSurface) } } - + undoManager.registerUndo( withTarget: target, expiresAfter: target.undoExpiration @@ -531,14 +539,14 @@ class BaseTerminalController: NSWindowController, // then we let it stay that way. x: if newFrame.origin.x < visibleFrame.origin.x { if let savedFrame, savedFrame.window.origin.x < savedFrame.screen.origin.x { - break x; + break x } newFrame.origin.x = visibleFrame.origin.x } y: if newFrame.origin.y < visibleFrame.origin.y { if let savedFrame, savedFrame.window.origin.y < savedFrame.screen.origin.y { - break y; + break y } newFrame.origin.y = visibleFrame.origin.y @@ -596,7 +604,7 @@ class BaseTerminalController: NSWindowController, guard let directionAny = notification.userInfo?["direction"] else { return } guard let direction = directionAny as? ghostty_action_split_direction_e else { return } let splitDirection: SplitTree.NewDirection - switch (direction) { + switch direction { case GHOSTTY_SPLIT_DIRECTION_RIGHT: splitDirection = .right case GHOSTTY_SPLIT_DIRECTION_LEFT: splitDirection = .left case GHOSTTY_SPLIT_DIRECTION_DOWN: splitDirection = .down @@ -609,14 +617,14 @@ class BaseTerminalController: NSWindowController, @objc private func ghosttyDidEqualizeSplits(_ notification: Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } - + // Check if target surface is in current controller's tree guard surfaceTree.contains(target) else { return } - + // Equalize the splits surfaceTree = surfaceTree.equalized() } - + @objc private func ghosttyDidFocusSplit(_ notification: Notification) { // The target must be within our tree guard let target = notification.object as? Ghostty.SurfaceView else { return } @@ -628,7 +636,7 @@ class BaseTerminalController: NSWindowController, // 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: direction.toSplitTreeFocusDirection(), from: targetNode) else { return @@ -649,7 +657,7 @@ class BaseTerminalController: NSWindowController, Ghostty.moveFocus(to: nextSurface, from: target) } } - + @objc private func ghosttyDidToggleSplitZoom(_ notification: Notification) { // The target must be within our tree guard let target = notification.object as? Ghostty.SurfaceView else { return } @@ -677,19 +685,19 @@ class BaseTerminalController: NSWindowController, Ghostty.moveFocus(to: target) } } - + @objc private func ghosttyDidResizeSplit(_ notification: Notification) { // The target must be within our tree guard let target = notification.object as? Ghostty.SurfaceView else { return } guard let targetNode = surfaceTree.root?.node(view: target) else { return } - + // Extract direction and amount from notification guard let directionAny = notification.userInfo?[Ghostty.Notification.ResizeSplitDirectionKey] else { return } guard let direction = directionAny as? Ghostty.SplitResizeDirection else { return } - + guard let amountAny = notification.userInfo?[Ghostty.Notification.ResizeSplitAmountKey] else { return } guard let amount = amountAny as? UInt16 else { return } - + // Convert Ghostty.SplitResizeDirection to SplitTree.Spatial.Direction let spatialDirection: SplitTree.Spatial.Direction switch direction { @@ -698,10 +706,10 @@ class BaseTerminalController: NSWindowController, case .left: spatialDirection = .left case .right: spatialDirection = .right } - + // Use viewBounds for the spatial calculation bounds let bounds = CGRect(origin: .zero, size: surfaceTree.viewBounds()) - + // Perform the resize using the new SplitTree resize method do { surfaceTree = try surfaceTree.resizing(node: targetNode, by: amount, in: spatialDirection, with: bounds) @@ -716,7 +724,7 @@ class BaseTerminalController: NSWindowController, // Bring the window to front and focus the surface. window?.makeKeyAndOrderFront(nil) - + // We use a small delay to ensure this runs after any UI cleanup // (e.g., command palette restoring focus to its original surface). Ghostty.moveFocus(to: target) @@ -729,11 +737,11 @@ class BaseTerminalController: NSWindowController, @objc private func ghosttySurfaceDragEndedNoTarget(_ notification: Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } guard let targetNode = surfaceTree.root?.node(view: target) else { return } - + // If our tree isn't split, then we never create a new window, because // it is already a single split. guard surfaceTree.isSplit else { return } - + // If we are removing our focused surface then we move it. We need to // keep track of our old one so undo sends focus back to the right place. let oldFocusedSurface = focusedSurface @@ -746,14 +754,14 @@ class BaseTerminalController: NSWindowController, // Create a new tree with the dragged surface and open a new window let newTree = SplitTree(view: target) - + // Treat our undo below as a full group. undoManager?.beginUndoGrouping() undoManager?.setActionName("Move Split") defer { undoManager?.endUndoGrouping() } - + replaceSurfaceTree(removedTree, moveFocusFrom: oldFocusedSurface) _ = TerminalController.newWindow( ghostty, @@ -783,7 +791,7 @@ class BaseTerminalController: NSWindowController, if NSApp.mainWindow == window { surfaces = surfaces.filter { $0 != focusedSurface } } - + for surface in surfaces { surface.flagsChanged(with: event) } @@ -817,10 +825,10 @@ class BaseTerminalController: NSWindowController, titleDidChange(to: "👻") } } - + private func computeTitle(title: String, bell: Bool) -> String { var result = title - if (bell && ghostty.config.bellFeatures.contains(.title)) { + if bell && ghostty.config.bellFeatures.contains(.title) { result = "🔔 \(result)" } @@ -834,17 +842,17 @@ class BaseTerminalController: NSWindowController, private func applyTitleToWindow() { guard let window else { return } - + if let titleOverride { window.title = computeTitle( title: titleOverride, bell: focusedSurface?.bell ?? false) return } - + window.title = lastComputedTitle } - + func pwdDidChange(to: URL?) { guard let window else { return } @@ -856,7 +864,6 @@ class BaseTerminalController: NSWindowController, } } - func cellSizeDidChange(to: NSSize) { guard derivedConfig.windowStepResize else { return } // Stage manager can sometimes present windows in such a way that the @@ -896,7 +903,7 @@ class BaseTerminalController: NSWindowController, case .left: .left case .right: .right } - + // Check if source is in our tree if let sourceNode = surfaceTree.root?.node(view: source) { // Source is in our tree - same window move @@ -908,7 +915,7 @@ class BaseTerminalController: NSWindowController, Ghostty.logger.warning("failed to insert surface during drop: \(error)") return } - + replaceSurfaceTree( newTree, moveFocusTo: source, @@ -916,7 +923,7 @@ class BaseTerminalController: NSWindowController, undoAction: "Move Split") return } - + // Source is not in our tree - search other windows var sourceController: BaseTerminalController? var sourceNode: SplitTree.Node? @@ -929,12 +936,12 @@ class BaseTerminalController: NSWindowController, break } } - + guard let sourceController, let sourceNode else { Ghostty.logger.warning("source surface not found in any window during drop") return } - + // Remove from source controller's tree and add it to our tree. // We do this first because if there is an error then we can // abort. @@ -945,17 +952,17 @@ class BaseTerminalController: NSWindowController, Ghostty.logger.warning("failed to insert surface during cross-window drop: \(error)") return } - + // Treat our undo below as a full group. undoManager?.beginUndoGrouping() undoManager?.setActionName("Move Split") defer { undoManager?.endUndoGrouping() } - + // Remove the node from the source. sourceController.removeSurfaceNode(sourceNode) - + // Add in the surface to our tree replaceSurfaceTree( newTree, @@ -966,7 +973,7 @@ class BaseTerminalController: NSWindowController, func performAction(_ action: String, on surfaceView: Ghostty.SurfaceView) { guard let surface = surfaceView.surface else { return } let len = action.utf8CString.count - if (len == 0) { return } + if len == 0 { return } _ = action.withCString { cString in ghostty_surface_binding_action(surface, cString, UInt(len - 1)) } @@ -980,17 +987,17 @@ class BaseTerminalController: NSWindowController, func toggleBackgroundOpacity() { // Do nothing if config is already fully opaque guard ghostty.config.backgroundOpacity < 1 else { return } - + // Do nothing if in fullscreen (transparency doesn't apply in fullscreen) guard let window, !window.styleMask.contains(.fullScreen) else { return } // Toggle between transparent and opaque isBackgroundOpaque.toggle() - + // Update our appearance syncAppearance() } - + /// Override this to resync any appearance related properties. This will be called automatically /// when certain window properties change that affect appearance. The list below should be updated /// as we add new things: @@ -1052,7 +1059,7 @@ class BaseTerminalController: NSWindowController, 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. @@ -1061,7 +1068,7 @@ class BaseTerminalController: NSWindowController, } else { updateOverlayIsVisible = defaultUpdateOverlayVisibility() } - + // Always resync our appearance syncAppearance() } @@ -1109,7 +1116,7 @@ class BaseTerminalController: NSWindowController, window?.endSheet(ccWindow) } - switch (request) { + switch request { case let .osc_52_write(pasteboard): guard case .confirm = action else { break } let pb = pasteboard ?? NSPasteboard.general @@ -1117,7 +1124,7 @@ class BaseTerminalController: NSWindowController, pb.setString(cc.contents, forType: .string) case .osc_52_read, .paste: let str: String - switch (action) { + switch action { case .cancel: str = "" @@ -1146,26 +1153,26 @@ 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 } @@ -1202,6 +1209,17 @@ class BaseTerminalController: NSWindowController, func windowWillClose(_ notification: Notification) { guard let window else { return } + // Emit a final bell-state transition so any observers can clear state + // without separately tracking NSWindow lifecycle events. + if bell { + bell = false + NotificationCenter.default.post( + name: .terminalWindowBellDidChangeNotification, + object: self, + userInfo: [Notification.Name.terminalWindowHasBellKey: false] + ) + } + // I don't know if this is required anymore. We previously had a ref cycle between // the view and the window so we had to nil this out to break it but I think this // may now be resolved. We should verify that no memory leaks and we can remove this. @@ -1221,9 +1239,11 @@ class BaseTerminalController: NSWindowController, } } - // Becoming/losing key means we have to notify our surface(s) that we have focus - // so things like cursors blink, pty events are sent, etc. - self.syncFocusToSurfaceTree() + // Becoming key can race with responder updates when activating a window. + // Sync on the next runloop so split focus has settled first. + DispatchQueue.main.async { + self.syncFocusToSurfaceTree() + } } func windowDidResignKey(_ notification: Notification) { @@ -1267,6 +1287,17 @@ class BaseTerminalController: NSWindowController, } @IBAction func changeTabTitle(_ sender: Any) { + if let targetWindow = window { + let inlineHostWindow = + targetWindow.tabbedWindows? + .first(where: { $0.tabBarView != nil }) as? TerminalWindow + ?? (targetWindow as? TerminalWindow) + + if let inlineHostWindow, inlineHostWindow.beginInlineTabTitleEdit(for: targetWindow) { + return + } + } + promptTabTitle() } @@ -1295,7 +1326,6 @@ class BaseTerminalController: NSWindowController, ghostty.splitToggleZoom(surface: surface) } - @IBAction func splitMoveFocusPrevious(_ sender: Any) { splitMoveFocus(direction: .previous) } @@ -1368,7 +1398,7 @@ class BaseTerminalController: NSWindowController, @IBAction func toggleCommandPalette(_ sender: Any?) { commandPaletteIsShowing.toggle() } - + @IBAction func find(_ sender: Any) { focusedSurface?.find(sender) } @@ -1384,11 +1414,11 @@ class BaseTerminalController: NSWindowController, @IBAction func findNext(_ sender: Any) { focusedSurface?.findNext(sender) } - + @IBAction func findPrevious(_ sender: Any) { focusedSurface?.findNext(sender) } - + @IBAction func findHide(_ sender: Any) { focusedSurface?.findHide(sender) } @@ -1430,7 +1460,7 @@ extension BaseTerminalController: NSMenuItemValidation { return true } } - + // MARK: - Surface Color Scheme /// Update the surface tree's color scheme only when it actually changes. @@ -1462,3 +1492,57 @@ extension BaseTerminalController: NSMenuItemValidation { appliedColorScheme = scheme } } + +// MARK: Combine Methods + +extension BaseTerminalController { + /// Publishes an app-wide notification whenever this terminal window's aggregate + /// bell state changes. + private func setupBellNotificationPublisher() { + bellStateCancellable = surfaceValuesPublisher(valueKeyPath: \.bell, publisherKeyPath: \.$bell) + .map { $0.values.contains(true) } + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] hasBell in + guard let self else { return } + bell = hasBell + NotificationCenter.default.post( + name: .terminalWindowBellDidChangeNotification, + object: self, + userInfo: [Notification.Name.terminalWindowHasBellKey: hasBell] + ) + } + } + + /// Creates a publisher for values on all surfaces in this controller's tree. + /// + /// The publisher emits a dictionary of surface IDs to values whenever the tree changes + /// or any surface publishes a new value for the key path. + func surfaceValuesPublisher( + valueKeyPath: KeyPath, + publisherKeyPath: KeyPath.Publisher> + ) -> AnyPublisher<[Ghostty.SurfaceView.ID: Value], Never> { + // `surfaceTree` can be replaced entirely when splits are added/removed/closed. + // For each tree snapshot we build a fresh publisher that watches all surfaces + // in that snapshot. + $surfaceTree + .map { tree in + tree.valuesPublisher( + valueKeyPath: valueKeyPath, + publisherKeyPath: publisherKeyPath + ) + } + // Keep only the latest tree publisher active. This automatically cancels + // subscriptions for old/removed surfaces when the tree changes. + .switchToLatest() + .eraseToAnyPublisher() + } +} + +// MARK: Notifications + +extension Notification.Name { + /// Terminal window aggregate bell state changed. + static let terminalWindowBellDidChangeNotification = Notification.Name("com.mitchellh.ghostty.terminalWindowBellDidChange") + static let terminalWindowHasBellKey = terminalWindowBellDidChangeNotification.rawValue + ".hasBell" +} diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index c7f9fe08612..56b0b40ad74 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -8,21 +8,21 @@ import GhosttyKit class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Controller { override var windowNibName: NSNib.Name? { let defaultValue = "Terminal" - + guard let appDelegate = NSApp.delegate as? AppDelegate else { return defaultValue } let config = appDelegate.ghostty.config - + // If we have no window decorations, there's no reason to do anything but // the default titlebar (because there will be no titlebar). if !config.windowDecorations { return defaultValue } - + let nib = switch config.macosTitlebarStyle { - case "native": "Terminal" - case "hidden": "TerminalHiddenTitlebar" - case "transparent": "TerminalTransparentTitlebar" - case "tabs": + case .native: "Terminal" + case .hidden: "TerminalHiddenTitlebar" + case .transparent: "TerminalTransparentTitlebar" + case .tabs: #if compiler(>=6.2) if #available(macOS 26.0, *) { "TerminalTabsTitlebarTahoe" @@ -32,35 +32,30 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr #else "TerminalTabsTitlebarVentura" #endif - default: defaultValue } - + return nib } - + /// This is set to true when we care about frame changes. This is a small optimization since /// this controller registers a listener for ALL frame change notifications and this lets us bail /// early if we don't care. private var tabListenForFrame: Bool = false - + /// This is the hash value of the last tabGroup.windows array. We use this to detect order /// changes in the list. private var tabWindowsHash: Int = 0 - + /// This is set to false by init if the window managed by this controller should not be restorable. /// For example, terminals executing custom scripts are not restorable. private var restorable: Bool = true - + /// The configuration derived from the Ghostty config so we don't need to rely on references. private(set) var derivedConfig: DerivedConfig - - + /// The notification cancellable for focused surface property changes. private var surfaceAppearanceCancellables: Set = [] - - /// This will be set to the initial frame of the window from the xib on load. - private var initialFrame: NSRect? = nil - + init(_ ghostty: Ghostty.App, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, withSurfaceTree tree: SplitTree? = nil, @@ -72,12 +67,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // as the script. We may want to revisit this behavior when we have scrollback // restoration. self.restorable = (base?.command ?? "") == "" - + // Setup our initial derived config based on the current app config self.derivedConfig = DerivedConfig(ghostty.config) - + super.init(ghostty, baseConfig: base, surfaceTree: tree) - + // Setup our notifications for behaviors let center = NotificationCenter.default center.addObserver( @@ -134,37 +129,37 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr object: nil ) } - + required init?(coder: NSCoder) { fatalError("init(coder:) is not supported for this view") } - + deinit { // Remove all of our notificationcenter subscriptions let center = NotificationCenter.default center.removeObserver(self) } - + // MARK: Base Controller Overrides - + override func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { super.surfaceTreeDidChange(from: from, to: to) - + // Whenever our surface tree changes in any way (new split, close split, etc.) // we want to invalidate our state. invalidateRestorableState() - + // Update our zoom state if let window = window as? TerminalWindow { window.surfaceIsZoomed = to.zoomed != nil } - + // If our surface tree is now nil then we close our window. - if (to.isEmpty) { + if to.isEmpty { self.window?.close() } } - + override func replaceSurfaceTree( _ newTree: SplitTree, moveFocusTo newView: Ghostty.SurfaceView? = nil, @@ -177,7 +172,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr closeTabImmediately() return } - + super.replaceSurfaceTree( newTree, moveFocusTo: newView, @@ -199,6 +194,18 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // of each other. private static var lastCascadePoint = NSPoint(x: 0, y: 0) + private static func applyCascade(to window: NSWindow, hasFixedPos: Bool) { + if hasFixedPos { return } + + if all.count > 1 { + lastCascadePoint = window.cascadeTopLeft(from: lastCascadePoint) + } else { + // We assume the window frame is already correct at this point, + // so we pass .zero to let cascade use the current frame position. + lastCascadePoint = window.cascadeTopLeft(from: .zero) + } + } + // The preferred parent terminal controller. static var preferredParent: TerminalController? { all.first { @@ -210,7 +217,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // to find the preferred window to attach new tabs, perform actions, etc. We // always prefer the main window but if there isn't any (because we're triggered // by something like an App Intent) then we prefer the most previous main. - static private(set) weak var lastMain: TerminalController? = nil + static private(set) weak var lastMain: TerminalController? /// The "new window" action. static func newWindow( @@ -224,27 +231,25 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // otherwise the focused terminal, otherwise an arbitrary one. let parent: NSWindow? = explicitParent ?? preferredParent?.window - if let parent { - if parent.styleMask.contains(.fullScreen) { - // If our previous window was fullscreen then we want our new window to - // be fullscreen. This behavior actually doesn't match the native tabbing - // behavior of macOS apps where new windows create tabs when in native - // fullscreen but this is how we've always done it. This matches iTerm2 - // behavior. + if let parent, parent.styleMask.contains(.fullScreen) { + // If our previous window was fullscreen then we want our new window to + // be fullscreen. This behavior actually doesn't match the native tabbing + // behavior of macOS apps where new windows create tabs when in native + // fullscreen but this is how we've always done it. This matches iTerm2 + // behavior. + c.toggleFullscreen(mode: .native) + } else if let fullscreenMode = ghostty.config.windowFullscreen { + switch fullscreenMode { + case .native: + // Native has to be done immediately so that our stylemask contains + // fullscreen for the logic later in this method. c.toggleFullscreen(mode: .native) - } else if ghostty.config.windowFullscreen { - switch (ghostty.config.windowFullscreenMode) { - case .native: - // Native has to be done immediately so that our stylemask contains - // fullscreen for the logic later in this method. - c.toggleFullscreen(mode: .native) - - case .nonNative, .nonNativeVisibleMenu, .nonNativePaddedNotch: - // If we're non-native then we have to do it on a later loop - // so that the content view is setup. - DispatchQueue.main.async { - c.toggleFullscreen(mode: ghostty.config.windowFullscreenMode) - } + + case .nonNative, .nonNativeVisibleMenu, .nonNativePaddedNotch: + // If we're non-native then we have to do it on a later loop + // so that the content view is setup. + DispatchQueue.main.async { + c.toggleFullscreen(mode: fullscreenMode) } } } @@ -253,15 +258,16 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // take effect. Our best theory is there is some next-event-loop-tick logic // that Cocoa is doing that we need to be after. DispatchQueue.main.async { + c.showWindow(self) + // Only cascade if we aren't fullscreen. if let window = c.window { - if (!window.styleMask.contains(.fullScreen)) { - Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint) + if !window.styleMask.contains(.fullScreen) { + let hasFixedPos = c.derivedConfig.windowPositionX != nil && c.derivedConfig.windowPositionY != nil + Self.applyCascade(to: window, hasFixedPos: hasFixedPos) } } - c.showWindow(self) - // All new_window actions force our app to be active, so that the new // window is focused and visible. NSApp.activate(ignoringOtherApps: true) @@ -314,6 +320,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr let treeSize: CGSize? = tree.root?.viewBounds() DispatchQueue.main.async { + c.showWindow(self) if let window = c.window { // If we have a tree size, resize the window's content to match if let treeSize, treeSize.width > 0, treeSize.height > 0 { @@ -326,12 +333,11 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr window.setFrameTopLeftPoint(position) window.constrainToScreen() } else { - Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint) + let hasFixedPos = c.derivedConfig.windowPositionX != nil && c.derivedConfig.windowPositionY != nil + Self.applyCascade(to: window, hasFixedPos: hasFixedPos) } } } - - c.showWindow(self) } // Setup our undo @@ -392,7 +398,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // If the parent is miniaturized, then macOS exhibits really strange behaviors // so we have to bring it back out. - if (parent.isMiniaturized) { parent.deminiaturize(self) } + if parent.isMiniaturized { parent.deminiaturize(self) } // If our parent tab group already has this window, macOS added it and // we need to remove it so we can set the correct order in the next line. @@ -407,21 +413,21 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } // If we don't allow tabs then we create a new window instead. - if (window.tabbingMode != .disallowed) { + if window.tabbingMode != .disallowed { // Add the window to the tab group and show it. switch ghostty.config.windowNewTabPosition { case "end": // If we already have a tab group and we want the new tab to open at the end, // then we use the last window in the tab group as the parent. if let last = parent.tabGroup?.windows.last { - last.addTabbedWindow(window, ordered: .above) + last.addTabbedWindowSafely(window, ordered: .above) } else { fallthrough } case "current": fallthrough default: - parent.addTabbedWindow(window, ordered: .above) + parent.addTabbedWindowSafely(window, ordered: .above) } } @@ -432,7 +438,8 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // Only cascade if we aren't fullscreen and are alone in the tab group. if !window.styleMask.contains(.fullScreen) && window.tabGroup?.windows.count ?? 1 == 1 { - Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint) + let hasFixedPos = controller.derivedConfig.windowPositionX != nil && controller.derivedConfig.windowPositionY != nil + Self.applyCascade(to: window, hasFixedPos: hasFixedPos) } controller.showWindow(self) @@ -483,8 +490,8 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr return controller } - - //MARK: - Methods + + // MARK: - Methods @objc private func ghosttyConfigDidChange(_ notification: Notification) { // Get our managed configuration object out @@ -493,7 +500,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr ] as? Ghostty.Config else { return } // If this is an app-level config update then we update some things. - if (notification.object == nil) { + if notification.object == nil { // Update our derived config self.derivedConfig = DerivedConfig(config) @@ -564,7 +571,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr tabWindowsHash = v self.relabelTabs() } - + override func syncAppearance() { // When our focus changes, we update our window appearance based on the // currently focused surface. @@ -588,6 +595,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // Call this last in case it uses any of the properties above. window.syncAppearance(surfaceConfig) + terminalViewContainer?.ghosttyConfigDidChange(ghostty.config, preferredBackgroundColor: window.preferredBackgroundColor) } /// Adjusts the given frame for the configured window position. @@ -866,7 +874,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr controller.showWindow(nil) if let firstWindow = firstController.window, let newWindow = controller.window { - firstWindow.addTabbedWindow(newWindow, ordered: .above) + firstWindow.addTabbedWindowSafely(newWindow, ordered: .above) } } @@ -909,7 +917,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr alert.addButton(withTitle: "Cancel") alert.alertStyle = .warning alert.beginSheetModal(for: confirmWindow, completionHandler: { response in - if (response == .alertFirstButtonReturn) { + if response == .alertFirstButtonReturn { // This is important so that we avoid losing focus when Stage // Manager is used (#8336) alert.window.orderOut(nil) @@ -938,9 +946,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr let tabColor: TerminalTabColor } - convenience init(_ ghostty: Ghostty.App, - with undoState: UndoState - ) { + convenience init(_ ghostty: Ghostty.App, with undoState: UndoState) { self.init(ghostty, withSurfaceTree: undoState.surfaceTree) // Show the window and restore its frame @@ -957,15 +963,15 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr if tabIndex < tabGroup.windows.count { // Find the window that is currently at that index let currentWindow = tabGroup.windows[tabIndex] - currentWindow.addTabbedWindow(window, ordered: .below) + currentWindow.addTabbedWindowSafely(window, ordered: .below) } else { - tabGroup.windows.last?.addTabbedWindow(window, ordered: .above) + tabGroup.windows.last?.addTabbedWindowSafely(window, ordered: .above) } // Make it the key window window.makeKeyAndOrderFront(nil) } - + // Restore focus to the previously focused surface if let focusedUUID = undoState.focusedSurface, let focusTarget = surfaceTree.first(where: { $0.id == focusedUUID }) { @@ -996,7 +1002,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr tabColor: (window as? TerminalWindow)?.tabColor ?? .none) } - //MARK: - NSWindowController + // MARK: - NSWindowController override func windowWillLoad() { // We do NOT want to cascade because we handle this manually from the manager. @@ -1015,7 +1021,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // Setting all three of these is required for restoration to work. window.isRestorable = restorable - if (restorable) { + if restorable { window.restorationClass = TerminalWindowRestoration.self window.identifier = .init(String(describing: TerminalWindowRestoration.self)) } @@ -1029,39 +1035,30 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } // Initialize our content view to the SwiftUI root - window.contentView = TerminalViewContainer( - ghostty: self.ghostty, - viewModel: self, - delegate: self, - ) + let container = TerminalViewContainer { + TerminalView(ghostty: ghostty, viewModel: self, delegate: self) + } + + // Set the initial content size on the container so that + // intrinsicContentSize returns the correct value immediately, + // without waiting for @FocusedValue to propagate through the + // SwiftUI focus chain. + container.initialContentSize = focusedSurface?.initialSize + + window.contentView = container // If we have a default size, we want to apply it. if let defaultSize { - switch (defaultSize) { - case .frame: - // Frames can be applied immediately - defaultSize.apply(to: window) + defaultSize.apply(to: window) - case .contentIntrinsicSize: - // Content intrinsic size requires a short delay so that AppKit - // can layout our SwiftUI views. - DispatchQueue.main.asyncAfter(deadline: .now() + .microseconds(10_000)) { [weak self, weak window] in - guard let self, let window else { return } - defaultSize.apply(to: window) - if let screen = window.screen ?? NSScreen.main { - let frame = self.adjustForWindowPosition(frame: window.frame, on: screen) - window.setFrameOrigin(frame.origin) - } + if case .contentIntrinsicSize = defaultSize { + if let screen = window.screen ?? NSScreen.main { + let frame = self.adjustForWindowPosition(frame: window.frame, on: screen) + window.setFrameOrigin(frame.origin) } } } - // Store our initial frame so we can know our default later. This MUST - // be after the defaultSize call above so that we don't re-apply our frame. - // Note: we probably want to set this on the first frame change or something - // so it respects cascade. - initialFrame = window.frame - // In various situations, macOS automatically tabs new windows. Ghostty handles // its own tabbing so we DONT want this behavior. This detects this scenario and undoes // it. @@ -1073,7 +1070,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // We don't run this logic in fullscreen because in fullscreen this will end up // removing the window and putting it into its own dedicated fullscreen, which is not // the expected or desired behavior of anyone I've found. - if (!window.styleMask.contains(.fullScreen)) { + if !window.styleMask.contains(.fullScreen) { // If we have more than 1 window in our tab group we know we're a new window. // Since Ghostty manages tabbing manually this will never be more than one // at this point in the AppKit lifecycle (we add to the group after this). @@ -1088,6 +1085,34 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr syncAppearance(.init(config)) } + /// Setup correct window frame before showing the window + override func showWindow(_ sender: Any?) { + guard let terminalWindow = window as? TerminalWindow else { return } + + // Set the initial window position. This must happen after the window + // is fully set up (content view, toolbar, default size) so that + // decorations added by subclass awakeFromNib (e.g. toolbar for tabs + // style) don't change the frame after the position is restored. + let originChanged = terminalWindow.setInitialWindowPosition( + x: derivedConfig.windowPositionX, + y: derivedConfig.windowPositionY, + ) + let restored = LastWindowPosition.shared.restore( + terminalWindow, + origin: !originChanged, + size: defaultSize == nil, + ) + + // If nothing is changed for the frame, + // we should center the window + if !originChanged, !restored { + // This doesn't work in `windowDidLoad` somehow + terminalWindow.center() + } + + super.showWindow(sender) + } + // Shows the "+" button in the tab bar, responds to that click. override func newWindowForTab(_ sender: Any?) { // Trigger the ghostty core event logic for a new tab. @@ -1103,7 +1128,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr override func windowShouldClose(_ sender: NSWindow) -> Bool { tabGroupCloseCoordinator.windowShouldClose(sender) { [weak self] scope in guard let self else { return } - switch (scope) { + switch scope { case .tab: closeTab(nil) case .window: guard self.window?.isFirstWindowInTabGroup ?? false else { return } @@ -1133,7 +1158,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // https://github.com/ghostty-org/ghostty/issues/2565 let oldFrame = focusedWindow.frame - Self.lastCascadePoint = focusedWindow.cascadeTopLeft(from: NSZeroPoint) + Self.lastCascadePoint = focusedWindow.cascadeTopLeft(from: .zero) if focusedWindow.frame != oldFrame { focusedWindow.setFrame(oldFrame, display: true) @@ -1153,6 +1178,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr super.windowDidBecomeKey(notification) self.relabelTabs() self.fixTabBar() + terminalViewContainer?.updateGlassTintOverlay(isKeyWindow: true) + } + + override func windowDidResignKey(_ notification: Notification) { + super.windowDidResignKey(notification) + terminalViewContainer?.updateGlassTintOverlay(isKeyWindow: false) } override func windowDidMove(_ notification: Notification) { @@ -1160,18 +1191,21 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr self.fixTabBar() // Whenever we move save our last position for the next start. - if let window { - LastWindowPosition.shared.save(window) - } + LastWindowPosition.shared.save(window) + } + + override func windowDidResize(_ notification: Notification) { + super.windowDidResize(notification) + + // Whenever we resize save our last position and size for the next start. + LastWindowPosition.shared.save(window) } func windowDidBecomeMain(_ notification: Notification) { // Whenever we get focused, use that as our last window position for // restart. This differs from Terminal.app but matches iTerm2 behavior // and I think its sensible. - if let window { - LastWindowPosition.shared.save(window) - } + LastWindowPosition.shared.save(window) // Remember our last main Self.lastMain = self @@ -1317,7 +1351,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr ghostty.toggleTerminalInspector(surface: surface) } - //MARK: - TerminalViewDelegate + // MARK: - TerminalViewDelegate override func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) { super.focusedSurfaceDidChange(to: to) @@ -1349,7 +1383,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } } - //MARK: - Notifications + // MARK: - Notifications @objc private func onMoveTab(notification: SwiftUI.Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } @@ -1391,7 +1425,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr if #available(macOS 26, *) { if window is TitlebarTabsTahoeTerminalWindow { tabGroup.removeWindow(selectedWindow) - targetWindow.addTabbedWindow(selectedWindow, ordered: action.amount < 0 ? .below : .above) + targetWindow.addTabbedWindowSafely(selectedWindow, ordered: action.amount < 0 ? .below : .above) DispatchQueue.main.async { selectedWindow.makeKey() } @@ -1406,7 +1440,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // Remove and re-add the window in the correct position tabGroup.removeWindow(selectedWindow) - targetWindow.addTabbedWindow(selectedWindow, ordered: action.amount < 0 ? .below : .above) + targetWindow.addTabbedWindowSafely(selectedWindow, ordered: action.amount < 0 ? .below : .above) // Ensure our window remains selected selectedWindow.makeKey() @@ -1432,23 +1466,23 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr let finalIndex: Int // An index that is invalid is used to signal some special values. - if (tabIndex <= 0) { + if tabIndex <= 0 { guard let selectedWindow = tabGroup.selectedWindow else { return } guard let selectedIndex = tabbedWindows.firstIndex(where: { $0 == selectedWindow }) else { return } - if (tabIndex == GHOSTTY_GOTO_TAB_PREVIOUS.rawValue) { - if (selectedIndex == 0) { + if tabIndex == GHOSTTY_GOTO_TAB_PREVIOUS.rawValue { + if selectedIndex == 0 { finalIndex = tabbedWindows.count - 1 } else { finalIndex = selectedIndex - 1 } - } else if (tabIndex == GHOSTTY_GOTO_TAB_NEXT.rawValue) { - if (selectedIndex == tabbedWindows.count - 1) { + } else if tabIndex == GHOSTTY_GOTO_TAB_NEXT.rawValue { + if selectedIndex == tabbedWindows.count - 1 { finalIndex = 0 } else { finalIndex = selectedIndex + 1 } - } else if (tabIndex == GHOSTTY_GOTO_TAB_LAST.rawValue) { + } else if tabIndex == GHOSTTY_GOTO_TAB_LAST.rawValue { finalIndex = tabbedWindows.count - 1 } else { return @@ -1516,7 +1550,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr struct DerivedConfig { let backgroundColor: Color let macosWindowButtons: Ghostty.MacOSWindowButtons - let macosTitlebarStyle: String + let macosTitlebarStyle: Ghostty.Config.MacOSTitlebarStyle let maximize: Bool let windowPositionX: Int16? let windowPositionY: Int16? @@ -1524,7 +1558,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr init() { self.backgroundColor = Color(NSColor.windowBackgroundColor) self.macosWindowButtons = .visible - self.macosTitlebarStyle = "system" + self.macosTitlebarStyle = .default self.maximize = false self.windowPositionX = nil self.windowPositionY = nil @@ -1549,25 +1583,25 @@ extension TerminalController { case #selector(closeTabsOnTheRight): guard let window, let tabGroup = window.tabGroup else { return false } guard let currentIndex = tabGroup.windows.firstIndex(of: window) else { return false } - return tabGroup.windows.enumerated().contains { $0.offset > currentIndex } - + return tabGroup.windows.indices.contains { $0 > currentIndex } + case #selector(returnToDefaultSize): guard let window else { return false } - + // Native fullscreen windows can't revert to default size. if window.styleMask.contains(.fullScreen) { return false } - + // If we're fullscreen at all then we can't change size if fullscreenStyle?.isFullscreen ?? false { return false } - + // If our window is already the default size or we don't have a // default size, then disable. return defaultSize?.isChanged(for: window) ?? false - + default: return super.validateMenuItem(item) } @@ -1623,9 +1657,6 @@ extension TerminalController { // Initial size as requested by the configuration (e.g. `window-width`) // takes next priority. return .contentIntrinsicSize - } else if let initialFrame { - // The initial frame we had when we started otherwise. - return .frame(initialFrame) } else { return nil } diff --git a/macos/Sources/Features/Terminal/TerminalRestorable.swift b/macos/Sources/Features/Terminal/TerminalRestorable.swift index fd0f4eab58e..aab51f6bdb2 100644 --- a/macos/Sources/Features/Terminal/TerminalRestorable.swift +++ b/macos/Sources/Features/Terminal/TerminalRestorable.swift @@ -98,7 +98,7 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { // no matter what. Note its safe to use "ghostty.config" directly here // because window restoration is only ever invoked on app start so we // don't have to deal with config reloads. - if (appDelegate.ghostty.config.windowSaveState == "never") { + if appDelegate.ghostty.config.windowSaveState == "never" { completionHandler(nil, nil) return } @@ -131,13 +131,11 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { // Find the focused surface in surfaceTree if let focusedStr = state.focusedSurface { var foundView: Ghostty.SurfaceView? - for view in c.surfaceTree { - if view.id.uuidString == focusedStr { - foundView = view - break - } + for view in c.surfaceTree where view.id.uuidString == focusedStr { + foundView = view + break } - + if let view = foundView { c.focusedSurface = view restoreFocus(to: view, inWindow: window) @@ -161,9 +159,9 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { // For the first attempt, we schedule it immediately. Subsequent events wait a bit // so we don't just spin the CPU at 100%. Give up after some period of time. let after: DispatchTime - if (attempts == 0) { + if attempts == 0 { after = .now() - } else if (attempts > 40) { + } else if attempts > 40 { // 2 seconds, give up return } else { @@ -185,11 +183,10 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { // If the window is main, then we also make sure it comes forward. This // prevents a bug found in #1177 where sometimes on restore the windows // would be behind other applications. - if (viewWindow.isMainWindow) { + if viewWindow.isMainWindow { viewWindow.orderFront(nil) } } } } - diff --git a/macos/Sources/Features/Terminal/TerminalTabColor.swift b/macos/Sources/Features/Terminal/TerminalTabColor.swift index 08d89324c16..2879822b32f 100644 --- a/macos/Sources/Features/Terminal/TerminalTabColor.swift +++ b/macos/Sources/Features/Terminal/TerminalTabColor.swift @@ -122,7 +122,7 @@ struct TabColorMenuView: View { VStack(alignment: .leading, spacing: 3) { Text("Tab Color") .padding(.bottom, 2) - + ForEach(Self.paletteRows, id: \.self) { row in HStack(spacing: 2) { ForEach(row, id: \.self) { color in @@ -142,7 +142,7 @@ struct TabColorMenuView: View { .padding(.top, 4) .padding(.bottom, 4) } - + static let paletteRows: [[TerminalTabColor]] = [ [.none, .blue, .purple, .pink, .red], [.orange, .yellow, .green, .teal, .graphite], diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index e117e0647e8..b6e1c637c49 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -17,7 +17,7 @@ protocol TerminalViewDelegate: AnyObject { /// Perform an action. At the time of writing this is only triggered by the command palette. func performAction(_ action: String, on: Ghostty.SurfaceView) - + /// A split tree operation func performSplitAction(_ action: TerminalSplitOperation) } @@ -32,7 +32,7 @@ protocol TerminalViewModel: ObservableObject { /// The command palette state. var commandPaletteIsShowing: Bool { get set } - + /// The update overlay should be visible. var updateOverlayIsVisible: Bool { get } } @@ -45,11 +45,10 @@ struct TerminalView: View { @ObservedObject var viewModel: ViewModel // An optional delegate to receive information about terminal changes. - weak var delegate: (any TerminalViewDelegate)? = nil - - // The most recently focused surface, equal to focusedSurface when - // it is non-nil. - @State private var lastFocusedSurface: Weak = .init() + weak var delegate: (any TerminalViewDelegate)? + + /// The most recently focused surface, equal to `focusedSurface` when it is non-nil. + @State private var lastFocusedSurface: Weak? // This seems like a crutch after switching from SwiftUI to AppKit lifecycle. @FocusState private var focused: Bool @@ -76,7 +75,7 @@ struct TerminalView: View { VStack(spacing: 0) { // If we're running in debug mode we show a warning so that users // know that performance will be degraded. - if (Ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG || Ghostty.info.mode == GHOSTTY_BUILD_MODE_RELEASE_SAFE) { + if Ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG || Ghostty.info.mode == GHOSTTY_BUILD_MODE_RELEASE_SAFE { DebugBuildWarningView() } @@ -84,6 +83,7 @@ struct TerminalView: View { tree: viewModel.surfaceTree, action: { delegate?.performSplitAction($0) }) .environmentObject(ghostty) + .ghosttyLastFocusedSurface(lastFocusedSurface) .focused($focused) .onAppear { self.focused = true } .onChange(of: focusedSurface) { newValue in @@ -101,13 +101,13 @@ struct TerminalView: View { guard let size = newValue else { return } self.delegate?.cellSizeDidChange(to: size) } - .frame(idealWidth: lastFocusedSurface.value?.initialSize?.width, - idealHeight: lastFocusedSurface.value?.initialSize?.height) + .frame(idealWidth: lastFocusedSurface?.value?.initialSize?.width, + idealHeight: lastFocusedSurface?.value?.initialSize?.height) } // Ignore safe area to extend up in to the titlebar region if we have the "hidden" titlebar style - .ignoresSafeArea(.container, edges: ghostty.config.macosTitlebarStyle == "hidden" ? .top : []) + .ignoresSafeArea(.container, edges: ghostty.config.macosTitlebarStyle == .hidden ? .top : []) - if let surfaceView = lastFocusedSurface.value { + if let surfaceView = lastFocusedSurface?.value { TerminalCommandPaletteView( surfaceView: surfaceView, isPresented: $viewModel.commandPaletteIsShowing, @@ -116,7 +116,7 @@ struct TerminalView: View { self.delegate?.performAction(action, on: surfaceView) } } - + // Show update information above all else. if viewModel.updateOverlayIsVisible { UpdateOverlay() @@ -127,12 +127,12 @@ struct TerminalView: View { } } -fileprivate struct UpdateOverlay: View { +private struct UpdateOverlay: View { var body: some View { if let appDelegate = NSApp.delegate as? AppDelegate { VStack { Spacer() - + HStack { Spacer() UpdatePill(model: appDelegate.updateViewModel) diff --git a/macos/Sources/Features/Terminal/TerminalViewContainer.swift b/macos/Sources/Features/Terminal/TerminalViewContainer.swift index c65dca1d225..dd0190c4c61 100644 --- a/macos/Sources/Features/Terminal/TerminalViewContainer.swift +++ b/macos/Sources/Features/Terminal/TerminalViewContainer.swift @@ -3,21 +3,27 @@ import SwiftUI /// Use this container to achieve a glass effect at the window level. /// Modifying `NSThemeFrame` can sometimes be unpredictable. -class TerminalViewContainer: NSView { +class TerminalViewContainer: NSView { private let terminalView: NSView - /// Glass effect view for liquid glass background when transparency is enabled - private var glassEffectView: NSView? - private var glassTopConstraint: NSLayoutConstraint? - private var derivedConfig: DerivedConfig - - init(ghostty: Ghostty.App, viewModel: ViewModel, delegate: (any TerminalViewDelegate)? = nil) { - self.derivedConfig = DerivedConfig(config: ghostty.config) - self.terminalView = NSHostingView(rootView: TerminalView( - ghostty: ghostty, - viewModel: viewModel, - delegate: delegate - )) + /// Combined glass effect and inactive tint overlay view + private(set) var glassEffectView: NSView? + private var derivedConfig: DerivedConfig? + + var windowThemeFrameView: NSView? { + window?.contentView?.superview + } + + var windowCornerRadius: CGFloat? { + guard let window, window.responds(to: Selector(("_cornerRadius"))) else { + return nil + } + + return window.value(forKey: "_cornerRadius") as? CGFloat + } + + init(@ViewBuilder rootView: () -> Root) { + self.terminalView = NSHostingView(rootView: rootView()) super.init(frame: .zero) setup() } @@ -27,11 +33,23 @@ class TerminalViewContainer: NSView { fatalError("init(coder:) has not been implemented") } - /// To make ``TerminalController/DefaultSize/contentIntrinsicSize`` - /// work in ``TerminalController/windowDidLoad()``, - /// we override this to provide the correct size. + /// The initial content size to use as a fallback before the SwiftUI + /// view hierarchy has completed layout (i.e. before @FocusedValue + /// propagates `lastFocusedSurface`). Once the hosting view reports + /// a valid intrinsic size, this fallback is no longer used. + var initialContentSize: NSSize? + override var intrinsicContentSize: NSSize { - terminalView.intrinsicContentSize + let hostingSize = terminalView.intrinsicContentSize + // The hosting view returns a valid size once SwiftUI has laid out + // with the correct idealWidth/idealHeight. Before that (when + // @FocusedValue hasn't propagated), it returns a tiny default. + // Fall back to initialContentSize in that case. + if let initialContentSize, + hostingSize.width < initialContentSize.width || hostingSize.height < initialContentSize.height { + return initialContentSize + } + return hostingSize } private func setup() { @@ -43,13 +61,6 @@ class TerminalViewContainer: NSView { terminalView.bottomAnchor.constraint(equalTo: bottomAnchor), terminalView.trailingAnchor.constraint(equalTo: trailingAnchor), ]) - - NotificationCenter.default.addObserver( - self, - selector: #selector(ghosttyConfigDidChange(_:)), - name: .ghosttyConfigDidChange, - object: nil - ) } override func viewDidMoveToWindow() { @@ -63,98 +74,200 @@ class TerminalViewContainer: NSView { updateGlassEffectTopInsetIfNeeded() } - @objc private func ghosttyConfigDidChange(_ notification: Notification) { - guard let config = notification.userInfo?[ - Notification.Name.GhosttyConfigChangeKey - ] as? Ghostty.Config else { return } - let newValue = DerivedConfig(config: config) + func ghosttyConfigDidChange(_ config: Ghostty.Config, preferredBackgroundColor: NSColor?) { + let newValue = DerivedConfig(config: config, preferredBackgroundColor: preferredBackgroundColor, cornerRadius: windowCornerRadius) guard newValue != derivedConfig else { return } derivedConfig = newValue DispatchQueue.main.async(execute: updateGlassEffectIfNeeded) } } +// MARK: - BaseTerminalController + terminalViewContainer + +extension BaseTerminalController { + var terminalViewContainer: TerminalViewContainer? { + window?.contentView as? TerminalViewContainer + } +} + // MARK: Glass -private extension TerminalViewContainer { +/// An `NSView` that contains a liquid glass background effect and +/// an inactive-window tint overlay. +#if compiler(>=6.2) +@available(macOS 26.0, *) +private class TerminalGlassView: NSView { + private let glassEffectView: NSGlassEffectView + private var topConstraint: NSLayoutConstraint! + private let tintOverlay: NSView + + init(topOffset: CGFloat) { + self.glassEffectView = NSGlassEffectView() + self.tintOverlay = NSView() + super.init(frame: .zero) + + translatesAutoresizingMaskIntoConstraints = false + + // Glass effect view fills this view. + glassEffectView.translatesAutoresizingMaskIntoConstraints = false + addSubview(glassEffectView) + topConstraint = glassEffectView.topAnchor.constraint( + equalTo: topAnchor, + constant: topOffset + ) + NSLayoutConstraint.activate([ + topConstraint, + glassEffectView.leadingAnchor.constraint(equalTo: leadingAnchor), + glassEffectView.bottomAnchor.constraint(equalTo: bottomAnchor), + glassEffectView.trailingAnchor.constraint(equalTo: trailingAnchor), + ]) + + // Tint overlay sits above the glass effect. + tintOverlay.translatesAutoresizingMaskIntoConstraints = false + tintOverlay.wantsLayer = true + tintOverlay.alphaValue = 0 + addSubview(tintOverlay, positioned: .above, relativeTo: glassEffectView) + + NSLayoutConstraint.activate([ + tintOverlay.topAnchor.constraint(equalTo: glassEffectView.topAnchor), + tintOverlay.leadingAnchor.constraint(equalTo: glassEffectView.leadingAnchor), + tintOverlay.bottomAnchor.constraint(equalTo: glassEffectView.bottomAnchor), + tintOverlay.trailingAnchor.constraint(equalTo: glassEffectView.trailingAnchor), + ]) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Configures the glass effect style, tint color, corner radius, and + /// updates the inactive tint overlay based on window key status. + func configure( + style: NSGlassEffectView.Style, + backgroundColor: NSColor, + backgroundOpacity: Double, + cornerRadius: CGFloat?, + isKeyWindow: Bool + ) { + glassEffectView.style = style + glassEffectView.tintColor = backgroundColor.withAlphaComponent(backgroundOpacity) + glassEffectView.cornerRadius = cornerRadius ?? 0 + updateKeyStatus(isKeyWindow, backgroundColor: backgroundColor) + } + + /// Updates the top inset offset for both the glass effect and tint overlay. + /// Call this when the safe area insets change (e.g., during layout). + func updateTopInset(_ offset: CGFloat) { + topConstraint.constant = offset + } + + /// Updates the tint overlay visibility based on window key status. + func updateKeyStatus(_ isKeyWindow: Bool, backgroundColor: NSColor) { + let tint = tintProperties(for: backgroundColor) + tintOverlay.layer?.backgroundColor = tint.color.cgColor + tintOverlay.alphaValue = isKeyWindow ? 0 : tint.opacity + } + + /// Computes a saturation-boosted tint color and opacity for the inactive overlay. + private func tintProperties(for color: NSColor) -> (color: NSColor, opacity: CGFloat) { + let isLight = color.isLightColor + let vibrant = color.adjustingSaturation(by: 1.2) + let overlayOpacity: CGFloat = isLight ? 0.35 : 0.85 + return (vibrant, overlayOpacity) + } +} +#endif // compiler(>=6.2) + +extension TerminalViewContainer { #if compiler(>=6.2) @available(macOS 26.0, *) - func addGlassEffectViewIfNeeded() -> NSGlassEffectView? { - if let existed = glassEffectView as? NSGlassEffectView { + private func addGlassEffectViewIfNeeded() -> TerminalGlassView? { + if let existed = glassEffectView as? TerminalGlassView { updateGlassEffectTopInsetIfNeeded() return existed } - guard let themeFrameView = window?.contentView?.superview else { + guard let themeFrameView = windowThemeFrameView else { return nil } - let effectView = NSGlassEffectView() + let effectView = TerminalGlassView(topOffset: -themeFrameView.safeAreaInsets.top) addSubview(effectView, positioned: .below, relativeTo: terminalView) - effectView.translatesAutoresizingMaskIntoConstraints = false - glassTopConstraint = effectView.topAnchor.constraint( - equalTo: topAnchor, - constant: -themeFrameView.safeAreaInsets.top - ) - if let glassTopConstraint { - NSLayoutConstraint.activate([ - glassTopConstraint, - effectView.leadingAnchor.constraint(equalTo: leadingAnchor), - effectView.bottomAnchor.constraint(equalTo: bottomAnchor), - effectView.trailingAnchor.constraint(equalTo: trailingAnchor), - ]) - } + NSLayoutConstraint.activate([ + effectView.topAnchor.constraint(equalTo: topAnchor), + effectView.leadingAnchor.constraint(equalTo: leadingAnchor), + effectView.bottomAnchor.constraint(equalTo: bottomAnchor), + effectView.trailingAnchor.constraint(equalTo: trailingAnchor), + ]) glassEffectView = effectView return effectView } #endif // compiler(>=6.2) - func updateGlassEffectIfNeeded() { + private func updateGlassEffectIfNeeded() { #if compiler(>=6.2) - guard #available(macOS 26.0, *), derivedConfig.backgroundBlur.isGlassStyle else { + guard #available(macOS 26.0, *), let derivedConfig else { glassEffectView?.removeFromSuperview() glassEffectView = nil - glassTopConstraint = nil return } guard let effectView = addGlassEffectViewIfNeeded() else { return } - switch derivedConfig.backgroundBlur { - case .macosGlassRegular: - effectView.style = NSGlassEffectView.Style.regular - case .macosGlassClear: - effectView.style = NSGlassEffectView.Style.clear - default: - break - } - let backgroundColor = (window as? TerminalWindow)?.preferredBackgroundColor ?? NSColor(derivedConfig.backgroundColor) - effectView.tintColor = backgroundColor - .withAlphaComponent(derivedConfig.backgroundOpacity) - if let window, window.responds(to: Selector(("_cornerRadius"))), let cornerRadius = window.value(forKey: "_cornerRadius") as? CGFloat { - effectView.cornerRadius = cornerRadius + + effectView.configure( + style: derivedConfig.style.official, + backgroundColor: derivedConfig.backgroundColor, + backgroundOpacity: derivedConfig.backgroundOpacity, + cornerRadius: derivedConfig.cornerRadius, + isKeyWindow: window?.isKeyWindow ?? true + ) +#endif // compiler(>=6.2) + } + + private func updateGlassEffectTopInsetIfNeeded() { +#if compiler(>=6.2) + guard + #available(macOS 26.0, *), + let effectView = glassEffectView as? TerminalGlassView, + let themeFrameView = windowThemeFrameView + else { + return } + effectView.updateTopInset(-themeFrameView.safeAreaInsets.top) #endif // compiler(>=6.2) } - func updateGlassEffectTopInsetIfNeeded() { + func updateGlassTintOverlay(isKeyWindow: Bool) { #if compiler(>=6.2) - guard #available(macOS 26.0, *), derivedConfig.backgroundBlur.isGlassStyle else { + guard + #available(macOS 26.0, *), + let effectView = glassEffectView as? TerminalGlassView, + let derivedConfig + else { return } - guard glassEffectView != nil else { return } - guard let themeFrameView = window?.contentView?.superview else { return } - glassTopConstraint?.constant = -themeFrameView.safeAreaInsets.top + effectView.updateKeyStatus(isKeyWindow, backgroundColor: derivedConfig.backgroundColor) #endif // compiler(>=6.2) } struct DerivedConfig: Equatable { - var backgroundOpacity: Double = 0 - var backgroundBlur: Ghostty.Config.BackgroundBlur - var backgroundColor: Color = .clear + let style: BackportNSGlassStyle + let backgroundColor: NSColor + let backgroundOpacity: Double + let cornerRadius: CGFloat? - init(config: Ghostty.Config) { - self.backgroundBlur = config.backgroundBlur + init?(config: Ghostty.Config, preferredBackgroundColor: NSColor?, cornerRadius: CGFloat?) { + switch config.backgroundBlur { + case .macosGlassRegular: + style = .regular + case .macosGlassClear: + style = .clear + default: + return nil + } + self.backgroundColor = preferredBackgroundColor ?? NSColor(config.backgroundColor) self.backgroundOpacity = config.backgroundOpacity - self.backgroundColor = config.backgroundColor + self.cornerRadius = cornerRadius } } } diff --git a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift index dd8b258f3e6..766ec5857ae 100644 --- a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift @@ -3,7 +3,7 @@ import AppKit class HiddenTitlebarTerminalWindow: TerminalWindow { // No titlebar, we don't support accessories. override var supportsUpdateAccessory: Bool { false } - + override func awakeFromNib() { super.awakeFromNib() @@ -34,7 +34,7 @@ class HiddenTitlebarTerminalWindow: TerminalWindow { .closable, .miniaturizable, ] - + /// Apply the hidden titlebar style. private func reapplyHiddenStyle() { // If our window is fullscreen then we don't reapply the hidden style because @@ -43,7 +43,7 @@ class HiddenTitlebarTerminalWindow: TerminalWindow { if terminalController?.fullscreenStyle?.isFullscreen ?? false { return } - + // Apply our style mask while preserving the .fullScreen option if styleMask.contains(.fullScreen) { styleMask = Self.hiddenStyleMask.union([.fullScreen]) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 501ac0e67a5..e19d6711f17 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -33,9 +33,15 @@ class TerminalWindow: NSWindow { /// The configuration derived from the Ghostty config so we don't need to rely on references. private(set) var derivedConfig: DerivedConfig = .init() - + /// Sets up our tab context menu - private var tabMenuObserver: NSObjectProtocol? = nil + private var tabMenuObserver: NSObjectProtocol? + + /// Handles inline tab title editing for this host window. + private(set) lazy var tabTitleEditor = TabTitleEditor( + hostWindow: self, + delegate: self + ) /// Whether this window supports the update accessory. If this is false, then views within this /// window should determine how to show update notifications. @@ -112,13 +118,12 @@ class TerminalWindow: NSWindow { } // If window decorations are disabled, remove our title - if (!config.windowDecorations) { styleMask.remove(.titled) } + if !config.windowDecorations { styleMask.remove(.titled) } - // Set our window positioning to coordinates if config value exists, otherwise - // fallback to original centering behavior - setInitialWindowPosition( - x: config.windowPositionX, - y: config.windowPositionY) + // NOTE: setInitialWindowPosition is NOT called here because subclass + // awakeFromNib may add decorations (e.g. toolbar for tabs style) that + // change the frame. It is called from TerminalController.windowDidLoad + // after the window is fully set up. // If our traffic buttons should be hidden, then hide them if config.macosWindowButtons == .hidden { @@ -166,7 +171,7 @@ class TerminalWindow: NSWindow { tab.accessoryView = stackView // Get our saved level - level = UserDefaults.standard.value(forKey: Self.defaultLevelKey) as? NSWindow.Level ?? .normal + level = UserDefaults.ghostty.value(forKey: Self.defaultLevelKey) as? NSWindow.Level ?? .normal } // Both of these must be true for windows without decorations to be able to @@ -174,7 +179,16 @@ class TerminalWindow: NSWindow { override var canBecomeKey: Bool { return true } override var canBecomeMain: Bool { return true } + override func sendEvent(_ event: NSEvent) { + if tabTitleEditor.handleMouseDown(event) { + return + } + + super.sendEvent(event) + } + override func close() { + tabTitleEditor.finishEditing(commit: true) NotificationCenter.default.post(name: Self.terminalWillCloseNotification, object: self) super.close() } @@ -187,6 +201,7 @@ class TerminalWindow: NSWindow { override func resignKey() { super.resignKey() resetZoomTabButton.contentTintColor = .secondaryLabelColor + tabTitleEditor.finishEditing(commit: true) } override func becomeMain() { @@ -207,6 +222,21 @@ class TerminalWindow: NSWindow { viewModel.isMainWindow = false } + @discardableResult + func beginInlineTabTitleEdit(for targetWindow: NSWindow) -> Bool { + tabTitleEditor.beginEditing(for: targetWindow) + } + + @objc private func renameTabFromContextMenu(_ sender: NSMenuItem) { + let targetWindow = sender.representedObject as? NSWindow ?? self + if beginInlineTabTitleEdit(for: targetWindow) { + return + } + + guard let targetController = targetWindow.windowController as? BaseTerminalController else { return } + targetController.promptTabTitle() + } + override func mergeAllWindows(_ sender: Any?) { super.mergeAllWindows(sender) @@ -295,7 +325,7 @@ class TerminalWindow: NSWindow { // MARK: Tab Key Equivalents - var keyEquivalent: String? = nil { + var keyEquivalent: String? { didSet { // When our key equivalent is set, we must update the tab label. guard let keyEquivalent else { @@ -347,7 +377,7 @@ class TerminalWindow: NSWindow { button.toolTip = "Reset Zoom" button.contentTintColor = isMainWindow ? .controlAccentColor : .secondaryLabelColor button.state = .on - button.image = NSImage(named:"ResetZoom") + button.image = NSImage(named: "ResetZoom") button.frame = NSRect(x: 0, y: 0, width: 20, height: 20) button.translatesAutoresizingMaskIntoConstraints = false button.widthAnchor.constraint(equalToConstant: 20).isActive = true @@ -449,8 +479,7 @@ class TerminalWindow: NSWindow { let forceOpaque = terminalController?.isBackgroundOpaque ?? false if !styleMask.contains(.fullScreen) && !forceOpaque && - (surfaceConfig.backgroundOpacity < 1 || surfaceConfig.backgroundBlur.isGlassStyle) - { + (surfaceConfig.backgroundOpacity < 1 || surfaceConfig.backgroundBlur.isGlassStyle) { isOpaque = false // This is weird, but we don't use ".clear" because this creates a look that @@ -459,7 +488,7 @@ class TerminalWindow: NSWindow { backgroundColor = .white.withAlphaComponent(0.001) // We don't need to set blur when using glass - if !surfaceConfig.backgroundBlur.isGlassStyle, let appDelegate = NSApp.delegate as? AppDelegate { + if !surfaceConfig.backgroundBlur.isGlassStyle, let appDelegate = NSApp.delegate as? AppDelegate { ghostty_set_window_background_blur( appDelegate.ghostty.app, Unmanaged.passUnretained(self).toOpaque()) @@ -507,30 +536,31 @@ class TerminalWindow: NSWindow { terminalController?.updateColorSchemeForSurfaceTree() } - private func setInitialWindowPosition(x: Int16?, y: Int16?) { + func setInitialWindowPosition(x: Int16?, y: Int16?) -> Bool { // If we don't have an X/Y then we try to use the previously saved window pos. - guard x != nil, y != nil else { - if (!LastWindowPosition.shared.restore(self)) { - center() - } - - return + guard let x = x, let y = y else { + return false } // Prefer the screen our window is being placed on otherwise our primary screen. guard let screen = screen ?? NSScreen.screens.first else { - center() - return + return false } - // We have an X/Y, use our controller function to set it up. - guard let terminalController else { - center() - return - } + // Convert top-left coordinates to bottom-left origin using our utility extension + let origin = screen.origin( + fromTopLeftOffsetX: CGFloat(x), + offsetY: CGFloat(y), + windowSize: frame.size) - let frame = terminalController.adjustForWindowPosition(frame: frame, on: screen) - setFrameOrigin(frame.origin) + // Clamp the origin to ensure the window stays fully visible on screen + var safeOrigin = origin + let vf = screen.visibleFrame + safeOrigin.x = min(max(safeOrigin.x, vf.minX), vf.maxX - frame.width) + safeOrigin.y = min(max(safeOrigin.y, vf.minY), vf.maxY - frame.height) + + setFrameOrigin(safeOrigin) + return true } private func hideWindowButtons() { @@ -544,7 +574,7 @@ class TerminalWindow: NSWindow { NotificationCenter.default.removeObserver(observer) } } - + // MARK: Config struct DerivedConfig { @@ -553,7 +583,7 @@ class TerminalWindow: NSWindow { let backgroundColor: NSColor let backgroundOpacity: Double let macosWindowButtons: Ghostty.MacOSWindowButtons - let macosTitlebarStyle: String + let macosTitlebarStyle: Ghostty.Config.MacOSTitlebarStyle let windowCornerRadius: CGFloat init() { @@ -562,7 +592,7 @@ class TerminalWindow: NSWindow { self.backgroundOpacity = 1 self.macosWindowButtons = .visible self.backgroundBlur = .disabled - self.macosTitlebarStyle = "transparent" + self.macosTitlebarStyle = .default self.windowCornerRadius = 16 } @@ -578,7 +608,7 @@ class TerminalWindow: NSWindow { // Native, transparent, and hidden styles use 16pt radius // Tabs style uses 20pt radius switch config.macosTitlebarStyle { - case "tabs": + case .tabs: self.windowCornerRadius = 20 default: self.windowCornerRadius = 16 @@ -732,10 +762,11 @@ extension TerminalWindow { separator.identifier = Self.tabColorSeparatorIdentifier menu.addItem(separator) - // Change Title... - let changeTitleItem = NSMenuItem(title: "Change Title...", action: #selector(BaseTerminalController.changeTabTitle(_:)), keyEquivalent: "") + // Rename Tab... + let changeTitleItem = NSMenuItem(title: "Rename Tab...", action: #selector(TerminalWindow.renameTabFromContextMenu(_:)), keyEquivalent: "") changeTitleItem.identifier = Self.changeTitleMenuItemIdentifier - changeTitleItem.target = target + changeTitleItem.target = self + changeTitleItem.representedObject = target?.window changeTitleItem.setImageIfDesired(systemSymbolName: "pencil.line") menu.addItem(changeTitleItem) @@ -761,3 +792,51 @@ private func makeTabColorPaletteView( hostingView.frame.size = hostingView.intrinsicContentSize return hostingView } + +// MARK: - Inline Tab Title Editing + +extension TerminalWindow: TabTitleEditorDelegate { + func tabTitleEditor( + _ editor: TabTitleEditor, + canRenameTabFor targetWindow: NSWindow + ) -> Bool { + targetWindow.windowController is BaseTerminalController + } + + func tabTitleEditor( + _ editor: TabTitleEditor, + titleFor targetWindow: NSWindow + ) -> String { + guard let targetController = targetWindow.windowController as? BaseTerminalController else { + return targetWindow.title + } + + return targetController.titleOverride ?? targetWindow.title + } + + func tabTitleEditor( + _ editor: TabTitleEditor, + didCommitTitle editedTitle: String, + for targetWindow: NSWindow + ) { + guard let targetController = targetWindow.windowController as? BaseTerminalController else { return } + targetController.titleOverride = editedTitle.isEmpty ? nil : editedTitle + } + + func tabTitleEditor( + _ editor: TabTitleEditor, + performFallbackRenameFor targetWindow: NSWindow + ) { + guard let targetController = targetWindow.windowController as? BaseTerminalController else { return } + targetController.promptTabTitle() + } + + func tabTitleEditor(_ editor: TabTitleEditor, didFinishEditing targetWindow: NSWindow) { + // After inline editing, the first responder is the window itself. + // Restore focus to the terminal surface so keyboard input works. + guard let controller = windowController as? BaseTerminalController, + let focusedSurface = controller.focusedSurface + else { return } + makeFirstResponder(focusedSurface) + } +} diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index 91819152240..6df1b14bce6 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -8,7 +8,7 @@ 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 } @@ -58,13 +58,13 @@ 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 } @@ -84,18 +84,22 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool super.sendEvent(event) return } - + guard let tabBarView else { super.sendEvent(event) return } - + + guard !tabTitleEditor.handleRightMouseDown(event) else { + return + } + let locationInTabBar = tabBarView.convert(event.locationInWindow, from: nil) guard tabBarView.bounds.contains(locationInTabBar) else { super.sendEvent(event) return } - + tabBarView.rightMouseDown(with: event) } @@ -107,7 +111,7 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // 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 } @@ -116,7 +120,7 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // 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. @@ -189,7 +193,7 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool guard let clipView = tabBarView.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return } guard let accessoryView = clipView.subviews[safe: 0] else { return } guard let toolbarView = titlebarView.firstDescendant(withClassName: "NSToolbarView") else { return } - + // Make sure tabBar's height won't be stretched guard let newTabButton = titlebarView.firstDescendant(withClassName: "NSTabBarNewTabButton") else { return } tabBarView.frame.size.height = newTabButton.frame.width @@ -199,7 +203,7 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // The padding for the tab bar. If we're showing window buttons then // we need to offset the window buttons. - let leftPadding: CGFloat = switch(self.derivedConfig.macosWindowButtons) { + let leftPadding: CGFloat = switch self.derivedConfig.macosWindowButtons { case .hidden: 0 case .visible: 70 } @@ -282,7 +286,7 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // This is the documented way to avoid the glass view on an item. // We don't want glass on our title. item.isBordered = false - + return item default: return NSToolbarItem(itemIdentifier: itemIdentifier) @@ -327,7 +331,7 @@ extension TitlebarTabsTahoeTerminalWindow { Color.clear.frame(width: 1, height: 1) } } - + @ViewBuilder var titleText: some View { Text(title) diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index 39db13c6dca..fe83fc5fdb7 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -20,13 +20,11 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // false if all three traffic lights are missing/hidden, otherwise true private var hasWindowButtons: Bool { - get { - // if standardWindowButton(.theButton) == nil, the button isn't there, so coalesce to true - let closeIsHidden = standardWindowButton(.closeButton)?.isHiddenOrHasHiddenAncestor ?? true - let miniaturizeIsHidden = standardWindowButton(.miniaturizeButton)?.isHiddenOrHasHiddenAncestor ?? true - let zoomIsHidden = standardWindowButton(.zoomButton)?.isHiddenOrHasHiddenAncestor ?? true - return !(closeIsHidden && miniaturizeIsHidden && zoomIsHidden) - } + // if standardWindowButton(.theButton) == nil, the button isn't there, so coalesce to true + let closeIsHidden = standardWindowButton(.closeButton)?.isHiddenOrHasHiddenAncestor ?? true + let miniaturizeIsHidden = standardWindowButton(.miniaturizeButton)?.isHiddenOrHasHiddenAncestor ?? true + let zoomIsHidden = standardWindowButton(.zoomButton)?.isHiddenOrHasHiddenAncestor ?? true + return !(closeIsHidden && miniaturizeIsHidden && zoomIsHidden) } // MARK: NSWindow @@ -159,7 +157,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { titlebarColor = derivedConfig.backgroundColor.withAlphaComponent(derivedConfig.backgroundOpacity) } - if (isOpaque || themeChanged) { + if isOpaque || themeChanged { // If there is transparency, calling this will make the titlebar opaque // so we only call this if we are opaque. updateTabBar() @@ -172,7 +170,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { backgroundColor.luminance < 0.05 } - private var newTabButtonImageLayer: VibrantLayer? = nil + private var newTabButtonImageLayer: VibrantLayer? func updateTabBar() { newTabButtonImageLayer = nil @@ -251,7 +249,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { button.toolTip = "Reset Zoom" button.contentTintColor = .controlAccentColor button.state = .on - button.image = NSImage(named:"ResetZoom") + button.image = NSImage(named: "ResetZoom") button.frame = NSRect(x: 0, y: 0, width: 20, height: 20) button.translatesAutoresizingMaskIntoConstraints = false button.widthAnchor.constraint(equalToConstant: 20).isActive = true @@ -286,9 +284,9 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // MARK: - Titlebar Tabs - private var windowButtonsBackdrop: WindowButtonsBackdropView? = nil + private var windowButtonsBackdrop: WindowButtonsBackdropView? - private var windowDragHandle: WindowDragView? = nil + private var windowDragHandle: WindowDragView? // Used by the window controller to enable/disable titlebar tabs. var titlebarTabs = false { @@ -340,7 +338,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { } } - // HACK: hide the "collapsed items" marker from the toolbar if it's present. // idk why it appears in macOS 15.0+ but it does... so... make it go away. (sigh) private func hideToolbarOverflowButton() { @@ -359,7 +356,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) { let isTabBar = self.titlebarTabs && isTabBar(childViewController) - if (isTabBar) { + if isTabBar { // Ensure it has the right layoutAttribute to force it next to our titlebar childViewController.layoutAttribute = .right @@ -374,7 +371,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { super.addTitlebarAccessoryViewController(childViewController) - if (isTabBar) { + if isTabBar { pushTabsToTitlebar(childViewController) } } @@ -382,7 +379,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { override func removeTitlebarAccessoryViewController(at index: Int) { let isTabBar = titlebarAccessoryViewControllers[index].identifier == Self.tabBarIdentifier super.removeTitlebarAccessoryViewController(at: index) - if (isTabBar) { + if isTabBar { resetCustomTabBarViews() } } @@ -403,7 +400,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { private func pushTabsToTitlebar(_ tabBarController: NSTitlebarAccessoryViewController) { // We need a toolbar as a target for our titlebar tabs. - if (toolbar == nil) { + if toolbar == nil { generateToolbar() } @@ -506,10 +503,10 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { } // Passes mouseDown events from this view to window.performDrag so that you can drag the window by it. -fileprivate class WindowDragView: NSView { +private class WindowDragView: NSView { override public func mouseDown(with event: NSEvent) { // Drag the window for single left clicks, double clicks should bypass the drag handle. - if (event.type == .leftMouseDown && event.clickCount == 1) { + if event.type == .leftMouseDown && event.clickCount == 1 { window?.performDrag(with: event) NSCursor.closedHand.set() } else { @@ -535,7 +532,7 @@ fileprivate class WindowDragView: NSView { } // A view that matches the color of selected and unselected tabs in the adjacent tab bar. -fileprivate class WindowButtonsBackdropView: NSView { +private class WindowButtonsBackdropView: NSView { // This must be weak because the window has this view. Otherwise // a retain cycle occurs. private weak var terminalWindow: TitlebarTabsVenturaTerminalWindow? @@ -588,7 +585,7 @@ fileprivate class WindowButtonsBackdropView: NSView { // Custom NSToolbar subclass that displays a centered window title, // in order to accommodate the titlebar tabs feature. -fileprivate class TerminalToolbar: NSToolbar, NSToolbarDelegate { +private class TerminalToolbar: NSToolbar, NSToolbarDelegate { private let titleTextField = CenteredDynamicLabel(labelWithString: "👻 Ghostty") var titleText: String { @@ -674,7 +671,7 @@ fileprivate class TerminalToolbar: NSToolbar, NSToolbarDelegate { } /// A label that expands to fit whatever text you put in it and horizontally centers itself in the current window. -fileprivate class CenteredDynamicLabel: NSTextField { +private class CenteredDynamicLabel: NSTextField { override func viewDidMoveToSuperview() { // Configure the text field isEditable = false diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index a72436d7fda..c0e506c349b 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -92,8 +92,8 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { // For glass background styles, use a transparent titlebar to let the glass effect show through // Only apply this for transparent and tabs titlebar styles let isGlassStyle = derivedConfig.backgroundBlur.isGlassStyle - let isTransparentTitlebar = derivedConfig.macosTitlebarStyle == "transparent" || - derivedConfig.macosTitlebarStyle == "tabs" + let isTransparentTitlebar = derivedConfig.macosTitlebarStyle == .transparent || + derivedConfig.macosTitlebarStyle == .tabs titlebarView.layer?.backgroundColor = (isGlassStyle && isTransparentTitlebar) ? NSColor.clear.cgColor @@ -151,7 +151,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { tabGroupWindowsObservation = tabGroup.observe( \.windows, options: [.new] - ) { [weak self] _, change in + ) { [weak self] _, _ in // NOTE: At one point, I guarded this on only if we went from 0 to N // or N to 0 under the assumption that the tab bar would only get // replaced on those cases. This turned out to be false (Tahoe). @@ -175,7 +175,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { tabBarVisibleObservation = tabGroup?.observe( \.isTabBarVisible, options: [.new] - ) { [weak self] _, change in + ) { [weak self] _, _ in guard let self else { return } guard let lastSurfaceConfig else { return } self.syncAppearance(lastSurfaceConfig) diff --git a/macos/Sources/Features/Update/UpdateBadge.swift b/macos/Sources/Features/Update/UpdateBadge.swift index 054fdf97165..ce98bd277c6 100644 --- a/macos/Sources/Features/Update/UpdateBadge.swift +++ b/macos/Sources/Features/Update/UpdateBadge.swift @@ -9,15 +9,15 @@ import SwiftUI 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 { @@ -28,10 +28,10 @@ struct UpdateBadge: View { } 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) @@ -47,7 +47,7 @@ struct UpdateBadge: View { } else { EmptyView() } - + default: if let iconName = model.iconName { Image(systemName: iconName) @@ -61,18 +61,18 @@ struct UpdateBadge: View { /// 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 { +private 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)) diff --git a/macos/Sources/Features/Update/UpdateController.swift b/macos/Sources/Features/Update/UpdateController.swift index 939eed42058..1ca218c8b3b 100644 --- a/macos/Sources/Features/Update/UpdateController.swift +++ b/macos/Sources/Features/Update/UpdateController.swift @@ -11,16 +11,16 @@ class UpdateController { private(set) var updater: SPUUpdater private let userDriver: UpdateDriver 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 @@ -34,11 +34,11 @@ class UpdateController { delegate: userDriver ) } - + deinit { installCancellable?.cancel() } - + /// Start the updater. /// /// This must be called before the updater can check for updates. If starting fails, @@ -59,35 +59,35 @@ class UpdateController { )) } } - + /// 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. @@ -97,11 +97,11 @@ class UpdateController { 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. @@ -109,7 +109,7 @@ class UpdateController { self?.updater.checkForUpdates() } } - + /// Validate the check for updates menu item. /// /// - Parameter item: The menu item to validate diff --git a/macos/Sources/Features/Update/UpdateDelegate.swift b/macos/Sources/Features/Update/UpdateDelegate.swift index 6195408513e..72d54bd222b 100644 --- a/macos/Sources/Features/Update/UpdateDelegate.swift +++ b/macos/Sources/Features/Update/UpdateDelegate.swift @@ -6,11 +6,11 @@ extension UpdateDriver: 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. - switch (appDelegate.ghostty.config.autoUpdateChannel) { + switch appDelegate.ghostty.config.autoUpdateChannel { case .tip: return "https://tip.files.ghostty.org/appcast.xml" case .stable: return "https://release.files.ghostty.org/appcast.xml" } diff --git a/macos/Sources/Features/Update/UpdateDriver.swift b/macos/Sources/Features/Update/UpdateDriver.swift index 3beb4c9beaf..b5f580f1b11 100644 --- a/macos/Sources/Features/Update/UpdateDriver.swift +++ b/macos/Sources/Features/Update/UpdateDriver.swift @@ -5,23 +5,23 @@ import Sparkle 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` @@ -36,7 +36,7 @@ class UpdateDriver: NSObject, SPUUserDriver { viewModel.state = .idle } } - + func show(_ request: SPUUpdatePermissionRequest, reply: @escaping @Sendable (SUUpdatePermissionResponse) -> Void) { viewModel.state = .permissionRequest(.init(request: request, reply: { [weak viewModel] response in @@ -47,7 +47,7 @@ class UpdateDriver: NSObject, SPUUserDriver { standard.show(request, reply: reply) } } - + func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) { viewModel.state = .checking(.init(cancel: cancellation)) @@ -55,7 +55,7 @@ class UpdateDriver: NSObject, SPUUserDriver { standard.showUserInitiatedUpdateCheck(cancellation: cancellation) } } - + func showUpdateFound(with appcastItem: SUAppcastItem, state: SPUUserUpdateState, reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) { @@ -64,25 +64,25 @@ class UpdateDriver: NSObject, SPUUserDriver { 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( @@ -98,71 +98,71 @@ class UpdateDriver: NSObject, SPUUserDriver { 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) { if !hasUnobtrusiveTarget { standard.showReady(toInstallAndRelaunch: reply) @@ -170,7 +170,7 @@ class UpdateDriver: NSObject, SPUUserDriver { reply(.install) } } - + func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) { viewModel.state = .installing(.init( retryTerminatingApplication: retryTerminatingApplication, @@ -178,30 +178,30 @@ class UpdateDriver: NSObject, SPUUserDriver { viewModel?.state = .idle } )) - + 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 diff --git a/macos/Sources/Features/Update/UpdatePill.swift b/macos/Sources/Features/Update/UpdatePill.swift index 29d1669e13d..b14cde1ac44 100644 --- a/macos/Sources/Features/Update/UpdatePill.swift +++ b/macos/Sources/Features/Update/UpdatePill.swift @@ -4,16 +4,16 @@ import SwiftUI 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 @@ -36,7 +36,7 @@ struct UpdatePill: View { } } } - + /// The pill-shaped button view that displays the update badge and text @ViewBuilder private var pillButton: some View { @@ -47,11 +47,11 @@ struct UpdatePill: View { } else { showPopover.toggle() } - }) { + }, label: { HStack(spacing: 6) { UpdateBadge(model: model) .frame(width: 14, height: 14) - + Text(model.text) .font(Font(textFont)) .lineLimit(1) @@ -66,12 +66,12 @@ struct UpdatePill: View { ) .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] diff --git a/macos/Sources/Features/Update/UpdatePopoverView.swift b/macos/Sources/Features/Update/UpdatePopoverView.swift index 87d76f8015c..aa4e822f313 100644 --- a/macos/Sources/Features/Update/UpdatePopoverView.swift +++ b/macos/Sources/Features/Update/UpdatePopoverView.swift @@ -8,10 +8,10 @@ import Sparkle 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 { @@ -19,31 +19,31 @@ struct UpdatePopoverView: View { // 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 .installing(let installing): // This is only required when `installing.isAutoUpdate == true`, // but we keep it anyway, just in case something unexpected // happens during installing InstallingView(installing: installing, dismiss: dismiss) - + case .notFound(let notFound): NotFoundView(notFound: notFound, dismiss: dismiss) - + case .error(let error): UpdateErrorView(error: error, dismiss: dismiss) } @@ -52,22 +52,22 @@ struct UpdatePopoverView: View { } } -fileprivate struct PermissionRequestView: View { +private 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( @@ -76,9 +76,9 @@ fileprivate struct PermissionRequestView: View { dismiss() } .keyboardShortcut(.cancelAction) - + Spacer() - + Button("Allow") { request.reply(SUUpdatePermissionResponse( automaticUpdateChecks: true, @@ -93,10 +93,10 @@ fileprivate struct PermissionRequestView: View { } } -fileprivate struct CheckingView: View { +private struct CheckingView: View { let checking: UpdateState.Checking let dismiss: DismissAction - + var body: some View { VStack(alignment: .leading, spacing: 16) { HStack(spacing: 10) { @@ -105,7 +105,7 @@ fileprivate struct CheckingView: View { Text("Checking for updates…") .font(.system(size: 13)) } - + HStack { Spacer() Button("Cancel") { @@ -120,19 +120,19 @@ fileprivate struct CheckingView: View { } } -fileprivate struct UpdateAvailableView: View { +private 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:") @@ -141,7 +141,7 @@ fileprivate struct UpdateAvailableView: View { Text(update.appcastItem.displayVersionString) } .font(.system(size: 11)) - + if update.appcastItem.contentLength > 0 { HStack(spacing: 6) { Text("Size:") @@ -151,7 +151,7 @@ fileprivate struct UpdateAvailableView: View { } .font(.system(size: 11)) } - + if let date = update.appcastItem.date { HStack(spacing: 6) { Text("Released:") @@ -164,23 +164,23 @@ fileprivate struct UpdateAvailableView: View { } .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 and Relaunch") { update.reply(.install) dismiss() @@ -191,10 +191,10 @@ fileprivate struct UpdateAvailableView: View { } } .padding(16) - + if let notes = update.releaseNotes { Divider() - + Link(destination: notes.url) { HStack { Image(systemName: "doc.text") @@ -217,16 +217,16 @@ fileprivate struct UpdateAvailableView: View { } } -fileprivate struct DownloadingView: View { +private 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) { @@ -240,7 +240,7 @@ fileprivate struct DownloadingView: View { .controlSize(.small) } } - + HStack { Spacer() Button("Cancel") { @@ -255,14 +255,14 @@ fileprivate struct DownloadingView: View { } } -fileprivate struct ExtractingView: View { +private 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)) @@ -274,22 +274,22 @@ fileprivate struct ExtractingView: View { } } -fileprivate struct InstallingView: View { +private 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 { Button("Restart Later") { installing.dismiss() @@ -297,9 +297,9 @@ fileprivate struct InstallingView: View { } .keyboardShortcut(.cancelAction) .controlSize(.small) - + Spacer() - + Button("Restart Now") { installing.retryTerminatingApplication() dismiss() @@ -313,22 +313,22 @@ fileprivate struct InstallingView: View { } } -fileprivate struct NotFoundView: View { +private 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") { @@ -343,10 +343,10 @@ fileprivate struct NotFoundView: View { } } -fileprivate struct UpdateErrorView: View { +private struct UpdateErrorView: View { let error: UpdateState.Error let dismiss: DismissAction - + var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) { @@ -357,13 +357,13 @@ fileprivate struct UpdateErrorView: View { 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() @@ -371,9 +371,9 @@ fileprivate struct UpdateErrorView: View { } .keyboardShortcut(.cancelAction) .controlSize(.small) - + Spacer() - + Button("Retry") { error.retry() dismiss() diff --git a/macos/Sources/Features/Update/UpdateSimulator.swift b/macos/Sources/Features/Update/UpdateSimulator.swift index bf168d9fc74..c893993e0e1 100644 --- a/macos/Sources/Features/Update/UpdateSimulator.swift +++ b/macos/Sources/Features/Update/UpdateSimulator.swift @@ -9,31 +9,31 @@ import Sparkle 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 - + /// Simulates auto-update flow: goes directly to installing state without showing intermediate UI case autoUpdate - + func simulate(with viewModel: UpdateViewModel) { switch self { case .happyPath: @@ -56,12 +56,12 @@ enum UpdateSimulator { simulateAutoUpdate(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(), @@ -75,28 +75,28 @@ enum UpdateSimulator { )) } } - + 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: [ @@ -111,12 +111,12 @@ enum UpdateSimulator { )) } } - + 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(), @@ -130,7 +130,7 @@ enum UpdateSimulator { )) } } - + private func simulateSlowDownloadProgress(_ viewModel: UpdateViewModel) { let download = UpdateState.Downloading( cancel: { @@ -140,7 +140,7 @@ enum UpdateSimulator { progress: 0 ) viewModel.state = .downloading(download) - + for i in 1...20 { DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.5) { let updatedDownload = UpdateState.Downloading( @@ -149,7 +149,7 @@ enum UpdateSimulator { progress: UInt64(i * 100) ) viewModel.state = .downloading(updatedDownload) - + if i == 20 { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { simulateExtract(viewModel) @@ -158,7 +158,7 @@ enum UpdateSimulator { } } } - + private func simulatePermissionRequest(_ viewModel: UpdateViewModel) { let request = SPUUpdatePermissionRequest(systemProfile: []) viewModel.state = .permissionRequest(.init( @@ -172,12 +172,12 @@ enum UpdateSimulator { } )) } - + 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(), @@ -191,7 +191,7 @@ enum UpdateSimulator { )) } } - + private func simulateDownloadThenCancel(_ viewModel: UpdateViewModel) { let download = UpdateState.Downloading( cancel: { @@ -201,7 +201,7 @@ enum UpdateSimulator { progress: 0 ) viewModel.state = .downloading(download) - + for i in 1...5 { DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { let updatedDownload = UpdateState.Downloading( @@ -210,7 +210,7 @@ enum UpdateSimulator { progress: UInt64(i * 100) ) viewModel.state = .downloading(updatedDownload) - + if i == 5 { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { viewModel.state = .idle @@ -219,17 +219,17 @@ enum UpdateSimulator { } } } - + 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: { @@ -239,7 +239,7 @@ enum UpdateSimulator { progress: 0 ) viewModel.state = .downloading(download) - + for i in 1...10 { DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { let updatedDownload = UpdateState.Downloading( @@ -248,7 +248,7 @@ enum UpdateSimulator { progress: UInt64(i * 100) ) viewModel.state = .downloading(updatedDownload) - + if i == 10 { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { simulateExtract(viewModel) @@ -257,14 +257,14 @@ enum UpdateSimulator { } } } - + 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) { simulateInstalling(viewModel) @@ -273,7 +273,7 @@ enum UpdateSimulator { } } } - + private func simulateInstalling(_ viewModel: UpdateViewModel) { viewModel.state = .installing(.init( retryTerminatingApplication: { @@ -285,7 +285,7 @@ enum UpdateSimulator { } )) } - + private func simulateAutoUpdate(_ viewModel: UpdateViewModel) { viewModel.state = .installing(.init( isAutoUpdate: true, diff --git a/macos/Sources/Features/Update/UpdateViewModel.swift b/macos/Sources/Features/Update/UpdateViewModel.swift index 1f93046162a..8e66f4a1627 100644 --- a/macos/Sources/Features/Update/UpdateViewModel.swift +++ b/macos/Sources/Features/Update/UpdateViewModel.swift @@ -4,7 +4,7 @@ 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. @@ -38,7 +38,7 @@ class UpdateViewModel: ObservableObject { 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 { @@ -51,7 +51,7 @@ class UpdateViewModel: ObservableObject { return text } } - + /// The SF Symbol icon name for the current update state. var iconName: String? { switch state { @@ -75,7 +75,7 @@ class UpdateViewModel: ObservableObject { 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 { @@ -100,7 +100,7 @@ class UpdateViewModel: ObservableObject { 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? { @@ -120,7 +120,7 @@ class UpdateViewModel: ObservableObject { return nil } } - + /// The color to apply to the icon for the current update state. var iconColor: Color { switch state { @@ -140,7 +140,7 @@ class UpdateViewModel: ObservableObject { return .orange } } - + /// The background color for the update pill. var backgroundColor: Color { switch state { @@ -156,7 +156,7 @@ class UpdateViewModel: ObservableObject { return Color(nsColor: .controlBackgroundColor) } } - + /// The foreground (text) color for the update pill. var foregroundColor: Color { switch state { @@ -184,27 +184,27 @@ enum UpdateState: Equatable { case downloading(Downloading) case extracting(Extracting) 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) { + switch self { case .checking, .updateAvailable, .downloading, .extracting, .installing: return true - + default: return false } } - + func cancel() { switch self { case .checking(let checking): @@ -221,7 +221,7 @@ enum UpdateState: Equatable { break } } - + /// Confirms or accepts the current update state. /// - For available updates: begins installation /// - For ready-to-install: proceeds with installation @@ -233,7 +233,7 @@ enum UpdateState: Equatable { break } } - + static func == (lhs: UpdateState, rhs: UpdateState) -> Bool { switch (lhs, rhs) { case (.idle, .idle): @@ -258,38 +258,38 @@ enum UpdateState: Equatable { 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: "-") @@ -298,12 +298,12 @@ enum UpdateState: Equatable { 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) @@ -313,7 +313,7 @@ enum UpdateState: Equatable { return nil } } - + private static func extractSemanticVersion(from version: String) -> String? { let pattern = #"^\d+\.\d+\.\d+$"# if version.range(of: pattern, options: .regularExpression) != nil { @@ -321,7 +321,7 @@ enum UpdateState: Equatable { } 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) { @@ -329,7 +329,7 @@ enum UpdateState: Equatable { } return nil } - + var url: URL { switch self { case .commit(let url): return url @@ -337,32 +337,32 @@ enum UpdateState: Equatable { case .tagged(let url): return url } } - + var label: String { - switch (self) { + 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 Installing { /// True if this state is triggered by ``Ghostty/UpdateDriver/updater(_:willInstallUpdateOnQuit:immediateInstallationBlock:)`` var isAutoUpdate = false diff --git a/macos/Sources/Ghostty/FullscreenMode+Extension.swift b/macos/Sources/Ghostty/FullscreenMode+Extension.swift index 0c0bba908ea..1970209cfe0 100644 --- a/macos/Sources/Ghostty/FullscreenMode+Extension.swift +++ b/macos/Sources/Ghostty/FullscreenMode+Extension.swift @@ -7,13 +7,13 @@ extension FullscreenMode { case GHOSTTY_FULLSCREEN_NATIVE: .native - case GHOSTTY_FULLSCREEN_NON_NATIVE: + case GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE: .nonNative - case GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU: + case GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_VISIBLE_MENU: .nonNativeVisibleMenu - case GHOSTTY_FULLSCREEN_NON_NATIVE_PADDED_NOTCH: + case GHOSTTY_FULLSCREEN_MACOS_NON_NATIVE_PADDED_NOTCH: .nonNativePaddedNotch default: diff --git a/macos/Sources/Ghostty/Ghostty.Action.swift b/macos/Sources/Ghostty/Ghostty.Action.swift index 91f1491dd16..f3842fc5628 100644 --- a/macos/Sources/Ghostty/Ghostty.Action.swift +++ b/macos/Sources/Ghostty/Ghostty.Action.swift @@ -18,7 +18,7 @@ extension Ghostty.Action { } init(c: ghostty_action_color_change_s) { - switch (c.kind) { + switch c.kind { case GHOSTTY_ACTION_COLOR_KIND_FOREGROUND: self.kind = .foreground case GHOSTTY_ACTION_COLOR_KIND_BACKGROUND: @@ -40,13 +40,13 @@ extension Ghostty.Action { self.amount = c.amount } } - + struct OpenURL { enum Kind { case unknown case text case html - + init(_ c: ghostty_action_open_url_kind_e) { switch c { case GHOSTTY_ACTION_OPEN_URL_KIND_TEXT: @@ -58,13 +58,13 @@ extension Ghostty.Action { } } } - + let kind: Kind let url: String - + init(c: ghostty_action_open_url_s) { self.kind = Kind(c.kind) - + if let urlCString = c.url { let data = Data(bytes: urlCString, count: Int(c.len)) self.url = String(data: data, encoding: .utf8) ?? "" @@ -81,7 +81,7 @@ extension Ghostty.Action { case error case indeterminate case pause - + init(_ c: ghostty_action_progress_report_state_e) { switch c { case GHOSTTY_PROGRESS_STATE_REMOVE: @@ -99,26 +99,26 @@ 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 + offset = c.offset len = c.len } } struct StartSearch { let needle: String? - + init(c: ghostty_action_start_search_s) { if let needleCString = c.needle { self.needle = String(cString: needleCString) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index e3441257fdd..2f0644b9380 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -33,7 +33,7 @@ extension Ghostty { private var configPath: String? /// The ghostty app instance. We only have one of these for the entire app, although I guess /// in theory you can have multiple... I don't know why you would... - @Published var app: ghostty_app_t? = nil { + @Published var app: ghostty_app_t? { didSet { guard let old = oldValue else { return } ghostty_app_free(old) @@ -140,7 +140,7 @@ extension Ghostty { guard let app = self.app else { return } // Soft updates just call with our existing config - if (soft) { + if soft { ghostty_app_update_config(app, config.config!) return } @@ -158,7 +158,7 @@ extension Ghostty { func reloadConfig(surface: ghostty_surface_t, soft: Bool = false) { // Soft updates just call with our existing config - if (soft) { + if soft { ghostty_surface_update_config(surface, config.config!) return } @@ -183,14 +183,14 @@ extension Ghostty { func newTab(surface: ghostty_surface_t) { let action = "new_tab" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } func newWindow(surface: ghostty_surface_t) { let action = "new_window" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } @@ -213,14 +213,14 @@ extension Ghostty { func splitToggleZoom(surface: ghostty_surface_t) { let action = "toggle_split_zoom" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } func toggleFullscreen(surface: ghostty_surface_t) { let action = "toggle_fullscreen" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } @@ -241,21 +241,21 @@ extension Ghostty { case .reset: action = "reset_font_size" } - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } func toggleTerminalInspector(surface: ghostty_surface_t) { let action = "inspector:toggle" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } func resetTerminal(surface: ghostty_surface_t) { let action = "reset" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } @@ -269,7 +269,9 @@ extension Ghostty { _ userdata: UnsafeMutableRawPointer?, location: ghostty_clipboard_e, state: UnsafeMutableRawPointer? - ) {} + ) -> Bool { + return false + } static func confirmReadClipboard( _ userdata: UnsafeMutableRawPointer?, @@ -312,7 +314,6 @@ extension Ghostty { ghostty_app_set_focus(app, false) } - // MARK: Ghostty Callbacks (macOS) static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) { @@ -322,20 +323,23 @@ extension Ghostty { ]) } - static func readClipboard(_ userdata: UnsafeMutableRawPointer?, location: ghostty_clipboard_e, state: UnsafeMutableRawPointer?) { - // If we don't even have a surface, something went terrible wrong so we have - // to leak "state". + static func readClipboard( + _ userdata: UnsafeMutableRawPointer?, + location: ghostty_clipboard_e, + state: UnsafeMutableRawPointer? + ) -> Bool { let surfaceView = self.surfaceUserdata(from: userdata) - guard let surface = surfaceView.surface else { return } + guard let surface = surfaceView.surface else { return false } // Get our pasteboard - guard let pasteboard = NSPasteboard.ghostty(location) else { - return completeClipboardRequest(surface, data: "", state: state) - } + guard let pasteboard = NSPasteboard.ghostty(location) else { return false } + + // Return false if there is no text-like clipboard content so + // performable paste bindings can pass through to the terminal. + guard let str = pasteboard.getOpinionatedStringContents() else { return false } - // Get our string - let str = pasteboard.getOpinionatedStringContents() ?? "" completeClipboardRequest(surface, data: str, state: state) + return true } static func confirmReadClipboard( @@ -379,25 +383,25 @@ extension Ghostty { let surface = self.surfaceUserdata(from: userdata) guard let pasteboard = NSPasteboard.ghostty(location) else { return } guard let content = content, len > 0 else { return } - + // Convert the C array to Swift array let contentArray = (0.. Bool { let userInfo = notification.request.content.userInfo + + // We always require the notification to be attached to a surface. guard let uuidString = userInfo["surface"] as? String, let uuid = UUID(uuidString: uuidString), let surface = delegate?.findSurface(forUUID: uuid), let window = surface.window else { return false } + + // If we don't require focus then we're good! + let requireFocus = userInfo["requireFocus"] as? Bool ?? true + if !requireFocus { return true } + return !window.isKeyWindow || !surface.focused } @@ -463,7 +474,7 @@ extension Ghostty { static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) -> Bool { // Make sure it a target we understand so all our action handlers can assert - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP, GHOSTTY_TARGET_SURFACE: break @@ -473,7 +484,7 @@ extension Ghostty { } // Action dispatch - switch (action.tag) { + switch action.tag { case GHOSTTY_ACTION_QUIT: quit(app) @@ -528,6 +539,9 @@ extension Ghostty { case GHOSTTY_ACTION_SET_TITLE: setTitle(app, target: target, v: action.action.set_title) + case GHOSTTY_ACTION_SET_TAB_TITLE: + return setTabTitle(app, target: target, v: action.action.set_tab_title) + case GHOSTTY_ACTION_PROMPT_TITLE: return promptTitle(app, target: target, v: action.action.prompt_title) @@ -605,7 +619,7 @@ extension Ghostty { case GHOSTTY_ACTION_CHECK_FOR_UPDATES: checkForUpdates(app) - + case GHOSTTY_ACTION_OPEN_URL: return openURL(action.action.open_url) @@ -633,6 +647,9 @@ extension Ghostty { case GHOSTTY_ACTION_SEARCH_SELECTED: searchSelected(app, target: target, v: action.action.search_selected) + case GHOSTTY_ACTION_COMMAND_FINISHED: + commandFinished(app, target: target, v: action.action.command_finished) + case GHOSTTY_ACTION_PRESENT_TERMINAL: return presentTerminal(app, target: target) @@ -681,12 +698,12 @@ extension Ghostty { appDelegate.checkForUpdates(nil) } } - + private static func openURL( _ v: ghostty_action_open_url_s ) -> Bool { let action = Ghostty.Action.OpenURL(c: v) - + // If the URL doesn't have a valid scheme we assume its a file path. The URL // initializer will gladly take invalid URLs (e.g. plain file paths) and turn // them into schema-less URLs, but these won't open properly in text editors. @@ -695,9 +712,12 @@ extension Ghostty { if let candidate = URL(string: action.url), candidate.scheme != nil { url = candidate } else { - url = URL(filePath: action.url) + // Expand ~ to the user's home directory so that file paths + // like ~/Documents/file.txt resolve correctly. + let expandedPath = NSString(string: action.url).standardizingPath + url = URL(filePath: expandedPath) } - + switch action.kind { case .text: // Open with the default editor for `*.ghostty` file or just system text editor @@ -706,15 +726,15 @@ extension Ghostty { NSWorkspace.shared.open([url], withApplicationAt: textEditor, configuration: NSWorkspace.OpenConfiguration()) return true } - + case .html: // The extension will be HTML and we do the right thing automatically. break - + case .unknown: break } - + // Open with the default application for the URL NSWorkspace.shared.open(url) return true @@ -722,7 +742,7 @@ extension Ghostty { private static func undo(_ app: ghostty_app_t, target: ghostty_target_s) -> Bool { let undoManager: UndoManager? - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: undoManager = (NSApp.delegate as? AppDelegate)?.undoManager @@ -743,7 +763,7 @@ extension Ghostty { private static func redo(_ app: ghostty_app_t, target: ghostty_target_s) -> Bool { let undoManager: UndoManager? - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: undoManager = (NSApp.delegate as? AppDelegate)?.undoManager @@ -763,7 +783,7 @@ extension Ghostty { } private static func newWindow(_ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: NotificationCenter.default.post( name: Notification.ghosttyNewWindow, @@ -782,14 +802,13 @@ extension Ghostty { ] ) - default: assertionFailure() } } private static func newTab(_ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: NotificationCenter.default.post( name: Notification.ghosttyNewTab, @@ -819,7 +838,6 @@ extension Ghostty { ] ) - default: assertionFailure() } @@ -829,7 +847,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, direction: ghostty_action_split_direction_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: // New split does nothing with an app target Ghostty.logger.warning("new split does nothing with an app target") @@ -848,7 +866,6 @@ extension Ghostty { ] ) - default: assertionFailure() } @@ -858,7 +875,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s ) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: return false @@ -879,7 +896,7 @@ extension Ghostty { } private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s, mode: ghostty_action_close_tab_mode_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("close tabs does nothing with an app target") return @@ -888,7 +905,7 @@ extension Ghostty { guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } - switch (mode) { + switch mode { case GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS: NotificationCenter.default.post( name: .ghosttyCloseTab, @@ -914,14 +931,13 @@ extension Ghostty { assertionFailure() } - default: assertionFailure() } } private static func closeWindow(_ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("close window does nothing with an app target") return @@ -949,7 +965,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, mode raw: ghostty_action_fullscreen_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle fullscreen does nothing with an app target") return @@ -969,7 +985,6 @@ extension Ghostty { ] ) - default: assertionFailure() } @@ -978,7 +993,7 @@ extension Ghostty { private static func toggleCommandPalette( _ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle command palette does nothing with an app target") return @@ -991,7 +1006,6 @@ extension Ghostty { object: surfaceView ) - default: assertionFailure() } @@ -1001,7 +1015,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s ) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle maximize does nothing with an app target") return @@ -1014,7 +1028,6 @@ extension Ghostty { object: surfaceView ) - default: assertionFailure() } @@ -1031,7 +1044,7 @@ extension Ghostty { private static func ringBell( _ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: // Technically we could still request app attention here but there // are no known cases where the bell is rang with an app target so @@ -1056,7 +1069,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_readonly_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("set readonly does nothing with an app target") return @@ -1081,7 +1094,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, move: ghostty_action_move_tab_s) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("move tab does nothing with an app target") return false @@ -1112,7 +1125,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, tab: ghostty_action_goto_tab_e) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("goto tab does nothing with an app target") return false @@ -1144,7 +1157,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, direction: ghostty_action_goto_split_e) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("goto split does nothing with an app target") return false @@ -1250,7 +1263,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, resize: ghostty_action_resize_split_s) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("resize split does nothing with an app target") return false @@ -1283,7 +1296,7 @@ extension Ghostty { private static func equalizeSplits( _ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("equalize splits does nothing with an app target") return @@ -1296,7 +1309,6 @@ extension Ghostty { object: surfaceView ) - default: assertionFailure() } @@ -1305,7 +1317,7 @@ extension Ghostty { private static func toggleSplitZoom( _ app: ghostty_app_t, target: ghostty_target_s) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle split zoom does nothing with an app target") return false @@ -1324,7 +1336,6 @@ extension Ghostty { ) return true - default: assertionFailure() return false @@ -1335,7 +1346,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, mode: ghostty_action_inspector_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle inspector does nothing with an app target") return @@ -1349,7 +1360,6 @@ extension Ghostty { userInfo: ["mode": mode] ) - default: assertionFailure() } @@ -1359,9 +1369,9 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, n: ghostty_action_desktop_notification_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: - Ghostty.logger.warning("toggle split zoom does nothing with an app target") + Ghostty.logger.warning("desktop notification does nothing with an app target") return case GHOSTTY_TARGET_SURFACE: @@ -1369,19 +1379,106 @@ extension Ghostty { guard let surfaceView = self.surfaceView(from: surface) else { return } guard let title = String(cString: n.title!, encoding: .utf8) else { return } guard let body = String(cString: n.body!, encoding: .utf8) else { return } + showDesktopNotification(surfaceView, title: title, body: body) - let center = UNUserNotificationCenter.current() - center.requestAuthorization(options: [.alert, .sound]) { _, error in - if let error = error { - Ghostty.logger.error("Error while requesting notification authorization: \(error)") - } + default: + assertionFailure() + } + } + + private static func showDesktopNotification( + _ surfaceView: SurfaceView, + title: String, + body: String, + requireFocus: Bool = true) { + let center = UNUserNotificationCenter.current() + center.requestAuthorization(options: [.alert, .sound]) { _, error in + if let error = error { + Ghostty.logger.error("Error while requesting notification authorization: \(error)") } + } + + center.getNotificationSettings { settings in + guard settings.authorizationStatus == .authorized else { return } + surfaceView.showUserNotification( + title: title, + body: body, + requireFocus: requireFocus + ) + } + } - center.getNotificationSettings() { settings in - guard settings.authorizationStatus == .authorized else { return } - surfaceView.showUserNotification(title: title, body: body) + private static func commandFinished( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_action_command_finished_s + ) { + switch target.tag { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("command finished 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 } + + // Determine if we even care about command finish notifications + guard let config = (NSApplication.shared.delegate as? AppDelegate)?.ghostty.config else { return } + switch config.notifyOnCommandFinish { + case .never: + return + + case .unfocused: + if surfaceView.focused { return } + + case .always: + break + } + + // Determine if the command was slow enough + let duration = Duration.nanoseconds(v.duration) + guard Duration.nanoseconds(v.duration) >= config.notifyOnCommandFinishAfter else { return } + + let actions = config.notifyOnCommandFinishAction + + if actions.contains(.bell) { + NotificationCenter.default.post( + name: .ghosttyBellDidRing, + object: surfaceView + ) } + if actions.contains(.notify) { + let title: String + if v.exit_code < 0 { + title = "Command Finished" + } else if v.exit_code == 0 { + title = "Command Succeeded" + } else { + title = "Command Failed" + } + + let body: String + let formattedDuration = duration.formatted( + .units( + allowed: [.hours, .minutes, .seconds, .milliseconds], + width: .abbreviated, + fractionalPart: .hide + ) + ) + if v.exit_code < 0 { + body = "Command took \(formattedDuration)." + } else { + body = "Command took \(formattedDuration) and exited with code \(v.exit_code)." + } + + showDesktopNotification( + surfaceView, + title: title, + body: body, + requireFocus: false + ) + } default: assertionFailure() @@ -1395,7 +1492,7 @@ extension Ghostty { ) { guard let mode = SetFloatWIndow.from(mode_raw) else { return } - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle float window does nothing with an app target") return @@ -1405,7 +1502,7 @@ extension Ghostty { guard let surfaceView = self.surfaceView(from: surface) else { return } guard let window = surfaceView.window as? TerminalWindow else { return } - switch (mode) { + switch mode { case .on: window.level = .floating @@ -1429,7 +1526,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s ) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle background opacity does nothing with an app target") return @@ -1453,7 +1550,7 @@ extension Ghostty { ) { guard let mode = SetSecureInput.from(mode_raw) else { return } - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return } appDelegate.setSecureInput(mode) @@ -1464,7 +1561,7 @@ extension Ghostty { guard let appState = self.appState(fromView: surfaceView) else { return } guard appState.config.autoSecureInput else { return } - switch (mode) { + switch mode { case .on: surfaceView.passwordInput = true @@ -1492,7 +1589,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_set_title_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("set title does nothing with an app target") return @@ -1508,10 +1605,37 @@ extension Ghostty { } } + private static func setTabTitle( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_action_set_title_s + ) -> Bool { + switch target.tag { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("set tab title does nothing with an app target") + return false + + case GHOSTTY_TARGET_SURFACE: + guard let title = String(cString: v.title!, encoding: .utf8) else { return false } + let titleOverride = title.isEmpty ? nil : title + guard let surface = target.target.surface else { return false } + guard let surfaceView = self.surfaceView(from: surface) else { return false } + guard let window = surfaceView.window, + let controller = window.windowController as? BaseTerminalController + else { return false } + controller.titleOverride = titleOverride + return true + + default: + assertionFailure() + return false + } + } + private static func copyTitleToClipboard( _ app: ghostty_app_t, target: ghostty_target_s) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_SURFACE: guard let surface = target.target.surface else { return false } guard let surfaceView = self.surfaceView(from: surface) else { return false } @@ -1534,7 +1658,7 @@ extension Ghostty { let promptTitle = Action.PromptTitle(v) switch promptTitle { case .surface: - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("set title prompt does nothing with an app target") return false @@ -1551,7 +1675,7 @@ extension Ghostty { } case .tab: - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: guard let window = NSApp.mainWindow ?? NSApp.keyWindow, let controller = window.windowController as? BaseTerminalController @@ -1579,7 +1703,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_pwd_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("pwd change does nothing with an app target") return @@ -1599,7 +1723,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, shape: ghostty_action_mouse_shape_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("set mouse shapes nothing with an app target") return @@ -1609,7 +1733,6 @@ extension Ghostty { guard let surfaceView = self.surfaceView(from: surface) else { return } surfaceView.setCursorShape(shape) - default: assertionFailure() } @@ -1619,7 +1742,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_mouse_visibility_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("set mouse shapes nothing with an app target") return @@ -1627,7 +1750,7 @@ extension Ghostty { case GHOSTTY_TARGET_SURFACE: guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } - switch (v) { + switch v { case GHOSTTY_MOUSE_VISIBLE: surfaceView.setCursorVisibility(true) @@ -1638,7 +1761,6 @@ extension Ghostty { return } - default: assertionFailure() } @@ -1648,7 +1770,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_mouse_over_link_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("mouse over link does nothing with an app target") return @@ -1664,7 +1786,6 @@ extension Ghostty { let buffer = Data(bytes: v.url!, count: v.len) surfaceView.hoverUrl = String(data: buffer, encoding: .utf8) - default: assertionFailure() } @@ -1674,7 +1795,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_initial_size_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("initial size does nothing with an app target") return @@ -1682,8 +1803,7 @@ extension Ghostty { case GHOSTTY_TARGET_SURFACE: guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } - surfaceView.initialSize = NSMakeSize(Double(v.width), Double(v.height)) - + surfaceView.initialSize = NSSize(width: Double(v.width), height: Double(v.height)) default: assertionFailure() @@ -1693,7 +1813,7 @@ extension Ghostty { private static func resetWindowSize( _ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("reset window size does nothing with an app target") return @@ -1706,7 +1826,6 @@ extension Ghostty { object: surfaceView ) - default: assertionFailure() } @@ -1716,7 +1835,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_cell_size_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("mouse over link does nothing with an app target") return @@ -1738,7 +1857,7 @@ extension Ghostty { private static func renderInspector( _ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("mouse over link does nothing with an app target") return @@ -1760,7 +1879,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_renderer_health_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("mouse over link does nothing with an app target") return @@ -1785,7 +1904,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_key_sequence_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("key sequence does nothing with an app target") return @@ -1817,7 +1936,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_key_table_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("key table does nothing with an app target") return @@ -1842,7 +1961,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_progress_report_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("progress report does nothing with an app target") return @@ -1850,7 +1969,16 @@ extension Ghostty { case GHOSTTY_TARGET_SURFACE: guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } - + guard let config = (NSApplication.shared.delegate as? AppDelegate)?.ghostty.config else { return } + + guard config.progressStyle else { + Ghostty.logger.debug("progress_report action blocked by config") + DispatchQueue.main.async { + surfaceView.progressReport = nil + } + return + } + let progressReport = Ghostty.Action.ProgressReport(c: v) DispatchQueue.main.async { if progressReport.state == .remove { @@ -1869,7 +1997,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_scrollbar_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("scrollbar does nothing with an app target") return @@ -1877,7 +2005,7 @@ extension Ghostty { 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, @@ -1896,7 +2024,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_start_search_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("start_search does nothing with an app target") return @@ -1914,7 +2042,7 @@ extension Ghostty { } else { surfaceView.searchState = Ghostty.SurfaceView.SearchState(from: startSearch) } - + NotificationCenter.default.post(name: .ghosttySearchFocus, object: surfaceView) } @@ -1926,7 +2054,7 @@ extension Ghostty { private static func endSearch( _ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("end_search does nothing with an app target") return @@ -1948,7 +2076,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_search_total_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("search_total does nothing with an app target") return @@ -1971,7 +2099,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_search_selected_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("search_selected does nothing with an app target") return @@ -1993,14 +2121,13 @@ extension Ghostty { private static func configReload( _ app: ghostty_app_t, target: ghostty_target_s, - v: ghostty_action_reload_config_s) - { + v: ghostty_action_reload_config_s) { logger.info("config reload notification") guard let app_ud = ghostty_app_userdata(app) else { return } let ghostty = Unmanaged.fromOpaque(app_ud).takeUnretainedValue() - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: ghostty.reloadConfig(soft: v.soft) return @@ -2026,7 +2153,7 @@ extension Ghostty { // something so apprt's do not have to do this. let config = Config(clone: v.config) - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: // Notify the world that the app config changed NotificationCenter.default.post( @@ -2066,7 +2193,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, change: ghostty_action_color_change_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("color change does nothing with an app target") return @@ -2087,7 +2214,6 @@ extension Ghostty { } } - // MARK: User Notifications /// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user @@ -2097,7 +2223,7 @@ extension Ghostty { let uuid = UUID(uuidString: uuidString), let surface = delegate?.findSurface(forUUID: uuid) else { return } - switch (response.actionIdentifier) { + switch response.actionIdentifier { case UNNotificationDefaultActionIdentifier, Ghostty.userNotificationActionShow: // The user clicked on a notification surface.handleUserNotification(notification: response.notification, focus: true) diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index c64646e25df..160894b18cf 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -7,7 +7,7 @@ extension Ghostty { // The underlying C pointer to the Ghostty config structure. This // should never be accessed directly. Any operations on this should // be called from the functions on this or another class. - private(set) var config: ghostty_config_t? = nil { + private(set) var config: ghostty_config_t? { didSet { // Free the old value whenever we change guard let old = oldValue else { return } @@ -22,7 +22,7 @@ extension Ghostty { var errors: [String] { guard let cfg = self.config else { return [] } - var diags: [String] = []; + var diags: [String] = [] let diagsCount = ghostty_config_diagnostics_count(cfg) for i in 0.. ghostty_config_t? { + static func loadConfig(at path: String?, finalize: Bool) -> ghostty_config_t? { // Initialize the global configuration. guard let cfg = ghostty_config_new() else { logger.critical("ghostty_config_new failed") @@ -73,10 +73,10 @@ extension Ghostty { // We only load CLI args when not running in Xcode because in Xcode we // pass some special parameters to control the debugger. if !isRunningInXcode() { - ghostty_config_load_cli_args(cfg); + ghostty_config_load_cli_args(cfg) } - ghostty_config_load_recursive_files(cfg); + ghostty_config_load_recursive_files(cfg) #endif // TODO: we'd probably do some config loading here... for now we'd @@ -92,7 +92,7 @@ extension Ghostty { let diagsCount = ghostty_config_diagnostics_count(cfg) if diagsCount > 0 { logger.warning("config error: \(diagsCount) configuration errors on reload") - var diags: [String] = []; + var diags: [String] = [] for i in 0..? + let key = "notify-on-command-finish" + guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .never } + guard let ptr = v else { return .never } + return NotifyOnCommandFinish(rawValue: String(cString: ptr)) ?? .never + } + + var notifyOnCommandFinishAction: NotifyOnCommandFinishAction { + let defaultValue = NotifyOnCommandFinishAction.bell + guard let config = self.config else { return defaultValue } + var v: CUnsignedInt = 0 + let key = "notify-on-command-finish-action" + guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } + return .init(rawValue: v) + } + + var notifyOnCommandFinishAfter: Duration { + guard let config = self.config else { return .seconds(5) } + var v: UInt = 0 + let key = "notify-on-command-finish-after" + _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) + return .milliseconds(v) + } + var splitPreserveZoom: SplitPreserveZoom { guard let config = self.config else { return .init() } var v: CUnsignedInt = 0 @@ -144,7 +187,7 @@ extension Ghostty { var initialWindow: Bool { guard let config = self.config else { return true } - var v = true; + var v = true let key = "initial-window" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v @@ -152,7 +195,7 @@ extension Ghostty { var shouldQuitAfterLastWindowClosed: Bool { guard let config = self.config else { return true } - var v = false; + var v = false let key = "quit-after-last-window-closed" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v @@ -160,7 +203,7 @@ extension Ghostty { var title: String? { guard let config = self.config else { return nil } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "title" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard let ptr = v else { return nil } @@ -169,7 +212,7 @@ extension Ghostty { var windowSaveState: String { guard let config = self.config else { return "" } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "window-save-state" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return "" } guard let ptr = v else { return "" } @@ -192,7 +235,7 @@ extension Ghostty { var windowNewTabPosition: String { guard let config = self.config else { return "" } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "window-new-tab-position" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return "" } guard let ptr = v else { return "" } @@ -202,7 +245,7 @@ extension Ghostty { var windowDecorations: Bool { let defaultValue = true guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "window-decoration" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -212,7 +255,7 @@ extension Ghostty { var windowTheme: String? { guard let config = self.config else { return nil } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "window-theme" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard let ptr = v else { return nil } @@ -227,19 +270,51 @@ extension Ghostty { return v } + /// Returns the fullscreen mode if fullscreen is enabled, or nil if disabled. + /// This parses the `fullscreen` enum config which supports both + /// native and non-native fullscreen modes. + #if canImport(AppKit) + var windowFullscreen: FullscreenMode? { + guard let config = self.config else { return nil } + var v: UnsafePointer? + let key = "fullscreen" + guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } + guard let ptr = v else { return nil } + let str = String(cString: ptr) + return switch str { + case "false": + nil + case "true": + .native + case "non-native": + .nonNative + case "non-native-visible-menu": + .nonNativeVisibleMenu + case "non-native-padded-notch": + .nonNativePaddedNotch + default: + nil + } + } + #else var windowFullscreen: Bool { - guard let config = self.config else { return true } - var v = false + guard let config = self.config else { return false } + var v: UnsafePointer? let key = "fullscreen" - _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) - return v + guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return false } + guard let ptr = v else { return false } + let str = String(cString: ptr) + return str != "false" } + #endif + /// Returns the fullscreen mode for toggle actions (keybindings). + /// This is controlled by `macos-non-native-fullscreen` config. #if canImport(AppKit) var windowFullscreenMode: FullscreenMode { let defaultValue: FullscreenMode = .native guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-non-native-fullscreen" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -261,7 +336,7 @@ extension Ghostty { var windowTitleFontFamily: String? { guard let config = self.config else { return nil } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "window-title-font-family" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard let ptr = v else { return nil } @@ -271,7 +346,7 @@ extension Ghostty { var macosWindowButtons: MacOSWindowButtons { let defaultValue = MacOSWindowButtons.visible guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-window-buttons" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -279,20 +354,20 @@ extension Ghostty { return MacOSWindowButtons(rawValue: str) ?? defaultValue } - var macosTitlebarStyle: String { - let defaultValue = "transparent" + var macosTitlebarStyle: MacOSTitlebarStyle { + let defaultValue = MacOSTitlebarStyle.transparent guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-titlebar-style" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } - return String(cString: ptr) + return MacOSTitlebarStyle(rawValue: String(cString: ptr)) ?? defaultValue } var macosTitlebarProxyIcon: MacOSTitlebarProxyIcon { let defaultValue = MacOSTitlebarProxyIcon.visible guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-titlebar-proxy-icon" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -303,7 +378,7 @@ extension Ghostty { var macosDockDropBehavior: MacDockDropBehavior { let defaultValue = MacDockDropBehavior.new_tab guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-dock-drop-behavior" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -313,7 +388,7 @@ extension Ghostty { var macosWindowShadow: Bool { guard let config = self.config else { return false } - var v = false; + var v = false let key = "macos-window-shadow" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v @@ -322,7 +397,7 @@ extension Ghostty { var macosIcon: MacOSIcon { let defaultValue = MacOSIcon.official guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-icon" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -334,7 +409,7 @@ extension Ghostty { #if os(macOS) let defaultValue = NSString("~/.config/ghostty/Ghostty.icns").expandingTildeInPath guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-custom-icon" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -348,7 +423,7 @@ extension Ghostty { var macosIconFrame: MacOSIconFrame { let defaultValue = MacOSIconFrame.aluminum guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-icon-frame" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -376,7 +451,7 @@ extension Ghostty { var macosHidden: MacHidden { guard let config = self.config else { return .never } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-hidden" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .never } guard let ptr = v else { return .never } @@ -384,18 +459,18 @@ extension Ghostty { return MacHidden(rawValue: str) ?? .never } - var focusFollowsMouse : Bool { + var focusFollowsMouse: Bool { guard let config = self.config else { return false } - var v = false; + var v = false let key = "focus-follows-mouse" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v } var backgroundColor: Color { - var color: ghostty_config_color_s = .init(); + var color: ghostty_config_color_s = .init() let bg_key = "background" - if (!ghostty_config_get(config, &color, bg_key, UInt(bg_key.lengthOfBytes(using: .utf8)))) { + if !ghostty_config_get(config, &color, bg_key, UInt(bg_key.lengthOfBytes(using: .utf8))) { #if os(macOS) return Color(NSColor.windowBackgroundColor) #elseif os(iOS) @@ -417,7 +492,7 @@ extension Ghostty { var v: Double = 1 let key = "background-opacity" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) - return v; + return v } var backgroundBlur: BackgroundBlur { @@ -439,11 +514,11 @@ extension Ghostty { var unfocusedSplitFill: Color { guard let config = self.config else { return .white } - var color: ghostty_config_color_s = .init(); + var color: ghostty_config_color_s = .init() let key = "unfocused-split-fill" - if (!ghostty_config_get(config, &color, key, UInt(key.lengthOfBytes(using: .utf8)))) { + if !ghostty_config_get(config, &color, key, UInt(key.lengthOfBytes(using: .utf8))) { let bg_key = "background" - _ = ghostty_config_get(config, &color, bg_key, UInt(bg_key.lengthOfBytes(using: .utf8))); + _ = ghostty_config_get(config, &color, bg_key, UInt(bg_key.lengthOfBytes(using: .utf8))) } return .init( @@ -460,9 +535,9 @@ extension Ghostty { guard let config = self.config else { return Color(newColor) } - var color: ghostty_config_color_s = .init(); + var color: ghostty_config_color_s = .init() let key = "split-divider-color" - if (!ghostty_config_get(config, &color, key, UInt(key.lengthOfBytes(using: .utf8)))) { + if !ghostty_config_get(config, &color, key, UInt(key.lengthOfBytes(using: .utf8))) { return Color(newColor) } @@ -476,7 +551,7 @@ extension Ghostty { #if canImport(AppKit) var quickTerminalPosition: QuickTerminalPosition { guard let config = self.config else { return .top } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "quick-terminal-position" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .top } guard let ptr = v else { return .top } @@ -486,7 +561,7 @@ extension Ghostty { var quickTerminalScreen: QuickTerminalScreen { guard let config = self.config else { return .main } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "quick-terminal-screen" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .main } guard let ptr = v else { return .main } @@ -512,7 +587,7 @@ extension Ghostty { var quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior { guard let config = self.config else { return .move } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "quick-terminal-space-behavior" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .move } guard let ptr = v else { return .move } @@ -531,7 +606,7 @@ extension Ghostty { var resizeOverlay: ResizeOverlay { guard let config = self.config else { return .after_first } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "resize-overlay" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .after_first } guard let ptr = v else { return .after_first } @@ -542,7 +617,7 @@ extension Ghostty { var resizeOverlayPosition: ResizeOverlayPosition { let defaultValue = ResizeOverlayPosition.center guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "resize-overlay-position" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -555,7 +630,7 @@ extension Ghostty { var v: UInt = 0 let key = "resize-overlay-duration" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) - return v; + return v } var undoTimeout: Duration { @@ -568,7 +643,7 @@ extension Ghostty { var autoUpdate: AutoUpdate? { guard let config = self.config else { return nil } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "auto-update" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard let ptr = v else { return nil } @@ -579,7 +654,7 @@ extension Ghostty { var autoUpdateChannel: AutoUpdateChannel { let defaultValue = AutoUpdateChannel.stable guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "auto-update-channel" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -589,7 +664,7 @@ extension Ghostty { var autoSecureInput: Bool { guard let config = self.config else { return true } - var v = false; + var v = false let key = "macos-auto-secure-input" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v @@ -597,15 +672,23 @@ extension Ghostty { var secureInputIndication: Bool { guard let config = self.config else { return true } - var v = false; + var v = false let key = "macos-secure-input-indication" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v } + var macosAppleScript: Bool { + guard let config = self.config else { return true } + var v = false + let key = "macos-applescript" + _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) + return v + } + var maximize: Bool { guard let config = self.config else { return true } - var v = false; + var v = false let key = "maximize" _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) return v @@ -614,7 +697,7 @@ extension Ghostty { var macosShortcuts: MacShortcuts { let defaultValue = MacShortcuts.ask guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-shortcuts" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -625,7 +708,7 @@ extension Ghostty { var scrollbar: Scrollbar { let defaultValue = Scrollbar.system guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "scrollbar" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -642,13 +725,21 @@ extension Ghostty { let buffer = UnsafeBufferPointer(start: v.commands, count: v.len) return buffer.map { Ghostty.Command(cValue: $0) } } + + var progressStyle: Bool { + guard let config = self.config else { return true } + var v = true + let key = "progress-style" + _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) + return v + } } } // MARK: Configuration Enums extension Ghostty.Config { - enum AutoUpdate : String { + enum AutoUpdate: String { case off case check case download @@ -731,13 +822,13 @@ extension Ghostty.Config { static let navigation = SplitPreserveZoom(rawValue: 1 << 0) } - + enum MacDockDropBehavior: String { case new_tab = "new-tab" case new_window = "new-window" } - enum MacHidden : String { + enum MacHidden: String { case never case always } @@ -753,13 +844,13 @@ extension Ghostty.Config { case never } - enum ResizeOverlay : String { + enum ResizeOverlay: String { case always case never case after_first = "after-first" } - enum ResizeOverlayPosition : String { + enum ResizeOverlayPosition: String { case center case top_left = "top-left" case top_center = "top-center" @@ -769,30 +860,30 @@ extension Ghostty.Config { case bottom_right = "bottom-right" func top() -> Bool { - switch (self) { - case .top_left, .top_center, .top_right: return true; - default: return false; + switch self { + case .top_left, .top_center, .top_right: return true + default: return false } } func bottom() -> Bool { - switch (self) { - case .bottom_left, .bottom_center, .bottom_right: return true; - default: return false; + switch self { + case .bottom_left, .bottom_center, .bottom_right: return true + default: return false } } func left() -> Bool { - switch (self) { - case .top_left, .bottom_left: return true; - default: return false; + switch self { + case .top_left, .bottom_left: return true + default: return false } } func right() -> Bool { - switch (self) { - case .top_right, .bottom_right: return true; - default: return false; + switch self { + case .top_right, .bottom_right: return true + default: return false } } } @@ -810,4 +901,22 @@ extension Ghostty.Config { } } } + + enum NotifyOnCommandFinish: String { + case never + case unfocused + case always + } + + struct NotifyOnCommandFinishAction: OptionSet { + let rawValue: CUnsignedInt + + static let bell = NotifyOnCommandFinishAction(rawValue: 1 << 0) + static let notify = NotifyOnCommandFinishAction(rawValue: 1 << 1) + } + + enum MacOSTitlebarStyle: String { + static let `default` = MacOSTitlebarStyle.transparent + case native, transparent, tabs, hidden + } } diff --git a/macos/Sources/Ghostty/Ghostty.ConfigTypes.swift b/macos/Sources/Ghostty/Ghostty.ConfigTypes.swift new file mode 100644 index 00000000000..90470f38a87 --- /dev/null +++ b/macos/Sources/Ghostty/Ghostty.ConfigTypes.swift @@ -0,0 +1,49 @@ +// This file contains the configuration types for Ghostty so that alternate targets +// can get typed information without depending on all the dependencies of GhosttyKit. + +extension Ghostty { + /// A configuration path value that may be optional or required. + struct ConfigPath: Sendable { + let path: String + let optional: Bool + } + + /// macos-icon + enum MacOSIcon: String, Sendable { + case official + case blueprint + case chalkboard + case glass + case holographic + case microchip + case paper + case retro + case xray + case custom + case customStyle = "custom-style" + + /// Bundled asset name for built-in icons + var assetName: String? { + switch self { + case .official: return nil + case .blueprint: return "BlueprintImage" + case .chalkboard: return "ChalkboardImage" + case .microchip: return "MicrochipImage" + case .glass: return "GlassImage" + case .holographic: return "HolographicImage" + case .paper: return "PaperImage" + case .retro: return "RetroImage" + case .xray: return "XrayImage" + case .custom, .customStyle: return nil + } + } + } + + /// macos-icon-frame + enum MacOSIconFrame: String, Codable { + case aluminum + case beige + case plastic + case chrome + } +} diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index 7b2905abb36..27f4d05ddb1 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -18,7 +18,7 @@ extension Ghostty { /// be used for things like NSMenu that only support keyboard shortcuts anyways. static func keyboardShortcut(for trigger: ghostty_input_trigger_s) -> KeyboardShortcut? { let key: KeyEquivalent - switch (trigger.tag) { + switch trigger.tag { case GHOSTTY_TRIGGER_PHYSICAL: // Only functional keys can be converted to a KeyboardShortcut. Other physical // mappings cannot because KeyboardShortcut in Swift is inherently layout-dependent. @@ -49,11 +49,11 @@ extension Ghostty { /// Returns the event modifier flags set for the Ghostty mods enum. static func eventModifierFlags(mods: ghostty_input_mods_e) -> NSEvent.ModifierFlags { - var flags = NSEvent.ModifierFlags(rawValue: 0); - if (mods.rawValue & GHOSTTY_MODS_SHIFT.rawValue != 0) { flags.insert(.shift) } - if (mods.rawValue & GHOSTTY_MODS_CTRL.rawValue != 0) { flags.insert(.control) } - if (mods.rawValue & GHOSTTY_MODS_ALT.rawValue != 0) { flags.insert(.option) } - if (mods.rawValue & GHOSTTY_MODS_SUPER.rawValue != 0) { flags.insert(.command) } + var flags = NSEvent.ModifierFlags(rawValue: 0) + if mods.rawValue & GHOSTTY_MODS_SHIFT.rawValue != 0 { flags.insert(.shift) } + if mods.rawValue & GHOSTTY_MODS_CTRL.rawValue != 0 { flags.insert(.control) } + if mods.rawValue & GHOSTTY_MODS_ALT.rawValue != 0 { flags.insert(.option) } + if mods.rawValue & GHOSTTY_MODS_SUPER.rawValue != 0 { flags.insert(.command) } return flags } @@ -61,19 +61,19 @@ extension Ghostty { static func ghosttyMods(_ flags: NSEvent.ModifierFlags) -> ghostty_input_mods_e { var mods: UInt32 = GHOSTTY_MODS_NONE.rawValue - if (flags.contains(.shift)) { mods |= GHOSTTY_MODS_SHIFT.rawValue } - if (flags.contains(.control)) { mods |= GHOSTTY_MODS_CTRL.rawValue } - if (flags.contains(.option)) { mods |= GHOSTTY_MODS_ALT.rawValue } - if (flags.contains(.command)) { mods |= GHOSTTY_MODS_SUPER.rawValue } - if (flags.contains(.capsLock)) { mods |= GHOSTTY_MODS_CAPS.rawValue } + if flags.contains(.shift) { mods |= GHOSTTY_MODS_SHIFT.rawValue } + if flags.contains(.control) { mods |= GHOSTTY_MODS_CTRL.rawValue } + if flags.contains(.option) { mods |= GHOSTTY_MODS_ALT.rawValue } + if flags.contains(.command) { mods |= GHOSTTY_MODS_SUPER.rawValue } + if flags.contains(.capsLock) { mods |= GHOSTTY_MODS_CAPS.rawValue } // Handle sided input. We can't tell that both are pressed in the // Ghostty structure but that's okay -- we don't use that information. let rawFlags = flags.rawValue - if (rawFlags & UInt(NX_DEVICERSHIFTKEYMASK) != 0) { mods |= GHOSTTY_MODS_SHIFT_RIGHT.rawValue } - if (rawFlags & UInt(NX_DEVICERCTLKEYMASK) != 0) { mods |= GHOSTTY_MODS_CTRL_RIGHT.rawValue } - if (rawFlags & UInt(NX_DEVICERALTKEYMASK) != 0) { mods |= GHOSTTY_MODS_ALT_RIGHT.rawValue } - if (rawFlags & UInt(NX_DEVICERCMDKEYMASK) != 0) { mods |= GHOSTTY_MODS_SUPER_RIGHT.rawValue } + if rawFlags & UInt(NX_DEVICERSHIFTKEYMASK) != 0 { mods |= GHOSTTY_MODS_SHIFT_RIGHT.rawValue } + if rawFlags & UInt(NX_DEVICERCTLKEYMASK) != 0 { mods |= GHOSTTY_MODS_CTRL_RIGHT.rawValue } + if rawFlags & UInt(NX_DEVICERALTKEYMASK) != 0 { mods |= GHOSTTY_MODS_ALT_RIGHT.rawValue } + if rawFlags & UInt(NX_DEVICERCMDKEYMASK) != 0 { mods |= GHOSTTY_MODS_SUPER_RIGHT.rawValue } return ghostty_input_mods_e(mods) } @@ -81,7 +81,7 @@ extension Ghostty { /// A map from the Ghostty key enum to the keyEquivalent string for shortcuts. Note that /// not all ghostty key enum values are represented here because not all of them can be /// mapped to a KeyEquivalent. - static let keyToEquivalent: [ghostty_input_key_e : KeyEquivalent] = [ + static let keyToEquivalent: [ghostty_input_key_e: KeyEquivalent] = [ // Function keys GHOSTTY_KEY_ARROW_UP: .upArrow, GHOSTTY_KEY_ARROW_DOWN: .downArrow, @@ -243,7 +243,7 @@ extension Ghostty.Input { extension Ghostty.Input.Action: AppEnum { static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Key Action") - static var caseDisplayRepresentations: [Ghostty.Input.Action : DisplayRepresentation] = [ + static var caseDisplayRepresentations: [Ghostty.Input.Action: DisplayRepresentation] = [ .release: "Release", .press: "Press", .repeat: "Repeat" @@ -355,7 +355,7 @@ extension Ghostty.Input { extension Ghostty.Input.MouseState: AppEnum { static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Mouse State") - static var caseDisplayRepresentations: [Ghostty.Input.MouseState : DisplayRepresentation] = [ + static var caseDisplayRepresentations: [Ghostty.Input.MouseState: DisplayRepresentation] = [ .release: "Release", .press: "Press" ] @@ -420,7 +420,7 @@ extension Ghostty.Input { extension Ghostty.Input.MouseButton: AppEnum { static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Mouse Button") - static var caseDisplayRepresentations: [Ghostty.Input.MouseButton : DisplayRepresentation] = [ + static var caseDisplayRepresentations: [Ghostty.Input.MouseButton: DisplayRepresentation] = [ .unknown: "Unknown", .left: "Left", .right: "Right", @@ -504,7 +504,7 @@ extension Ghostty.Input { extension Ghostty.Input.Momentum: AppEnum { static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Scroll Momentum") - static var caseDisplayRepresentations: [Ghostty.Input.Momentum : DisplayRepresentation] = [ + static var caseDisplayRepresentations: [Ghostty.Input.Momentum: DisplayRepresentation] = [ .none: "None", .began: "Began", .stationary: "Stationary", @@ -1223,7 +1223,7 @@ extension Ghostty.Input.Key: AppEnum { ] } - static var caseDisplayRepresentations: [Ghostty.Input.Key : DisplayRepresentation] = [ + static var caseDisplayRepresentations: [Ghostty.Input.Key: DisplayRepresentation] = [ // Letters (A-Z) .a: "A", .b: "B", .c: "C", .d: "D", .e: "E", .f: "F", .g: "G", .h: "H", .i: "I", .j: "J", .k: "K", .l: "L", .m: "M", .n: "N", .o: "O", .p: "P", .q: "Q", .r: "R", .s: "S", .t: "T", diff --git a/macos/Sources/Ghostty/Ghostty.Surface.swift b/macos/Sources/Ghostty/Ghostty.Surface.swift index 7cb32ed71ae..b072db15e36 100644 --- a/macos/Sources/Ghostty/Ghostty.Surface.swift +++ b/macos/Sources/Ghostty/Ghostty.Surface.swift @@ -40,7 +40,7 @@ extension Ghostty { @MainActor func sendText(_ text: String) { let len = text.utf8CString.count - if (len == 0) { return } + if len == 0 { return } text.withCString { ptr in // len includes the null terminator so we do len - 1 @@ -149,7 +149,7 @@ extension Ghostty { @MainActor func perform(action: String) -> Bool { let len = action.utf8CString.count - if (len == 0) { return false } + if len == 0 { return false } return action.withCString { cString in ghostty_surface_binding_action(surface, cString, UInt(len - 1)) } diff --git a/macos/Sources/Ghostty/GhosttyPackage.swift b/macos/Sources/Ghostty/GhosttyPackage.swift new file mode 100644 index 00000000000..03211862fba --- /dev/null +++ b/macos/Sources/Ghostty/GhosttyPackage.swift @@ -0,0 +1,453 @@ +import os +import SwiftUI +import GhosttyKit + +// MARK: C Extensions + +/// A command is fully self-contained so it is Sendable. +extension ghostty_command_s: @unchecked @retroactive Sendable {} + +/// A surface is sendable because it is just a reference type. Using the surface in parameters +/// may be unsafe but the value itself is safe to send across threads. +extension ghostty_surface_t: @unchecked @retroactive Sendable {} + +extension Ghostty { + // The user notification category identifier + static let userNotificationCategory = "com.mitchellh.ghostty.userNotification" + + // The user notification "Show" action + static let userNotificationActionShow = "com.mitchellh.ghostty.userNotification.Show" +} + +// MARK: Build Info + +extension Ghostty { + struct Info { + var mode: ghostty_build_mode_e + var version: String + } + + static var info: Info { + let raw = ghostty_info() + let version = NSString( + bytes: raw.version, + length: Int(raw.version_len), + encoding: NSUTF8StringEncoding + ) ?? "unknown" + + return Info(mode: raw.build_mode, version: String(version)) + } +} + +// MARK: General Helpers + +extension Ghostty { + enum LaunchSource: String { + case cli + case app + case zig_run + } + + /// Returns the mechanism that launched the app. This is based on an env var so + /// its up to the env var being set in the correct circumstance. + static var launchSource: LaunchSource { + guard let envValue = ProcessInfo.processInfo.environment["GHOSTTY_MAC_LAUNCH_SOURCE"] else { + // We default to the CLI because the app bundle always sets the + // source. If its unset we assume we're in a CLI environment. + return .cli + } + + // If the env var is set but its unknown then we default back to the app. + return LaunchSource(rawValue: envValue) ?? .app + } +} + +// MARK: Swift Types for C Types + +extension Ghostty { + class AllocatedString { + private let cString: ghostty_string_s + + init(_ c: ghostty_string_s) { + self.cString = c + } + + var string: String { + guard let ptr = cString.ptr else { return "" } + let data = Data(bytes: ptr, count: Int(cString.len)) + return String(data: data, encoding: .utf8) ?? "" + } + + deinit { + ghostty_string_free(cString) + } + } +} + +extension Ghostty { + enum SetFloatWIndow { + case on + case off + case toggle + + static func from(_ c: ghostty_action_float_window_e) -> Self? { + switch c { + case GHOSTTY_FLOAT_WINDOW_ON: + return .on + + case GHOSTTY_FLOAT_WINDOW_OFF: + return .off + + case GHOSTTY_FLOAT_WINDOW_TOGGLE: + return .toggle + + default: + return nil + } + } + } + + enum SetSecureInput { + case on + case off + case toggle + + static func from(_ c: ghostty_action_secure_input_e) -> Self? { + switch c { + case GHOSTTY_SECURE_INPUT_ON: + return .on + + case GHOSTTY_SECURE_INPUT_OFF: + return .off + + case GHOSTTY_SECURE_INPUT_TOGGLE: + return .toggle + + default: + return nil + } + } + } + + /// An enum that is used for the directions that a split focus event can change. + enum SplitFocusDirection { + case previous, next, up, down, left, right + + /// Initialize from a Ghostty API enum. + static func from(direction: ghostty_action_goto_split_e) -> Self? { + switch direction { + case GHOSTTY_GOTO_SPLIT_PREVIOUS: + return .previous + + case GHOSTTY_GOTO_SPLIT_NEXT: + return .next + + case GHOSTTY_GOTO_SPLIT_UP: + return .up + + case GHOSTTY_GOTO_SPLIT_DOWN: + return .down + + case GHOSTTY_GOTO_SPLIT_LEFT: + return .left + + case GHOSTTY_GOTO_SPLIT_RIGHT: + return .right + + default: + return nil + } + } + + func toNative() -> ghostty_action_goto_split_e { + switch self { + case .previous: + return GHOSTTY_GOTO_SPLIT_PREVIOUS + + case .next: + return GHOSTTY_GOTO_SPLIT_NEXT + + case .up: + return GHOSTTY_GOTO_SPLIT_UP + + case .down: + return GHOSTTY_GOTO_SPLIT_DOWN + + case .left: + return GHOSTTY_GOTO_SPLIT_LEFT + + case .right: + return GHOSTTY_GOTO_SPLIT_RIGHT + } + } + } + + /// Enum used for resizing splits. This is the direction the split divider will move. + enum SplitResizeDirection { + case up, down, left, right + + static func from(direction: ghostty_action_resize_split_direction_e) -> Self? { + switch direction { + case GHOSTTY_RESIZE_SPLIT_UP: + return .up + case GHOSTTY_RESIZE_SPLIT_DOWN: + return .down + case GHOSTTY_RESIZE_SPLIT_LEFT: + return .left + case GHOSTTY_RESIZE_SPLIT_RIGHT: + return .right + default: + return nil + } + } + + func toNative() -> ghostty_action_resize_split_direction_e { + switch self { + case .up: + return GHOSTTY_RESIZE_SPLIT_UP + case .down: + return GHOSTTY_RESIZE_SPLIT_DOWN + case .left: + return GHOSTTY_RESIZE_SPLIT_LEFT + case .right: + return GHOSTTY_RESIZE_SPLIT_RIGHT + } + } + } +} + +#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 + case paste + + /// An application is attempting to read from the clipboard using OSC 52 + case osc_52_read + + /// An application is attempting to write to the clipboard using OSC 52 + case osc_52_write(OSPasteboard?) + + /// The text to show in the clipboard confirmation prompt for a given request type + func text() -> String { + switch self { + case .paste: + return """ + Pasting this text to the terminal may be dangerous as it looks like some commands may be executed. + """ + case .osc_52_read: + return """ + An application is attempting to read from the clipboard. + The current clipboard contents are shown below. + """ + case .osc_52_write: + return """ + An application is attempting to write to the clipboard. + The content to write is shown below. + """ + } + } + + static func from(request: ghostty_clipboard_request_e) -> ClipboardRequest? { + switch request { + case GHOSTTY_CLIPBOARD_REQUEST_PASTE: + return .paste + case GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ: + return .osc_52_read + case GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE: + return .osc_52_write(nil) + default: + return nil + } + } + } + + struct ClipboardContent { + let mime: String + let data: String + + static func from(content: ghostty_clipboard_content_s) -> ClipboardContent? { + guard let mimePtr = content.mime, + let dataPtr = content.data else { + return nil + } + + return ClipboardContent( + mime: String(cString: mimePtr), + data: String(cString: dataPtr) + ) + } + } + + /// Enum for the macos-window-buttons config option + enum MacOSWindowButtons: String { + case visible + case hidden + } + + /// Enum for the macos-titlebar-proxy-icon config option + enum MacOSTitlebarProxyIcon: String { + case visible + case hidden + } + + /// Enum for auto-update-channel config option + enum AutoUpdateChannel: String { + case tip + case stable + } +} + +// MARK: Surface Notification + +extension Notification.Name { + /// Configuration change. If the object is nil then it is app-wide. Otherwise its surface-specific. + static let ghosttyConfigDidChange = Notification.Name("com.mitchellh.ghostty.configDidChange") + static let GhosttyConfigChangeKey = ghosttyConfigDidChange.rawValue + + /// Color change. Object is the surface changing. + static let ghosttyColorDidChange = Notification.Name("com.mitchellh.ghostty.ghosttyColorDidChange") + static let GhosttyColorChangeKey = ghosttyColorDidChange.rawValue + + /// Goto tab. Has tab index in the userinfo. + static let ghosttyMoveTab = Notification.Name("com.mitchellh.ghostty.moveTab") + static let GhosttyMoveTabKey = ghosttyMoveTab.rawValue + + /// Close tab + static let ghosttyCloseTab = Notification.Name("com.mitchellh.ghostty.closeTab") + + /// Close other tabs + static let ghosttyCloseOtherTabs = Notification.Name("com.mitchellh.ghostty.closeOtherTabs") + + /// Close tabs to the right of the focused tab + static let ghosttyCloseTabsOnTheRight = Notification.Name("com.mitchellh.ghostty.closeTabsOnTheRight") + + /// Close window + static let ghosttyCloseWindow = Notification.Name("com.mitchellh.ghostty.closeWindow") + + /// Resize the window to a default size. + static let ghosttyResetWindowSize = Notification.Name("com.mitchellh.ghostty.resetWindowSize") + + /// Ring the bell + static let ghosttyBellDidRing = Notification.Name("com.mitchellh.ghostty.ghosttyBellDidRing") + + /// Readonly mode changed + static let ghosttyDidChangeReadonly = Notification.Name("com.mitchellh.ghostty.didChangeReadonly") + static let ReadonlyKey = ghosttyDidChangeReadonly.rawValue + ".readonly" + static let ghosttyCommandPaletteDidToggle = Notification.Name("com.mitchellh.ghostty.commandPaletteDidToggle") + + /// Toggle maximize of current window + 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" + + /// Focus the search field + static let ghosttySearchFocus = Notification.Name("com.mitchellh.ghostty.searchFocus") +} + +// NOTE: I am moving all of these to Notification.Name extensions over time. This +// namespace was the old namespace. +extension Ghostty.Notification { + /// Used to pass a configuration along when creating a new tab/window/split. + static let NewSurfaceConfigKey = "com.mitchellh.ghostty.newSurfaceConfig" + + /// Posted when a new split is requested. The sending object will be the surface that had focus. The + /// userdata has one key "direction" with the direction to split to. + static let ghosttyNewSplit = Notification.Name("com.mitchellh.ghostty.newSplit") + + /// Close the calling surface. + static let ghosttyCloseSurface = Notification.Name("com.mitchellh.ghostty.closeSurface") + + /// Focus previous/next split. Has a SplitFocusDirection in the userinfo. + static let ghosttyFocusSplit = Notification.Name("com.mitchellh.ghostty.focusSplit") + static let SplitDirectionKey = ghosttyFocusSplit.rawValue + + /// Goto tab. Has tab index in the userinfo. + static let ghosttyGotoTab = Notification.Name("com.mitchellh.ghostty.gotoTab") + static let GotoTabKey = ghosttyGotoTab.rawValue + + /// New tab. Has base surface config requested in userinfo. + static let ghosttyNewTab = Notification.Name("com.mitchellh.ghostty.newTab") + + /// New window. Has base surface config requested in userinfo. + static let ghosttyNewWindow = Notification.Name("com.mitchellh.ghostty.newWindow") + + /// Present terminal. Bring the surface's window to focus without activating the app. + static let ghosttyPresentTerminal = Notification.Name("com.mitchellh.ghostty.presentTerminal") + + /// Toggle fullscreen of current window + static let ghosttyToggleFullscreen = Notification.Name("com.mitchellh.ghostty.toggleFullscreen") + static let FullscreenModeKey = ghosttyToggleFullscreen.rawValue + + /// Notification sent to toggle split maximize/unmaximize. + static let didToggleSplitZoom = Notification.Name("com.mitchellh.ghostty.didToggleSplitZoom") + + /// Notification + static let didReceiveInitialWindowFrame = Notification.Name("com.mitchellh.ghostty.didReceiveInitialWindowFrame") + static let FrameKey = "com.mitchellh.ghostty.frame" + + /// Notification to render the inspector for a surface + static let inspectorNeedsDisplay = Notification.Name("com.mitchellh.ghostty.inspectorNeedsDisplay") + + /// Notification to show/hide the inspector + static let didControlInspector = Notification.Name("com.mitchellh.ghostty.didControlInspector") + + static let confirmClipboard = Notification.Name("com.mitchellh.ghostty.confirmClipboard") + static let ConfirmClipboardStrKey = confirmClipboard.rawValue + ".str" + static let ConfirmClipboardStateKey = confirmClipboard.rawValue + ".state" + static let ConfirmClipboardRequestKey = confirmClipboard.rawValue + ".request" + + /// Notification sent to the active split view to resize the split. + static let didResizeSplit = Notification.Name("com.mitchellh.ghostty.didResizeSplit") + static let ResizeSplitDirectionKey = didResizeSplit.rawValue + ".direction" + static let ResizeSplitAmountKey = didResizeSplit.rawValue + ".amount" + + /// Notification sent to the split root to equalize split sizes + static let didEqualizeSplits = Notification.Name("com.mitchellh.ghostty.didEqualizeSplits") + + /// Notification that renderer health changed + static let didUpdateRendererHealth = Notification.Name("com.mitchellh.ghostty.didUpdateRendererHealth") + + /// Notifications related to key sequences + static let didContinueKeySequence = Notification.Name("com.mitchellh.ghostty.didContinueKeySequence") + static let didEndKeySequence = Notification.Name("com.mitchellh.ghostty.didEndKeySequence") + static let KeySequenceKey = didContinueKeySequence.rawValue + ".key" + + /// Notifications related to key tables + static let didChangeKeyTable = Notification.Name("com.mitchellh.ghostty.didChangeKeyTable") + static let KeyTableKey = didChangeKeyTable.rawValue + ".action" +} + +// Make the input enum hashable. +extension ghostty_input_key_e: @retroactive Hashable {} diff --git a/macos/Sources/Ghostty/GhosttyPackageMeta.swift b/macos/Sources/Ghostty/GhosttyPackageMeta.swift new file mode 100644 index 00000000000..8e035c3234c --- /dev/null +++ b/macos/Sources/Ghostty/GhosttyPackageMeta.swift @@ -0,0 +1,16 @@ +import Foundation +import os + +// This defines the minimal information required so all other files can do +// `extension Ghostty` to add more to it. This purposely has minimal +// dependencies so things like our dock tile plugin can use it. +enum Ghostty { + // The primary logger used by the GhosttyKit libraries. + static let logger = Logger( + subsystem: Bundle.main.bundleIdentifier!, + category: "ghostty" + ) + + // All the notifications that will be emitted will be put here. + struct Notification {} +} diff --git a/macos/Sources/Ghostty/NSEvent+Extension.swift b/macos/Sources/Ghostty/NSEvent+Extension.swift index b67c1932e4c..55888944ecb 100644 --- a/macos/Sources/Ghostty/NSEvent+Extension.swift +++ b/macos/Sources/Ghostty/NSEvent+Extension.swift @@ -39,8 +39,7 @@ extension NSEvent { key_ev.unshifted_codepoint = 0 if type == .keyDown || type == .keyUp { if let chars = characters(byApplyingModifiers: []), - let codepoint = chars.unicodeScalars.first - { + let codepoint = chars.unicodeScalars.first { key_ev.unshifted_codepoint = codepoint.value } } diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift deleted file mode 100644 index 15cb3a51e0c..00000000000 --- a/macos/Sources/Ghostty/Package.swift +++ /dev/null @@ -1,501 +0,0 @@ -import os -import SwiftUI -import GhosttyKit - -struct Ghostty { - // The primary logger used by the GhosttyKit libraries. - static let logger = Logger( - subsystem: Bundle.main.bundleIdentifier!, - category: "ghostty" - ) - - // All the notifications that will be emitted will be put here. - struct Notification {} - - // The user notification category identifier - static let userNotificationCategory = "com.mitchellh.ghostty.userNotification" - - // The user notification "Show" action - static let userNotificationActionShow = "com.mitchellh.ghostty.userNotification.Show" -} - -// MARK: C Extensions - -/// A command is fully self-contained so it is Sendable. -extension ghostty_command_s: @unchecked @retroactive Sendable {} - -/// A surface is sendable because it is just a reference type. Using the surface in parameters -/// may be unsafe but the value itself is safe to send across threads. -extension ghostty_surface_t: @unchecked @retroactive Sendable {} - -// MARK: Build Info - -extension Ghostty { - struct Info { - var mode: ghostty_build_mode_e - var version: String - } - - static var info: Info { - let raw = ghostty_info() - let version = NSString( - bytes: raw.version, - length: Int(raw.version_len), - encoding: NSUTF8StringEncoding - ) ?? "unknown" - - return Info(mode: raw.build_mode, version: String(version)) - } -} - -// MARK: General Helpers - -extension Ghostty { - enum LaunchSource: String { - case cli - case app - case zig_run - } - - /// Returns the mechanism that launched the app. This is based on an env var so - /// its up to the env var being set in the correct circumstance. - static var launchSource: LaunchSource { - guard let envValue = ProcessInfo.processInfo.environment["GHOSTTY_MAC_LAUNCH_SOURCE"] else { - // We default to the CLI because the app bundle always sets the - // source. If its unset we assume we're in a CLI environment. - return .cli - } - - // If the env var is set but its unknown then we default back to the app. - return LaunchSource(rawValue: envValue) ?? .app - } -} - -// MARK: Swift Types for C Types - -extension Ghostty { - class AllocatedString { - private let cString: ghostty_string_s - - init(_ c: ghostty_string_s) { - self.cString = c - } - - var string: String { - guard let ptr = cString.ptr else { return "" } - let data = Data(bytes: ptr, count: Int(cString.len)) - return String(data: data, encoding: .utf8) ?? "" - } - - deinit { - ghostty_string_free(cString) - } - } -} - -extension Ghostty { - enum SetFloatWIndow { - case on - case off - case toggle - - static func from(_ c: ghostty_action_float_window_e) -> Self? { - switch (c) { - case GHOSTTY_FLOAT_WINDOW_ON: - return .on - - case GHOSTTY_FLOAT_WINDOW_OFF: - return .off - - case GHOSTTY_FLOAT_WINDOW_TOGGLE: - return .toggle - - default: - return nil - } - } - } - - enum SetSecureInput { - case on - case off - case toggle - - static func from(_ c: ghostty_action_secure_input_e) -> Self? { - switch (c) { - case GHOSTTY_SECURE_INPUT_ON: - return .on - - case GHOSTTY_SECURE_INPUT_OFF: - return .off - - case GHOSTTY_SECURE_INPUT_TOGGLE: - return .toggle - - default: - return nil - } - } - } - - /// An enum that is used for the directions that a split focus event can change. - enum SplitFocusDirection { - case previous, next, up, down, left, right - - /// Initialize from a Ghostty API enum. - static func from(direction: ghostty_action_goto_split_e) -> Self? { - switch (direction) { - case GHOSTTY_GOTO_SPLIT_PREVIOUS: - return .previous - - case GHOSTTY_GOTO_SPLIT_NEXT: - return .next - - case GHOSTTY_GOTO_SPLIT_UP: - return .up - - case GHOSTTY_GOTO_SPLIT_DOWN: - return .down - - case GHOSTTY_GOTO_SPLIT_LEFT: - return .left - - case GHOSTTY_GOTO_SPLIT_RIGHT: - return .right - - default: - return nil - } - } - - func toNative() -> ghostty_action_goto_split_e { - switch (self) { - case .previous: - return GHOSTTY_GOTO_SPLIT_PREVIOUS - - case .next: - return GHOSTTY_GOTO_SPLIT_NEXT - - case .up: - return GHOSTTY_GOTO_SPLIT_UP - - case .down: - return GHOSTTY_GOTO_SPLIT_DOWN - - case .left: - return GHOSTTY_GOTO_SPLIT_LEFT - - case .right: - return GHOSTTY_GOTO_SPLIT_RIGHT - } - } - } - - /// Enum used for resizing splits. This is the direction the split divider will move. - enum SplitResizeDirection { - case up, down, left, right - - static func from(direction: ghostty_action_resize_split_direction_e) -> Self? { - switch (direction) { - case GHOSTTY_RESIZE_SPLIT_UP: - return .up; - case GHOSTTY_RESIZE_SPLIT_DOWN: - return .down; - case GHOSTTY_RESIZE_SPLIT_LEFT: - return .left; - case GHOSTTY_RESIZE_SPLIT_RIGHT: - return .right; - default: - return nil - } - } - - func toNative() -> ghostty_action_resize_split_direction_e { - switch (self) { - case .up: - return GHOSTTY_RESIZE_SPLIT_UP; - case .down: - return GHOSTTY_RESIZE_SPLIT_DOWN; - case .left: - return GHOSTTY_RESIZE_SPLIT_LEFT; - case .right: - return GHOSTTY_RESIZE_SPLIT_RIGHT; - } - } - } -} - -#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 - case paste - - /// An application is attempting to read from the clipboard using OSC 52 - case osc_52_read - - /// An application is attempting to write to the clipboard using OSC 52 - case osc_52_write(OSPasteboard?) - - /// The text to show in the clipboard confirmation prompt for a given request type - func text() -> String { - switch (self) { - case .paste: - return """ - Pasting this text to the terminal may be dangerous as it looks like some commands may be executed. - """ - case .osc_52_read: - return """ - An application is attempting to read from the clipboard. - The current clipboard contents are shown below. - """ - case .osc_52_write: - return """ - An application is attempting to write to the clipboard. - The content to write is shown below. - """ - } - } - - static func from(request: ghostty_clipboard_request_e) -> ClipboardRequest? { - switch (request) { - case GHOSTTY_CLIPBOARD_REQUEST_PASTE: - return .paste - case GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ: - return .osc_52_read - case GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE: - return .osc_52_write(nil) - default: - return nil - } - } - } - - struct ClipboardContent { - let mime: String - let data: String - - static func from(content: ghostty_clipboard_content_s) -> ClipboardContent? { - guard let mimePtr = content.mime, - let dataPtr = content.data else { - return nil - } - - return ClipboardContent( - mime: String(cString: mimePtr), - data: String(cString: dataPtr) - ) - } - } - - /// macos-icon - enum MacOSIcon: String, Sendable { - case official - case blueprint - case chalkboard - case glass - case holographic - case microchip - case paper - case retro - case xray - case custom - case customStyle = "custom-style" - - /// Bundled asset name for built-in icons - var assetName: String? { - switch self { - case .official: return nil - case .blueprint: return "BlueprintImage" - case .chalkboard: return "ChalkboardImage" - case .microchip: return "MicrochipImage" - case .glass: return "GlassImage" - case .holographic: return "HolographicImage" - case .paper: return "PaperImage" - case .retro: return "RetroImage" - case .xray: return "XrayImage" - case .custom, .customStyle: return nil - } - } - } - - /// macos-icon-frame - enum MacOSIconFrame: String { - case aluminum - case beige - case plastic - case chrome - } - - /// Enum for the macos-window-buttons config option - enum MacOSWindowButtons: String { - case visible - case hidden - } - - /// Enum for the macos-titlebar-proxy-icon config option - enum MacOSTitlebarProxyIcon: String { - case visible - case hidden - } - - /// Enum for auto-update-channel config option - enum AutoUpdateChannel: String { - case tip - case stable - } -} - -// MARK: Surface Notification - -extension Notification.Name { - /// Configuration change. If the object is nil then it is app-wide. Otherwise its surface-specific. - static let ghosttyConfigDidChange = Notification.Name("com.mitchellh.ghostty.configDidChange") - static let GhosttyConfigChangeKey = ghosttyConfigDidChange.rawValue - - /// Color change. Object is the surface changing. - static let ghosttyColorDidChange = Notification.Name("com.mitchellh.ghostty.ghosttyColorDidChange") - static let GhosttyColorChangeKey = ghosttyColorDidChange.rawValue - - /// Goto tab. Has tab index in the userinfo. - static let ghosttyMoveTab = Notification.Name("com.mitchellh.ghostty.moveTab") - static let GhosttyMoveTabKey = ghosttyMoveTab.rawValue - - /// Close tab - static let ghosttyCloseTab = Notification.Name("com.mitchellh.ghostty.closeTab") - - /// Close other tabs - static let ghosttyCloseOtherTabs = Notification.Name("com.mitchellh.ghostty.closeOtherTabs") - - /// Close tabs to the right of the focused tab - static let ghosttyCloseTabsOnTheRight = Notification.Name("com.mitchellh.ghostty.closeTabsOnTheRight") - - /// Close window - static let ghosttyCloseWindow = Notification.Name("com.mitchellh.ghostty.closeWindow") - - /// Resize the window to a default size. - static let ghosttyResetWindowSize = Notification.Name("com.mitchellh.ghostty.resetWindowSize") - - /// Ring the bell - static let ghosttyBellDidRing = Notification.Name("com.mitchellh.ghostty.ghosttyBellDidRing") - - /// Readonly mode changed - static let ghosttyDidChangeReadonly = Notification.Name("com.mitchellh.ghostty.didChangeReadonly") - static let ReadonlyKey = ghosttyDidChangeReadonly.rawValue + ".readonly" - static let ghosttyCommandPaletteDidToggle = Notification.Name("com.mitchellh.ghostty.commandPaletteDidToggle") - - /// Toggle maximize of current window - 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" - - /// Focus the search field - static let ghosttySearchFocus = Notification.Name("com.mitchellh.ghostty.searchFocus") -} - -// NOTE: I am moving all of these to Notification.Name extensions over time. This -// namespace was the old namespace. -extension Ghostty.Notification { - /// Used to pass a configuration along when creating a new tab/window/split. - static let NewSurfaceConfigKey = "com.mitchellh.ghostty.newSurfaceConfig" - - /// Posted when a new split is requested. The sending object will be the surface that had focus. The - /// userdata has one key "direction" with the direction to split to. - static let ghosttyNewSplit = Notification.Name("com.mitchellh.ghostty.newSplit") - - /// Close the calling surface. - static let ghosttyCloseSurface = Notification.Name("com.mitchellh.ghostty.closeSurface") - - /// Focus previous/next split. Has a SplitFocusDirection in the userinfo. - static let ghosttyFocusSplit = Notification.Name("com.mitchellh.ghostty.focusSplit") - static let SplitDirectionKey = ghosttyFocusSplit.rawValue - - /// Goto tab. Has tab index in the userinfo. - static let ghosttyGotoTab = Notification.Name("com.mitchellh.ghostty.gotoTab") - static let GotoTabKey = ghosttyGotoTab.rawValue - - /// New tab. Has base surface config requested in userinfo. - static let ghosttyNewTab = Notification.Name("com.mitchellh.ghostty.newTab") - - /// New window. Has base surface config requested in userinfo. - static let ghosttyNewWindow = Notification.Name("com.mitchellh.ghostty.newWindow") - - /// Present terminal. Bring the surface's window to focus without activating the app. - static let ghosttyPresentTerminal = Notification.Name("com.mitchellh.ghostty.presentTerminal") - - /// Toggle fullscreen of current window - static let ghosttyToggleFullscreen = Notification.Name("com.mitchellh.ghostty.toggleFullscreen") - static let FullscreenModeKey = ghosttyToggleFullscreen.rawValue - - /// Notification sent to toggle split maximize/unmaximize. - static let didToggleSplitZoom = Notification.Name("com.mitchellh.ghostty.didToggleSplitZoom") - - /// Notification - static let didReceiveInitialWindowFrame = Notification.Name("com.mitchellh.ghostty.didReceiveInitialWindowFrame") - static let FrameKey = "com.mitchellh.ghostty.frame" - - /// Notification to render the inspector for a surface - static let inspectorNeedsDisplay = Notification.Name("com.mitchellh.ghostty.inspectorNeedsDisplay") - - /// Notification to show/hide the inspector - static let didControlInspector = Notification.Name("com.mitchellh.ghostty.didControlInspector") - - static let confirmClipboard = Notification.Name("com.mitchellh.ghostty.confirmClipboard") - static let ConfirmClipboardStrKey = confirmClipboard.rawValue + ".str" - static let ConfirmClipboardStateKey = confirmClipboard.rawValue + ".state" - static let ConfirmClipboardRequestKey = confirmClipboard.rawValue + ".request" - - /// Notification sent to the active split view to resize the split. - static let didResizeSplit = Notification.Name("com.mitchellh.ghostty.didResizeSplit") - static let ResizeSplitDirectionKey = didResizeSplit.rawValue + ".direction" - static let ResizeSplitAmountKey = didResizeSplit.rawValue + ".amount" - - /// Notification sent to the split root to equalize split sizes - static let didEqualizeSplits = Notification.Name("com.mitchellh.ghostty.didEqualizeSplits") - - /// Notification that renderer health changed - static let didUpdateRendererHealth = Notification.Name("com.mitchellh.ghostty.didUpdateRendererHealth") - - /// Notifications related to key sequences - static let didContinueKeySequence = Notification.Name("com.mitchellh.ghostty.didContinueKeySequence") - static let didEndKeySequence = Notification.Name("com.mitchellh.ghostty.didEndKeySequence") - static let KeySequenceKey = didContinueKeySequence.rawValue + ".key" - - /// Notifications related to key tables - static let didChangeKeyTable = Notification.Name("com.mitchellh.ghostty.didChangeKeyTable") - static let KeyTableKey = didChangeKeyTable.rawValue + ".action" -} - -// Make the input enum hashable. -extension ghostty_input_key_e : @retroactive Hashable {} diff --git a/macos/Sources/Ghostty/Surface View/InspectorView.swift b/macos/Sources/Ghostty/Surface View/InspectorView.swift index 03be794e94f..e7320c782c3 100644 --- a/macos/Sources/Ghostty/Surface View/InspectorView.swift +++ b/macos/Sources/Ghostty/Surface View/InspectorView.swift @@ -23,7 +23,7 @@ extension Ghostty { let pubInspector = center.publisher(for: Notification.didControlInspector, object: surfaceView) ZStack { - if (!surfaceView.inspectorVisible) { + if !surfaceView.inspectorVisible { SurfaceWrapper(surfaceView: surfaceView, isSplit: isSplit) } else { SplitView(.vertical, $split, dividerColor: ghostty.config.splitDividerColor, left: { @@ -42,7 +42,7 @@ extension Ghostty { .onChange(of: surfaceView.inspectorVisible) { inspectorVisible in // When we show the inspector, we want to focus on the inspector. // When we hide the inspector, we want to move focus back to the surface. - if (inspectorVisible) { + if inspectorVisible { // We need to delay this until SwiftUI shows the inspector. DispatchQueue.main.async { _ = surfaceView.resignFirstResponder() @@ -59,7 +59,7 @@ extension Ghostty { guard let modeAny = notification.userInfo?["mode"] else { return } guard let mode = modeAny as? ghostty_action_inspector_e else { return } - switch (mode) { + switch mode { case GHOSTTY_INSPECTOR_TOGGLE: surfaceView.inspectorVisible = !surfaceView.inspectorVisible @@ -94,7 +94,7 @@ extension Ghostty { class InspectorView: MTKView, NSTextInputClient { let commandQueue: MTLCommandQueue - var surfaceView: SurfaceView? = nil { + var surfaceView: SurfaceView? { didSet { surfaceViewDidChange() } } @@ -180,7 +180,7 @@ extension Ghostty { override func becomeFirstResponder() -> Bool { let result = super.becomeFirstResponder() - if (result) { + if result { if let inspector = self.inspector { inspector.setFocus(true) } @@ -190,7 +190,7 @@ extension Ghostty { override func resignFirstResponder() -> Bool { let result = super.resignFirstResponder() - if (result) { + if result { if let inspector = self.inspector { inspector.setFocus(false) } @@ -275,7 +275,7 @@ extension Ghostty { // Determine our momentum value var momentum: ghostty_input_mouse_momentum_e = GHOSTTY_MOUSE_MOMENTUM_NONE - switch (event.momentumPhase) { + switch event.momentumPhase { case .began: momentum = GHOSTTY_MOUSE_MOMENTUM_BEGAN case .stationary: @@ -309,8 +309,8 @@ extension Ghostty { } override func flagsChanged(with event: NSEvent) { - let mod: UInt32; - switch (event.keyCode) { + let mod: UInt32 + switch event.keyCode { case 0x39: mod = GHOSTTY_MODS_CAPS.rawValue case 0x38, 0x3C: mod = GHOSTTY_MODS_SHIFT.rawValue case 0x3B, 0x3E: mod = GHOSTTY_MODS_CTRL.rawValue @@ -325,7 +325,7 @@ extension Ghostty { // If the key that pressed this is active, its a press, else release var action = GHOSTTY_ACTION_RELEASE - if (mods.rawValue & mod != 0) { action = GHOSTTY_ACTION_PRESS } + if mods.rawValue & mod != 0 { action = GHOSTTY_ACTION_PRESS } keyAction(action, event: event) } @@ -382,7 +382,7 @@ extension Ghostty { } func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect { - return NSMakeRect(frame.origin.x, frame.origin.y, 0, 0) + return NSRect(x: frame.origin.x, y: frame.origin.y, width: 0, height: 0) } func insertText(_ string: Any, replacementRange: NSRange) { @@ -392,7 +392,7 @@ extension Ghostty { // We want the string view of the any value var chars = "" - switch (string) { + switch string { case let v as NSAttributedString: chars = v.string case let v as String: @@ -402,7 +402,7 @@ extension Ghostty { } let len = chars.utf8CString.count - if (len == 0) { return } + if len == 0 { return } inspector.text(chars) } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift index 37a69852ee4..dd2f3ef5e7a 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift @@ -5,13 +5,13 @@ extension Ghostty { /// A preference key that propagates the ID of the SurfaceView currently being dragged, /// or nil if no surface is being dragged. struct DraggingSurfaceKey: PreferenceKey { - static var defaultValue: SurfaceView.ID? = nil - + static var defaultValue: SurfaceView.ID? + static func reduce(value: inout SurfaceView.ID?, nextValue: () -> SurfaceView.ID?) { value = nextValue() ?? value } } - + /// A SwiftUI view that provides drag source functionality for terminal surfaces. /// /// This view wraps an AppKit-based drag source to enable drag-and-drop reordering @@ -24,13 +24,13 @@ extension Ghostty { struct SurfaceDragSource: View { /// The surface view that will be dragged. let surfaceView: SurfaceView - + /// Binding that reflects whether a drag session is currently active. @Binding var isDragging: Bool - + /// Binding that reflects whether the mouse is hovering over this view. @Binding var isHovering: Bool - + var body: some View { SurfaceDragSourceViewRepresentable( surfaceView: surfaceView, @@ -46,7 +46,7 @@ extension Ghostty { let surfaceView: SurfaceView @Binding var isDragging: Bool @Binding var isHovering: Bool - + func makeNSView(context: Context) -> SurfaceDragSourceView { let view = SurfaceDragSourceView() view.surfaceView = surfaceView @@ -60,7 +60,7 @@ extension Ghostty { } return view } - + func updateNSView(_ nsView: SurfaceDragSourceView, context: Context) { nsView.surfaceView = surfaceView nsView.onDragStateChanged = { dragging in @@ -73,7 +73,7 @@ extension Ghostty { } } } - + /// The underlying NSView that handles drag operations. /// /// This view manages mouse tracking and drag initiation for surface reordering. @@ -82,26 +82,26 @@ extension Ghostty { fileprivate class SurfaceDragSourceView: NSView, NSDraggingSource { /// Scale factor applied to the surface snapshot for the drag preview image. private static let previewScale: CGFloat = 0.2 - + /// The surface view that will be dragged. Its UUID is encoded into the /// pasteboard for drop targets to identify which surface is being moved. var surfaceView: SurfaceView? - + /// Callback invoked when the drag state changes. Called with `true` when /// a drag session begins, and `false` when it ends (completed or cancelled). var onDragStateChanged: ((Bool) -> Void)? - + /// Callback invoked when the mouse enters or exits this view's bounds. /// Used to update the hover state for visual feedback in the parent view. var onHoverChanged: ((Bool) -> Void)? - + /// Whether we are currently in a mouse tracking loop (between mouseDown /// and either mouseUp or drag initiation). Used to determine cursor state. private var isTracking: Bool = false - + /// Local event monitor to detect escape key presses during drag. private var escapeMonitor: Any? - + /// Whether the current drag was cancelled by pressing escape. private var dragCancelledByEscape: Bool = false @@ -137,26 +137,26 @@ extension Ghostty { userInfo: nil )) } - + override func resetCursorRects() { addCursorRect(bounds, cursor: isTracking ? .closedHand : .openHand) } - + override func mouseEntered(with event: NSEvent) { onHoverChanged?(true) } - + override func mouseExited(with event: NSEvent) { onHoverChanged?(false) } - + override func mouseDragged(with event: NSEvent) { guard !isTracking, let surfaceView = surfaceView else { return } - + // Create our dragging item from our transferable guard let pasteboardItem = surfaceView.pasteboardItem() else { return } let item = NSDraggingItem(pasteboardWriter: pasteboardItem) - + // Create a scaled preview image from the surface snapshot if let snapshot = surfaceView.asImage { let imageSize = NSSize( @@ -172,7 +172,7 @@ extension Ghostty { fraction: 1.0 ) scaledImage.unlockFocus() - + // Position the drag image so the mouse is at the center of the image. // I personally like the top middle or top left corner best but // this matches macOS native tab dragging behavior (at least, as of @@ -187,30 +187,30 @@ extension Ghostty { contents: scaledImage ) } - + onDragStateChanged?(true) let session = beginDraggingSession(with: [item], event: event, source: self) - + // We need to disable this so that endedAt happens immediately for our // drags outside of any targets. session.animatesToStartingPositionsOnCancelOrFail = false } - + // MARK: NSDraggingSource - + func draggingSession( _ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext ) -> NSDragOperation { return context == .withinApplication ? .move : [] } - + func draggingSession( _ session: NSDraggingSession, willBeginAt screenPoint: NSPoint ) { isTracking = true - + // Reset our escape tracking dragCancelledByEscape = false escapeMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in @@ -220,14 +220,14 @@ extension Ghostty { return event } } - + func draggingSession( _ session: NSDraggingSession, movedTo screenPoint: NSPoint ) { NSCursor.closedHand.set() } - + func draggingSession( _ session: NSDraggingSession, endedAt screenPoint: NSPoint, @@ -262,7 +262,7 @@ extension Notification.Name { /// released outside a valid drop target) and was not cancelled by the user /// pressing escape. The notification's object is the SurfaceView that was dragged. static let ghosttySurfaceDragEndedNoTarget = Notification.Name("ghosttySurfaceDragEndedNoTarget") - + /// Key for the screen point where the drag ended in the userInfo dictionary. static let ghosttySurfaceDragEndedNoTargetPointKey = "endedAtPoint" } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift index f3ee80874fd..c5ab84124b1 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift @@ -1,41 +1,81 @@ -import AppKit import SwiftUI -extension Ghostty { - /// A grab handle overlay at the top of the surface for dragging the window. - /// Only appears when hovering in the top region of the surface. +extension Ghostty { + /// A grab handle overlay at the top of the surface for dragging a surface. struct SurfaceGrabHandle: View { - private let handleHeight: CGFloat = 10 - - let surfaceView: SurfaceView - + // Size of the actual drag handle; the hover reveal region is larger. + private static let handleSize = CGSize(width: 80, height: 12) + + // Reveal the handle anywhere within the top % of the pane height. + private static let hoverHeightFactor: CGFloat = 0.2 + + @ObservedObject var surfaceView: SurfaceView + @State private var isHovering: Bool = false @State private var isDragging: Bool = false - + + private var handleVisible: Bool { + // Handle should always be visible in non-fullscreen + guard let window = surfaceView.window else { return true } + guard window.styleMask.contains(.fullScreen) else { return true } + + // If fullscreen, only show the handle if we have splits + guard let controller = window.windowController as? BaseTerminalController else { return false } + return controller.surfaceTree.isSplit + } + + private var ellipsisVisible: Bool { + // If the cursor isn't visible, never show the handle + guard surfaceView.cursorVisible else { return false } + // If we're hovering or actively dragging, always visible + if isHovering || isDragging { return true } + + // Require our mouse location to be within the top area of the + // surface. + guard let mouseLocation = surfaceView.mouseLocationInSurface else { return false } + return Self.isInHoverRegion(mouseLocation, in: surfaceView.bounds) + } + var body: some View { - VStack(spacing: 0) { - Rectangle() - .fill(Color.primary.opacity(isHovering || isDragging ? 0.15 : 0)) - .frame(height: handleHeight) - .overlay(alignment: .center) { - if isHovering || isDragging { - Image(systemName: "ellipsis") - .font(.system(size: 14, weight: .semibold)) - .foregroundColor(.primary.opacity(0.5)) - } - } + if handleVisible { + ZStack { + SurfaceDragSource( + surfaceView: surfaceView, + isDragging: $isDragging, + isHovering: $isHovering + ) + .frame(width: Self.handleSize.width, height: Self.handleSize.height) .contentShape(Rectangle()) - .overlay { - SurfaceDragSource( - surfaceView: surfaceView, - isDragging: $isDragging, - isHovering: $isHovering - ) + + if ellipsisVisible { + Image(systemName: "ellipsis") + .font(.system(size: 10, weight: .semibold)) + .foregroundColor(.primary.opacity(isHovering ? 0.8 : 0.3)) + .offset(y: -3) + .allowsHitTesting(false) + .transition(.opacity) } - - Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } - .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + /// The full-width hover band that reveals the drag handle. + private static func hoverRect(in bounds: CGRect) -> CGRect { + guard !bounds.isEmpty else { return .zero } + + let hoverHeight = min(bounds.height, max(handleSize.height, bounds.height * hoverHeightFactor)) + return CGRect( + x: bounds.minX, + y: bounds.maxY - hoverHeight, + width: bounds.width, + height: hoverHeight + ) + } + + /// Returns true when the pointer is inside the top hover band. + private static func isInHoverRegion(_ point: CGPoint, in bounds: CGRect) -> Bool { + hoverRect(in: bounds).contains(point) } } } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift b/macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift index 82d26e68149..0478bf2bf4a 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift @@ -5,7 +5,7 @@ import SwiftUI /// control. struct SurfaceProgressBar: View { let report: Ghostty.Action.ProgressReport - + private var color: Color { switch report.state { case .error: return .red @@ -13,17 +13,17 @@ struct SurfaceProgressBar: View { default: return .accentColor } } - + private var progress: UInt8? { // If we have an explicit progress use that. if let v = report.progress { return v } - + // Otherwise, if we're in the pause state, we act as if we're at 100%. if report.state == .pause { return 100 } - + return nil } - + private var accessibilityLabel: String { switch report.state { case .error: return "Terminal progress - Error" @@ -32,7 +32,7 @@ struct SurfaceProgressBar: View { default: return "Terminal progress" } } - + private var accessibilityValue: String { if let progress { return "\(progress) percent complete" @@ -45,7 +45,7 @@ struct SurfaceProgressBar: View { } } } - + var body: some View { GeometryReader { geometry in ZStack(alignment: .leading) { @@ -78,15 +78,15 @@ struct SurfaceProgressBar: View { private struct BouncingProgressBar: View { let color: Color @State private var position: CGFloat = 0 - + private let barWidthRatio: CGFloat = 0.25 - + var body: some View { GeometryReader { geometry in ZStack(alignment: .leading) { Rectangle() .fill(color.opacity(0.3)) - + Rectangle() .fill(color) .frame( @@ -110,4 +110,3 @@ private struct BouncingProgressBar: View { } } - diff --git a/macos/Sources/Ghostty/Surface View/SurfaceScrollView.swift b/macos/Sources/Ghostty/Surface View/SurfaceScrollView.swift index b55f2e2312e..aab99c088ec 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceScrollView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceScrollView.swift @@ -19,12 +19,12 @@ class SurfaceScrollView: NSView { 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 @@ -44,26 +44,26 @@ class SurfaceScrollView: NSView { // (we currently only use overlay scrollers, but might as well // configure the views correctly in case we change our mind) scrollView.contentView.clipsToBounds = false - + // The document view is what the scrollview is actually going // 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 @@ -74,7 +74,7 @@ class SurfaceScrollView: NSView { ) { [weak self] notification in self?.handleScrollChange(notification) }) - + // Listen for scrollbar updates from Ghostty observers.append(NotificationCenter.default.addObserver( forName: .ghosttyDidUpdateScrollbar, @@ -83,7 +83,7 @@ class SurfaceScrollView: NSView { ) { [weak self] notification in self?.handleScrollbarUpdate(notification) }) - + // Listen for live scroll events observers.append(NotificationCenter.default.addObserver( forName: NSScrollView.willStartLiveScrollNotification, @@ -92,7 +92,7 @@ class SurfaceScrollView: NSView { ) { [weak self] _ in self?.isLiveScrolling = true }) - + observers.append(NotificationCenter.default.addObserver( forName: NSScrollView.didEndLiveScrollNotification, object: scrollView, @@ -100,7 +100,7 @@ class SurfaceScrollView: NSView { ) { [weak self] _ in self?.isLiveScrolling = false }) - + observers.append(NotificationCenter.default.addObserver( forName: NSScrollView.didLiveScrollNotification, object: scrollView, @@ -108,7 +108,7 @@ class SurfaceScrollView: NSView { ) { [weak self] _ in self?.handleLiveScroll() }) - + observers.append(NotificationCenter.default.addObserver( forName: NSScroller.preferredScrollerStyleDidChangeNotification, object: nil, @@ -150,11 +150,11 @@ class SurfaceScrollView: NSView { } .store(in: &cancellables) } - + required init?(coder: NSCoder) { fatalError("init(coder:) not implemented") } - + deinit { observers.forEach { NotificationCenter.default.removeObserver($0) } } @@ -163,10 +163,10 @@ class SurfaceScrollView: NSView { // 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 layout() { super.layout() - + // Fill entire bounds with scroll view scrollView.frame = bounds surfaceView.frame.size = scrollView.bounds.size @@ -174,13 +174,13 @@ class SurfaceScrollView: NSView { // We only set the width of the documentView here, as the height depends // on the scrollbar state and is updated in synchronizeScrollView documentView.frame.size.width = scrollView.bounds.width - + // When our scrollview changes make sure our scroller and surface views are synchronized synchronizeScrollView() synchronizeSurfaceView() synchronizeCoreSurface() } - + // MARK: Scrolling private func synchronizeAppearance() { @@ -220,7 +220,7 @@ class SurfaceScrollView: NSView { private func synchronizeScrollView() { // Update the document height to give our scroller the correct proportions documentView.frame.size.height = documentHeight() - + // Only update our actual scroll position if we're not actively scrolling. if !isLiveScrolling { // Convert row units to pixels using cell height, ignore zero height. @@ -236,13 +236,13 @@ class SurfaceScrollView: NSView { lastSentRow = Int(scrollbar.offset) } } - + // Always update our scrolled view with the latest dimensions scrollView.reflectScrolledClipView(scrollView.contentView) } - + // MARK: Notifications - + /// Handles bounds changes in the scroll view's clip view, keeping the surface view synchronized. private func handleScrollChange(_ notification: Notification) { synchronizeSurfaceView() @@ -259,7 +259,7 @@ class SurfaceScrollView: NSView { synchronizeAppearance() synchronizeCoreSurface() } - + /// 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 @@ -270,21 +270,21 @@ class SurfaceScrollView: NSView { // 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 diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift index 509713309e4..10687581324 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift @@ -17,11 +17,11 @@ extension Ghostty.SurfaceView: Transferable { let uuid = data.withUnsafeBytes { $0.load(as: UUID.self) } - + guard let imported = await Self.find(uuid: uuid) else { throw TransferError.invalidData } - + return imported } } @@ -29,7 +29,7 @@ extension Ghostty.SurfaceView: Transferable { enum TransferError: Error { case invalidData } - + @MainActor static func find(uuid: UUID) -> Self? { #if canImport(AppKit) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index c5c2ee97c6c..47503dc0e80 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -49,13 +49,18 @@ extension Ghostty { // True if we're hovering over the left URL view, so we can show it on the right. @State private var isHoveringURLLeft: Bool = false - + #if canImport(AppKit) // Observe SecureInput to detect when its enabled @ObservedObject private var secureInput = SecureInput.shared #endif @EnvironmentObject private var ghostty: Ghostty.App + @Environment(\.ghosttyLastFocusedSurface) private var lastFocusedSurface + + private var isFocusedSurface: Bool { + surfaceFocus || lastFocusedSurface?.value === surfaceView + } var body: some View { let center = NotificationCenter.default @@ -84,7 +89,7 @@ extension Ghostty { .onReceive(pubResign) { notification in guard let window = notification.object as? NSWindow else { return } guard let surfaceWindow = surfaceView.window else { return } - if (surfaceWindow == window) { + if surfaceWindow == window { windowFocus = false } } @@ -103,7 +108,7 @@ extension Ghostty { } } .ghosttySurfaceView(surfaceView) - + // Progress report if let progressReport = surfaceView.progressReport, progressReport.state != .remove { VStack(spacing: 0) { @@ -114,7 +119,7 @@ extension Ghostty { .allowsHitTesting(false) .transition(.opacity) } - + #if canImport(AppKit) // Readonly indicator badge if surfaceView.readonly { @@ -122,7 +127,7 @@ extension Ghostty { surfaceView.toggleReadonly(nil) } } - + // Show key state indicator for active key tables and/or pending key sequences KeyStateIndicator( keyTables: surfaceView.keyTables, @@ -177,10 +182,10 @@ extension Ghostty { #if canImport(AppKit) // If we have secure input enabled and we're the focused surface and window // then we want to show the secure input overlay. - if (ghostty.config.secureInputIndication && + if ghostty.config.secureInputIndication && secureInput.enabled && surfaceFocus && - windowFocus) { + windowFocus { SecureInputOverlay() } #endif @@ -200,7 +205,7 @@ extension Ghostty { } // Show bell border if enabled - if (ghostty.config.bellFeatures.contains(.border)) { + if ghostty.config.bellFeatures.contains(.border) { BellBorderOverlay(bell: surfaceView.bell) } @@ -208,21 +213,20 @@ extension Ghostty { HighlightOverlay(highlighted: surfaceView.highlighted) // If our surface is not healthy, then we render an error view over it. - if (!surfaceView.healthy) { + if !surfaceView.healthy { Rectangle().fill(ghostty.config.backgroundColor) SurfaceRendererUnhealthyView() - } else if (surfaceView.error != nil) { + } else if surfaceView.error != nil { Rectangle().fill(ghostty.config.backgroundColor) SurfaceErrorView() } // If we're part of a split view and don't have focus, we put a semi-transparent - // rectangle above our view to make it look unfocused. We use "surfaceFocus" - // because we want to keep our focused surface dark even if we don't have window - // focus. - if (isSplit && !surfaceFocus) { - let overlayOpacity = ghostty.config.unfocusedSplitOpacity; - if (overlayOpacity > 0) { + // rectangle above our view to make it look unfocused. We include the last + // focused surface so this still works while SwiftUI focus is temporarily nil. + if isSplit && !isFocusedSurface { + let overlayOpacity = ghostty.config.unfocusedSplitOpacity + if overlayOpacity > 0 { Rectangle() .fill(ghostty.config.unfocusedSplitFill) .allowsHitTesting(false) @@ -286,8 +290,6 @@ extension Ghostty { } } - - // This is the resize overlay that shows on top of a surface to show the current // size during a resize operation. struct SurfaceResizeOverlay: View { @@ -300,7 +302,7 @@ extension Ghostty { // This is the last size that we processed. This is how we handle our // timer state. - @State var lastSize: CGSize? = nil + @State var lastSize: CGSize? // Ready is set to true after a short delay. This avoids some of the // challenges of initial view sizing from SwiftUI. @@ -312,42 +314,42 @@ extension Ghostty { // This computed boolean is set to true when the overlay should be hidden. private var hidden: Bool { // If we aren't ready yet then we wait... - if (!ready) { return true; } + if !ready { return true; } // Hidden if we already processed this size. - if (lastSize == geoSize) { return true; } + if lastSize == geoSize { return true; } // If we were focused recently we hide it as well. This avoids showing // the resize overlay when SwiftUI is lazily resizing. if let instant = focusInstant { let d = instant.duration(to: ContinuousClock.now) - if (d < .milliseconds(500)) { + if d < .milliseconds(500) { // Avoid this size completely. We can't set values during // view updates so we have to defer this to another tick. DispatchQueue.main.async { lastSize = geoSize } - return true; + return true } } // Hidden depending on overlay config - switch (overlay) { - case .never: return true; - case .always: return false; - case .after_first: return lastSize == nil; + switch overlay { + case .never: return true + case .always: return false + case .after_first: return lastSize == nil } } var body: some View { VStack { - if (!position.top()) { + if !position.top() { Spacer() } HStack { - if (!position.left()) { + if !position.left() { Spacer() } @@ -361,12 +363,12 @@ extension Ghostty { .lineLimit(1) .truncationMode(.tail) - if (!position.right()) { + if !position.right() { Spacer() } } - if (!position.bottom()) { + if !position.bottom() { Spacer() } } @@ -386,7 +388,7 @@ extension Ghostty { // We only sleep if we're ready. If we're not ready then we want to set // our last size right away to avoid a flash. - if (ready) { + if ready { try? await Task.sleep(nanoseconds: UInt64(duration) * 1_000_000) } @@ -404,9 +406,9 @@ extension Ghostty { @State private var dragOffset: CGSize = .zero @State private var barSize: CGSize = .zero @FocusState private var isSearchFieldFocused: Bool - + private let padding: CGFloat = 8 - + var body: some View { GeometryReader { geo in HStack(spacing: 4) { @@ -456,20 +458,20 @@ extension Ghostty { guard let surface = surfaceView.surface else { return } let action = "navigate_search:next" ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) - }) { + }, label: { Image(systemName: "chevron.up") - } + }) .buttonStyle(SearchButtonStyle()) - + Button(action: { guard let surface = surfaceView.surface else { return } let action = "navigate_search:previous" ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) - }) { + }, label: { Image(systemName: "chevron.down") - } + }) .buttonStyle(SearchButtonStyle()) - + Button(action: onClose) { Image(systemName: "xmark") } @@ -529,7 +531,7 @@ extension Ghostty { enum Corner { case topLeft, topRight, bottomLeft, bottomRight - + var alignment: Alignment { switch self { case .topLeft: return .topLeading @@ -539,11 +541,11 @@ extension Ghostty { } } } - + private func centerPosition(for corner: Corner, in containerSize: CGSize, barSize: CGSize) -> CGPoint { let halfWidth = barSize.width / 2 + padding let halfHeight = barSize.height / 2 + padding - + switch corner { case .topLeft: return CGPoint(x: halfWidth, y: halfHeight) @@ -555,21 +557,21 @@ extension Ghostty { return CGPoint(x: containerSize.width - halfWidth, y: containerSize.height - halfHeight) } } - + private func closestCorner(to point: CGPoint, in containerSize: CGSize) -> Corner { let midX = containerSize.width / 2 let midY = containerSize.height / 2 - + if point.x < midX { return point.y < midY ? .topLeft : .bottomLeft } else { return point.y < midY ? .topRight : .bottomRight } } - + struct SearchButtonStyle: ButtonStyle { @State private var isHovered = false - + func makeBody(configuration: Configuration) -> some View { configuration.label .foregroundStyle(isHovered || configuration.isPressed ? .primary : .secondary) @@ -584,7 +586,7 @@ extension Ghostty { } .backport.pointerStyle(.link) } - + private func backgroundColor(isPressed: Bool) -> Color { if isPressed { return Color.primary.opacity(0.2) @@ -640,20 +642,20 @@ extension Ghostty { /// libghostty, usually from the Ghostty configuration. struct SurfaceConfiguration { /// Explicit font size to use in points - var fontSize: Float32? = nil + var fontSize: Float32? /// Explicit working directory to set - var workingDirectory: String? = nil + var workingDirectory: String? /// Explicit command to set - var command: String? = nil - + var command: String? + /// Environment variables to set for the terminal var environmentVariables: [String: String] = [:] /// Extra input to send as stdin - var initialInput: String? = nil - + var initialInput: String? + /// Wait after the command var waitAfterCommand: Bool = false @@ -711,7 +713,7 @@ extension Ghostty { // Zero is our default value that means to inherit the font size. config.font_size = fontSize ?? 0 - + // Set wait after command config.wait_after_command = waitAfterCommand @@ -736,7 +738,7 @@ extension Ghostty { return try keys.withCStrings { keyCStrings in return try values.withCStrings { valueCStrings in // Create array of ghostty_env_var_s - var envVars = Array() + var envVars = [ghostty_env_var_s]() envVars.reserveCapacity(environmentVariables.count) for i in 0.. Double { let phase = animationPhase let offset = Double(index) / 3.0 @@ -981,7 +983,7 @@ extension Ghostty { /// Visual overlay that shows a border around the edges when the bell rings with border feature enabled. struct BellBorderOverlay: View { let bell: Bool - + var body: some View { Rectangle() .strokeBorder( @@ -998,7 +1000,7 @@ extension Ghostty { /// Uses a soft, soothing highlight with a pulsing border effect. struct HighlightOverlay: View { let highlighted: Bool - + @State private var borderPulse: Bool = false var body: some View { @@ -1051,21 +1053,21 @@ extension Ghostty { } // MARK: Readonly Badge - + /// A badge overlay that indicates a surface is in readonly mode. /// Positioned in the top-right corner and styled to be noticeable but unobtrusive. struct ReadonlyBadge: View { let onDisable: () -> Void - + @State private var showingPopover = false - + private let badgeColor = Color(hue: 0.08, saturation: 0.5, brightness: 0.8) - + var body: some View { VStack { HStack { Spacer() - + HStack(spacing: 5) { Image(systemName: "eye.fill") .font(.system(size: 12)) @@ -1085,13 +1087,13 @@ extension Ghostty { } } .padding(8) - + Spacer() } .accessibilityElement(children: .ignore) .accessibilityLabel("Read-only terminal") } - + private var badgeBackground: some View { RoundedRectangle(cornerRadius: 6) .fill(.regularMaterial) @@ -1101,11 +1103,11 @@ extension Ghostty { ) } } - + struct ReadonlyPopoverView: View { let onDisable: () -> Void @Binding var isPresented: Bool - + var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) { @@ -1116,16 +1118,16 @@ extension Ghostty { Text("Read-Only Mode") .font(.system(size: 13, weight: .semibold)) } - + Text("This terminal is in read-only mode. You can still view, select, and scroll through the content, but no input events will be sent to the running application.") .font(.system(size: 11)) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) } - + HStack { Spacer() - + Button("Disable") { onDisable() isPresented = false @@ -1203,17 +1205,33 @@ private struct GhosttySurfaceViewKey: EnvironmentKey { static let defaultValue: Ghostty.SurfaceView? = nil } +private struct GhosttyLastFocusedSurfaceKey: EnvironmentKey { + /// Optional read-only last-focused surface reference. If a surface view is currently focused this + /// is equal to the currently focused surface. + static let defaultValue: Weak? = nil +} + extension EnvironmentValues { var ghosttySurfaceView: Ghostty.SurfaceView? { get { self[GhosttySurfaceViewKey.self] } set { self[GhosttySurfaceViewKey.self] = newValue } } + + var ghosttyLastFocusedSurface: Weak? { + get { self[GhosttyLastFocusedSurfaceKey.self] } + set { self[GhosttyLastFocusedSurfaceKey.self] = newValue } + } } extension View { func ghosttySurfaceView(_ surfaceView: Ghostty.SurfaceView?) -> some View { environment(\.ghosttySurfaceView, surfaceView) } + + /// The most recently focused surface (can be currently focused if the surface is currently focused). + func ghosttyLastFocusedSurface(_ surfaceView: Weak?) -> some View { + environment(\.ghosttyLastFocusedSurface, surfaceView) + } } // MARK: Surface Focus Keys @@ -1252,8 +1270,8 @@ extension FocusedValues { extension Ghostty.SurfaceView { class SearchState: ObservableObject { @Published var needle: String = "" - @Published var selected: UInt? = nil - @Published var total: UInt? = nil + @Published var selected: UInt? + @Published var total: UInt? init(from startSearch: Ghostty.Action.StartSearch) { self.needle = startSearch.needle ?? "" diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index c856b0163c5..3129acfba4a 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -27,7 +27,7 @@ extension Ghostty { // The current pwd of the surface as defined by the pty. This can be // changed with escape codes. - @Published var pwd: String? = nil + @Published var pwd: String? // The cell size of this surface. This is set by the core when the // surface is first created and any time the cell size changes (i.e. @@ -40,13 +40,13 @@ extension Ghostty { @Published var healthy: Bool = true // Any error while initializing the surface. - @Published var error: Error? = nil + @Published var error: Error? // The hovered URL string - @Published var hoverUrl: String? = nil + @Published var hoverUrl: String? // The progress report (if any) - @Published var progressReport: Action.ProgressReport? = nil { + @Published var progressReport: Action.ProgressReport? { didSet { // Cancel any existing timer progressReportTimer?.invalidate() @@ -69,7 +69,7 @@ extension Ghostty { @Published var keyTables: [String] = [] // The current search state. When non-nil, the search overlay should be shown. - @Published var searchState: SearchState? = nil { + @Published var searchState: SearchState? { didSet { if let searchState { // I'm not a Combine expert so if there is a better way to do this I'm @@ -107,21 +107,31 @@ extension Ghostty { // The time this surface last became focused. This is a ContinuousClock.Instant // on supported platforms. - @Published var focusInstant: ContinuousClock.Instant? = nil + @Published var focusInstant: ContinuousClock.Instant? // Returns sizing information for the surface. This is the raw C // structure because I'm lazy. - @Published var surfaceSize: ghostty_surface_size_s? = nil + @Published var surfaceSize: ghostty_surface_size_s? // Whether the pointer should be visible or not @Published private(set) var pointerStyle: CursorStyle = .horizontalText + // Whether the mouse is currently over this surface + @Published private(set) var mouseOverSurface: Bool = false + + // The last known mouse location in the surface's local coordinate space, + // used by overlays such as the split drag handle reveal region. + @Published private(set) var mouseLocationInSurface: CGPoint? + + // Whether the cursor is currently visible (not hidden by typing, etc.) + @Published private(set) var cursorVisible: Bool = true + /// The configuration derived from the Ghostty config so we don't need to rely on references. @Published private(set) var derivedConfig: DerivedConfig /// The background color within the color palette of the surface. This is only set if it is /// dynamically updated. Otherwise, the background color is the default background color. - @Published private(set) var backgroundColor: Color? = nil + @Published private(set) var backgroundColor: Color? /// True when the bell is active. This is set inactive on focus or event. @Published private(set) var bell: Bool = false @@ -134,7 +144,7 @@ extension Ghostty { // An initial size to request for a window. This will only affect // then the view is moved to a new window. - var initialSize: NSSize? = nil + var initialSize: NSSize? // A content size received through sizeDidChange that may in some cases // be different from the frame size. @@ -151,7 +161,7 @@ extension Ghostty { // We need to update our state within the SecureInput manager. let input = SecureInput.shared let id = ObjectIdentifier(self) - if (passwordInput) { + if passwordInput { input.setScoped(id, focused: focused) } else { input.removeScoped(id) @@ -183,7 +193,7 @@ extension Ghostty { // True if the inspector should be visible @Published var inspectorVisible: Bool = false { didSet { - if (oldValue && !inspectorVisible) { + if oldValue && !inspectorVisible { guard let surface = self.surface else { return } ghostty_inspector_free(surface) } @@ -210,10 +220,14 @@ extension Ghostty { private var markedText: NSMutableAttributedString private(set) var focused: Bool = true private var prevPressureStage: Int = 0 - private var appearanceObserver: NSKeyValueObservation? = nil + private var appearanceObserver: NSKeyValueObservation? // This is set to non-null during keyDown to accumulate insertText contents - private var keyTextAccumulator: [String]? = nil + private var keyTextAccumulator: [String]? + + // True when we've consumed a left mouse-down only to move focus and + // should suppress the matching mouse-up from being reported. + private var suppressNextLeftMouseUp: Bool = false // A small delay that is introduced before a title change to avoid flickers private var titleChangeTimer: Timer? @@ -234,7 +248,7 @@ extension Ghostty { private(set) var cachedVisibleContents: CachedValue /// Event monitor (see individual events for why) - private var eventMonitor: Any? = nil + private var eventMonitor: Any? // We need to support being a first responder so that we can get input events override var acceptsFirstResponder: Bool { return true } @@ -259,7 +273,7 @@ extension Ghostty { // Initialize with some default frame size. The important thing is that this // is non-zero so that our layer bounds are non-zero so that our renderer // can do SOMETHING. - super.init(frame: NSMakeRect(0, 0, 800, 600)) + super.init(frame: NSRect(x: 0, y: 0, width: 800, height: 600)) // Our cache of screen data cachedScreenContents = .init(duration: .milliseconds(500)) { [weak self] in @@ -428,14 +442,23 @@ extension Ghostty { guard let surface = self.surface else { return } guard self.focused != focused else { return } self.focused = focused + + // If we lost our focus then remove the mouse event suppression so + // our mouse release event leaving the surface can properly be + // sent to stop things like mouse selection. + if !focused { + suppressNextLeftMouseUp = false + } + + // Notify libghostty ghostty_surface_set_focus(surface, focused) // Update our secure input state if we are a password input - if (passwordInput) { + if passwordInput { SecureInput.shared.setScoped(ObjectIdentifier(self), focused: focused) } - if (focused) { + if focused { // On macOS 13+ we can store our continuous clock... focusInstant = ContinuousClock.now @@ -480,7 +503,7 @@ extension Ghostty { } func setCursorShape(_ shape: ghostty_action_mouse_shape_e) { - switch (shape) { + switch shape { case GHOSTTY_MOUSE_SHAPE_DEFAULT: pointerStyle = .default @@ -533,6 +556,7 @@ extension Ghostty { } func setCursorVisibility(_ visible: Bool) { + cursorVisible = visible // Technically this action could be called anytime we want to // change the mouse visibility but at the time of writing this // mouse-hide-while-typing is the only use case so this is the @@ -637,12 +661,24 @@ extension Ghostty { let location = convert(event.locationInWindow, from: nil) guard hitTest(location) == self else { return event } - // We only want to grab focus if either our app or window was - // not focused. - guard !NSApp.isActive || !window.isKeyWindow else { return event } + // We always assume that we're resetting our mouse suppression + // unless we see the specific scenario below to set it. + suppressNextLeftMouseUp = false - // If we're already focused we do nothing - guard !focused else { return event } + // If we're already the first responder then no focus transfer is + // happening, so the click should continue as normal. + guard window.firstResponder !== self else { + return event + } + + // If our window/app is already focused, then this click is only + // being used to transfer split focus. Consume it so it does not + // get forwarded to the terminal as a mouse click. + if NSApp.isActive && window.isKeyWindow { + window.makeFirstResponder(self) + suppressNextLeftMouseUp = true + return nil + } // Make ourselves the first responder window.makeFirstResponder(self) @@ -656,7 +692,7 @@ extension Ghostty { private func localEventKeyUp(_ event: NSEvent) -> NSEvent? { // We only care about events with "command" because all others will // trigger the normal responder chain. - if (!event.modifierFlags.contains(.command)) { return event } + if !event.modifierFlags.contains(.command) { return event } // Command keyUp events are never sent to the normal responder chain // so we send them here. @@ -671,7 +707,7 @@ extension Ghostty { guard let healthAny = notification.userInfo?["health"] else { return } guard let health = healthAny as? ghostty_action_renderer_health_e else { return } DispatchQueue.main.async { [weak self] in - self?.healthy = health == GHOSTTY_RENDERER_HEALTH_OK + self?.healthy = health == GHOSTTY_RENDERER_HEALTH_HEALTHY } } @@ -722,7 +758,7 @@ extension Ghostty { SwiftUI.Notification.Name.GhosttyColorChangeKey ] as? Ghostty.Action.ColorChange else { return } - switch (change.kind) { + switch change.kind { case .background: DispatchQueue.main.async { [weak self] in self?.backgroundColor = change.color @@ -767,7 +803,7 @@ extension Ghostty { override func becomeFirstResponder() -> Bool { let result = super.becomeFirstResponder() - if (result) { focusDidChange(true) } + if result { focusDidChange(true) } return result } @@ -776,7 +812,7 @@ extension Ghostty { // We sometimes call this manually (see SplitView) as a way to force us to // yield our focus state. - if (result) { focusDidChange(false) } + if result { focusDidChange(false) } return result } @@ -847,6 +883,13 @@ extension Ghostty { } override func mouseUp(with event: NSEvent) { + // If this mouse-up corresponds to a focus-only click transfer, + // suppress it so we don't emit a release without a press. + if suppressNextLeftMouseUp { + suppressNextLeftMouseUp = false + return + } + // Always reset our pressure when the mouse goes up prevPressureStage = 0 @@ -873,17 +916,16 @@ extension Ghostty { ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, button.cMouseButton, mods) } - override func rightMouseDown(with event: NSEvent) { guard let surface = self.surface else { return super.rightMouseDown(with: event) } let mods = Ghostty.ghosttyMods(event.modifierFlags) - if (ghostty_surface_mouse_button( + if ghostty_surface_mouse_button( surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_RIGHT, mods - )) { + ) { // Consumed return } @@ -896,12 +938,12 @@ extension Ghostty { guard let surface = self.surface else { return super.rightMouseUp(with: event) } let mods = Ghostty.ghosttyMods(event.modifierFlags) - if (ghostty_surface_mouse_button( + if ghostty_surface_mouse_button( surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_RIGHT, mods - )) { + ) { // Handled return } @@ -911,15 +953,18 @@ extension Ghostty { } override func mouseEntered(with event: NSEvent) { + mouseOverSurface = true super.mouseEntered(with: event) + let pos = self.convert(event.locationInWindow, from: nil) + mouseLocationInSurface = pos + guard let surfaceModel else { return } // On mouse enter we need to reset our cursor position. This is // super important because we set it to -1/-1 on mouseExit and // lots of mouse logic (i.e. whether to send mouse reports) depend // on the position being in the viewport if it is. - let pos = self.convert(event.locationInWindow, from: nil) let mouseEvent = Ghostty.Input.MousePosEvent( x: pos.x, y: frame.height - pos.y, @@ -929,6 +974,8 @@ extension Ghostty { } override func mouseExited(with event: NSEvent) { + mouseOverSurface = false + mouseLocationInSurface = nil guard let surfaceModel else { return } // If the mouse is being dragged then we don't have to emit @@ -948,10 +995,12 @@ extension Ghostty { } override func mouseMoved(with event: NSEvent) { + let pos = self.convert(event.locationInWindow, from: nil) + mouseLocationInSurface = pos + guard let surfaceModel else { return } // Convert window position to view position. Note (0, 0) is bottom left. - let pos = self.convert(event.locationInWindow, from: nil) let mouseEvent = Ghostty.Input.MousePosEvent( x: pos.x, y: frame.height - pos.y, @@ -963,10 +1012,9 @@ extension Ghostty { if let window, let controller = window.windowController as? BaseTerminalController, !controller.commandPaletteIsShowing, - (window.isKeyWindow && + window.isKeyWindow && !self.focused && - controller.focusFollowsMouse) - { + controller.focusFollowsMouse { Ghostty.moveFocus(to: self) } } @@ -992,8 +1040,8 @@ extension Ghostty { if precision { // We do a 2x speed multiplier. This is subjective, it "feels" better to me. - x *= 2; - y *= 2; + x *= 2 + y *= 2 // TODO(mitchellh): do we have to scale the x/y here by window scale factor? } @@ -1022,7 +1070,7 @@ extension Ghostty { // If the user has force click enabled then we do a quick look. There // is no public API for this as far as I can tell. - guard UserDefaults.standard.bool(forKey: "com.apple.trackpad.forceClick") else { return } + guard UserDefaults.ghostty.bool(forKey: "com.apple.trackpad.forceClick") else { return } quickLook(with: event) } @@ -1048,7 +1096,7 @@ extension Ghostty { // for exact states and set them. var translationMods = event.modifierFlags for flag in [NSEvent.ModifierFlags.shift, .control, .option, .command] { - if (translationModsGhostty.contains(flag)) { + if translationModsGhostty.contains(flag) { translationMods.insert(flag) } else { translationMods.remove(flag) @@ -1061,7 +1109,7 @@ extension Ghostty { // this keeps things like Korean input working. There must be some object // equality happening in AppKit somewhere because this is required. let translationEvent: NSEvent - if (translationMods == event.modifierFlags) { + if translationMods == event.modifierFlags { translationEvent = event } else { translationEvent = NSEvent.keyEvent( @@ -1093,7 +1141,7 @@ extension Ghostty { // We need to know the keyboard layout before below because some keyboard // input events will change our keyboard layout and we don't want those // going to the terminal. - let keyboardIdBefore: String? = if (!markedTextBefore) { + let keyboardIdBefore: String? = if !markedTextBefore { KeyboardLayout.id } else { nil @@ -1108,7 +1156,7 @@ extension Ghostty { // If our keyboard changed from this we just assume an input method // grabbed it and do nothing. - if (!markedTextBefore && keyboardIdBefore != KeyboardLayout.id) { + if !markedTextBefore && keyboardIdBefore != KeyboardLayout.id { return } @@ -1185,17 +1233,17 @@ extension Ghostty { // We only care about key down events. It might not even be possible // to receive any other event type here. guard event.type == .keyDown else { return false } - + // Only process events if we're focused. Some key events like C-/ macOS // appears to send to the first view in the hierarchy rather than the // the first responder (I don't know why). This prevents us from handling it. // Besides C-/, its important we don't process key equivalents if unfocused // because there are other event listeners for that (i.e. AppDelegate's // local event handler). - if (!focused) { + if !focused { return false } - + // Get information about if this is a binding. let bindingFlags = surfaceModel.flatMap { surface in var ghosttyEvent = event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS) @@ -1204,7 +1252,7 @@ extension Ghostty { return surface.keyIsBinding(ghosttyEvent) } } - + // If this is a binding then we want to perform it. if let bindingFlags { // Attempt to trigger a menu item for this key binding. We only do this if: @@ -1217,21 +1265,22 @@ extension Ghostty { keyTables.isEmpty, bindingFlags.isDisjoint(with: [.all, .performable]), bindingFlags.contains(.consumed) { - if let menu = NSApp.mainMenu, menu.performKeyEquivalent(with: event) { + if let appDelegate = NSApp.delegate as? AppDelegate, + appDelegate.performGhosttyBindingMenuKeyEquivalent(with: event) { return true } } - + self.keyDown(with: event) return true } let equivalent: String - switch (event.charactersIgnoringModifiers) { + switch event.charactersIgnoringModifiers { case "\r": // Pass C- through verbatim // (prevent the default context menu equivalent) - if (!event.modifierFlags.contains(.control)) { + if !event.modifierFlags.contains(.control) { return false } @@ -1240,8 +1289,8 @@ extension Ghostty { case "/": // Treat C-/ as C-_. We do this because C-/ makes macOS make a beep // sound and we don't like the beep sound. - if (!event.modifierFlags.contains(.control) || - !event.modifierFlags.isDisjoint(with: [.shift, .command, .option])) { + if !event.modifierFlags.contains(.control) || + !event.modifierFlags.isDisjoint(with: [.shift, .command, .option]) { return false } @@ -1265,8 +1314,8 @@ extension Ghostty { // Ignore all other non-command events. This lets the event continue // through the AppKit event systems. - if (!event.modifierFlags.contains(.command) && - !event.modifierFlags.contains(.control)) { + if !event.modifierFlags.contains(.command) && + !event.modifierFlags.contains(.control) { // Reset since we got a non-command event. lastPerformKeyEvent = nil return false @@ -1304,8 +1353,8 @@ extension Ghostty { } override func flagsChanged(with event: NSEvent) { - let mod: UInt32; - switch (event.keyCode) { + let mod: UInt32 + switch event.keyCode { case 0x39: mod = GHOSTTY_MODS_CAPS.rawValue case 0x38, 0x3C: mod = GHOSTTY_MODS_SHIFT.rawValue case 0x3B, 0x3E: mod = GHOSTTY_MODS_CTRL.rawValue @@ -1323,26 +1372,26 @@ extension Ghostty { // If the key that pressed this is active, its a press, else release. var action = GHOSTTY_ACTION_RELEASE - if (mods.rawValue & mod != 0) { + if mods.rawValue & mod != 0 { // If the key is pressed, its slightly more complicated, because we // want to check if the pressed modifier is the correct side. If the // correct side is pressed then its a press event otherwise its a release // event with the opposite modifier still held. let sidePressed: Bool - switch (event.keyCode) { + switch event.keyCode { case 0x3C: - sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERSHIFTKEYMASK) != 0; + sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERSHIFTKEYMASK) != 0 case 0x3E: - sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERCTLKEYMASK) != 0; + sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERCTLKEYMASK) != 0 case 0x3D: - sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERALTKEYMASK) != 0; + sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERALTKEYMASK) != 0 case 0x36: - sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERCMDKEYMASK) != 0; + sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERCMDKEYMASK) != 0 default: sidePressed = true } - if (sidePressed) { + if sidePressed { action = GHOSTTY_ACTION_PRESS } } @@ -1389,7 +1438,7 @@ extension Ghostty { // since we always have a primary font. The only scenario this doesn't // work is if someone is using a non-CoreText build which would be // unofficial. - var attributes: [ NSAttributedString.Key : Any ] = [:]; + var attributes: [ NSAttributedString.Key: Any ] = [:] if let fontRaw = ghostty_surface_quicklook_font(surface) { // Memory management here is wonky: ghostty_surface_quicklook_font // will create a copy of a CTFont, Swift will auto-retain the @@ -1400,9 +1449,9 @@ extension Ghostty { } // Ghostty coordinate system is top-left, convert to bottom-left for AppKit - let pt = NSMakePoint(text.tl_px_x, frame.size.height - text.tl_px_y) + let pt = NSPoint(x: text.tl_px_x, y: frame.size.height - text.tl_px_y) let str = NSAttributedString.init(string: String(cString: text.text), attributes: attributes) - self.showDefinition(for: str, at: pt); + self.showDefinition(for: str, at: pt) } override func menu(for event: NSEvent) -> NSMenu? { @@ -1483,7 +1532,7 @@ extension Ghostty { @IBAction func copy(_ sender: Any?) { guard let surface = self.surface else { return } let action = "copy_to_clipboard" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1491,16 +1540,15 @@ extension Ghostty { @IBAction func paste(_ sender: Any?) { guard let surface = self.surface else { return } let action = "paste_from_clipboard" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } - @IBAction func pasteAsPlainText(_ sender: Any?) { guard let surface = self.surface else { return } let action = "paste_from_clipboard" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1508,7 +1556,7 @@ extension Ghostty { @IBAction func pasteSelection(_ sender: Any?) { guard let surface = self.surface else { return } let action = "paste_from_selection" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1516,7 +1564,7 @@ extension Ghostty { @IBAction override func selectAll(_ sender: Any?) { guard let surface = self.surface else { return } let action = "select_all" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1524,7 +1572,7 @@ extension Ghostty { @IBAction func find(_ sender: Any?) { guard let surface = self.surface else { return } let action = "start_search" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1532,7 +1580,7 @@ extension Ghostty { @IBAction func selectionForFind(_ sender: Any?) { guard let surface = self.surface else { return } let action = "search_selection" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1540,7 +1588,7 @@ extension Ghostty { @IBAction func scrollToSelection(_ sender: Any?) { guard let surface = self.surface else { return } let action = "scroll_to_selection" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1548,7 +1596,7 @@ extension Ghostty { @IBAction func findNext(_ sender: Any?) { guard let surface = self.surface else { return } let action = "search:next" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1556,7 +1604,7 @@ extension Ghostty { @IBAction func findPrevious(_ sender: Any?) { guard let surface = self.surface else { return } let action = "search:previous" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1564,7 +1612,7 @@ extension Ghostty { @IBAction func findHide(_ sender: Any?) { guard let surface = self.surface else { return } let action = "end_search" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1572,7 +1620,7 @@ extension Ghostty { @IBAction func toggleReadonly(_ sender: Any?) { guard let surface = self.surface else { return } let action = "toggle_readonly" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1608,7 +1656,7 @@ extension Ghostty { @objc func resetTerminal(_ sender: Any) { guard let surface = self.surface else { return } let action = "reset" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1616,7 +1664,7 @@ extension Ghostty { @objc func toggleTerminalInspector(_ sender: Any) { guard let surface = self.surface else { return } let action = "inspector:toggle" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1626,14 +1674,17 @@ extension Ghostty { } /// Show a user notification and associate it with this surface - func showUserNotification(title: String, body: String) { + func showUserNotification(title: String, body: String, requireFocus: Bool = true) { let content = UNMutableNotificationContent() content.title = title content.subtitle = self.title content.body = body content.sound = UNNotificationSound.default content.categoryIdentifier = Ghostty.userNotificationCategory - content.userInfo = ["surface": self.id.uuidString] + content.userInfo = [ + "surface": self.id.uuidString, + "requireFocus": requireFocus, + ] let uuid = UUID().uuidString let request = UNNotificationRequest( @@ -1657,7 +1708,7 @@ extension Ghostty { // If we're focused then we schedule to remove the notification // after a few seconds. If we gain focus we automatically remove it // in focusDidChange. - if (self.focused) { + if self.focused { Task { @MainActor [weak self] in try await Task.sleep(for: .seconds(3)) self?.notificationIdentifiers.remove(uuid) @@ -1831,7 +1882,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { // since we always have a primary font. The only scenario this doesn't // work is if someone is using a non-CoreText build which would be // unofficial. - var attributes: [ NSAttributedString.Key : Any ] = [:]; + var attributes: [ NSAttributedString.Key: Any ] = [:] if let fontRaw = ghostty_surface_quicklook_font(surface) { // Memory management here is wonky: ghostty_surface_quicklook_font // will create a copy of a CTFont, Swift will auto-retain the @@ -1850,7 +1901,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect { guard let surface = self.surface else { - return NSMakeRect(frame.origin.x, frame.origin.y, 0, 0) + return NSRect(x: frame.origin.x, y: frame.origin.y, width: 0, height: 0) } // Ghostty will tell us where it thinks an IME keyboard should render. @@ -1869,8 +1920,8 @@ extension Ghostty.SurfaceView: NSTextInputClient { if ghostty_surface_read_selection(surface, &text) { // The -2/+2 here is subjective. QuickLook seems to offset the rectangle // a bit and I think these small adjustments make it look more natural. - x = text.tl_px_x - 2; - y = text.tl_px_y + 2; + x = text.tl_px_x - 2 + y = text.tl_px_y + 2 // Free our text ghostty_surface_free_text(surface, &text) @@ -1892,11 +1943,11 @@ extension Ghostty.SurfaceView: NSTextInputClient { // when there's is no characters selected, // width should be 0 so that dictation indicator // can start in the right place - let viewRect = NSMakeRect( - x, - frame.size.height - y, - width, - max(height, cellSize.height)) + let viewRect = NSRect( + x: x, + y: frame.size.height - y, + width: width, + height: max(height, cellSize.height)) // Convert the point to the window coordinates let winRect = self.convert(viewRect, to: nil) @@ -1913,7 +1964,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { // We want the string view of the any value var chars = "" - switch (string) { + switch string { case let v as NSAttributedString: chars = v.string case let v as String: @@ -1944,8 +1995,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { // we send it back through the event system so it can be encoded. if let lastPerformKeyEvent, let current = NSApp.currentEvent, - lastPerformKeyEvent == current.timestamp - { + lastPerformKeyEvent == current.timestamp { NSApp.sendEvent(current) return } @@ -2052,7 +2102,7 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor { guard let str = pboard.getOpinionatedStringContents() else { return false } let len = str.utf8CString.count - if (len == 0) { return true } + if len == 0 { return true } str.withCString { ptr in // len includes the null terminator so we do len - 1 ghostty_surface_text(surface, ptr, UInt(len - 1)) @@ -2134,7 +2184,7 @@ extension Ghostty.SurfaceView { DispatchQueue.main.async { self.insertText( content, - replacementRange: NSMakeRange(0, 0) + replacementRange: NSRange(location: 0, length: 0) ) } return true diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift index f9baf56c9b4..9a4cf4d9b88 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift @@ -15,7 +15,7 @@ extension Ghostty { @Published var title: String = "👻" // The current pwd of the surface. - @Published var pwd: String? = nil + @Published var pwd: String? // The cell size of this surface. This is set by the core when the // surface is first created and any time the cell size changes (i.e. @@ -28,30 +28,30 @@ extension Ghostty { @Published var healthy: Bool = true // Any error while initializing the surface. - @Published var error: Error? = nil + @Published var error: Error? // The hovered URL - @Published var hoverUrl: String? = nil - + @Published var hoverUrl: String? + // The progress report (if any) - @Published var progressReport: Action.ProgressReport? = nil + @Published var progressReport: Action.ProgressReport? // The time this surface last became focused. This is a ContinuousClock.Instant // on supported platforms. - @Published var focusInstant: ContinuousClock.Instant? = nil + @Published var focusInstant: ContinuousClock.Instant? /// True when the bell is active. This is set inactive on focus or event. @Published var bell: Bool = false - + // The current search state. When non-nil, the search overlay should be shown. - @Published var searchState: SearchState? = nil + @Published var searchState: SearchState? // The currently active key tables. Empty if no tables are active. @Published var keyTables: [String] = [] /// True when the surface is in readonly mode. @Published private(set) var readonly: Bool = false - + /// True when the surface should show a highlight effect (e.g., when presented via goto_split). @Published private(set) var highlighted: Bool = false @@ -81,7 +81,7 @@ extension Ghostty { // TODO return } - self.surface = surface; + self.surface = surface } required init?(coder: NSCoder) { @@ -98,7 +98,7 @@ extension Ghostty { ghostty_surface_set_focus(surface, focused) // On macOS 13+ we can store our continuous clock... - if (focused) { + if focused { focusInstant = ContinuousClock.now } } @@ -122,9 +122,7 @@ extension Ghostty { // MARK: UIView override class var layerClass: AnyClass { - get { - return CAMetalLayer.self - } + return CAMetalLayer.self } override func didMoveToWindow() { diff --git a/macos/Sources/Helpers/AnySortKey.swift b/macos/Sources/Helpers/AnySortKey.swift index 6813ccf45eb..ffafb6b90e2 100644 --- a/macos/Sources/Helpers/AnySortKey.swift +++ b/macos/Sources/Helpers/AnySortKey.swift @@ -4,7 +4,7 @@ import Foundation struct AnySortKey: Comparable { private let value: Any private let comparator: (Any, Any) -> ComparisonResult - + init(_ value: T) { self.value = value self.comparator = { lhs, rhs in @@ -14,11 +14,11 @@ struct AnySortKey: Comparable { return .orderedSame } } - + static func < (lhs: AnySortKey, rhs: AnySortKey) -> Bool { lhs.comparator(lhs.value, rhs.value) == .orderedAscending } - + static func == (lhs: AnySortKey, rhs: AnySortKey) -> Bool { lhs.comparator(lhs.value, rhs.value) == .orderedSame } diff --git a/macos/Sources/Helpers/AppInfo.swift b/macos/Sources/Helpers/AppInfo.swift index 281bad18b04..940d247d567 100644 --- a/macos/Sources/Helpers/AppInfo.swift +++ b/macos/Sources/Helpers/AppInfo.swift @@ -2,9 +2,5 @@ import Foundation /// True if we appear to be running in Xcode. func isRunningInXcode() -> Bool { - if let _ = ProcessInfo.processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] { - return true - } - - return false + ProcessInfo.processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] != nil } diff --git a/macos/Sources/Helpers/Backport.swift b/macos/Sources/Helpers/Backport.swift index 8c43652e4e3..e2afb5b0cab 100644 --- a/macos/Sources/Helpers/Backport.swift +++ b/macos/Sources/Helpers/Backport.swift @@ -48,7 +48,7 @@ extension Backport where Content: View { return content #endif } - + /// Backported onKeyPress that works on macOS 14+ and is a no-op on macOS 13. func onKeyPress(_ key: KeyEquivalent, action: @escaping (EventModifiers) -> BackportKeyPressResult) -> some View { #if canImport(AppKit) @@ -117,3 +117,17 @@ enum BackportPointerStyle { } #endif } + +enum BackportNSGlassStyle { + case regular, clear + + #if canImport(AppKit) + @available(macOS 26, *) + var official: NSGlassEffectView.Style { + switch self { + case .regular: return .regular + case .clear: return .clear + } + } + #endif +} diff --git a/macos/Sources/Helpers/ExpiringUndoManager.swift b/macos/Sources/Helpers/ExpiringUndoManager.swift index 5fde0e87041..3b1abd44a17 100644 --- a/macos/Sources/Helpers/ExpiringUndoManager.swift +++ b/macos/Sources/Helpers/ExpiringUndoManager.swift @@ -98,10 +98,10 @@ class ExpiringUndoManager: UndoManager { private class ExpiringTarget { /// The actual target object for the undo operation, held weakly to avoid retain cycles. private(set) weak var target: AnyObject? - + /// Timer that triggers expiration after the specified duration. private var timer: Timer? - + /// The undo manager from which to remove actions when this target expires. private weak var undoManager: UndoManager? @@ -141,7 +141,7 @@ extension ExpiringTarget: Hashable, Equatable { static func == (lhs: ExpiringTarget, rhs: ExpiringTarget) -> Bool { return lhs === rhs } - + func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } diff --git a/macos/Sources/Helpers/Extensions/Array+Extension.swift b/macos/Sources/Helpers/Extensions/Array+Extension.swift index 4e8e399182d..92beb050599 100644 --- a/macos/Sources/Helpers/Extensions/Array+Extension.swift +++ b/macos/Sources/Helpers/Extensions/Array+Extension.swift @@ -2,7 +2,7 @@ extension Array { subscript(safe index: Int) -> Element? { return indices.contains(index) ? self[index] : nil } - + /// Returns the index before i, with wraparound. Assumes i is a valid index. func indexWrapping(before i: Int) -> Int { if i == 0 { @@ -35,7 +35,7 @@ extension Array where Element == String { if index == count { return try body(accumulated) } - + return try self[index].withCString { cStr in var newAccumulated = accumulated newAccumulated.append(cStr) diff --git a/macos/Sources/Helpers/Extensions/EventModifiers+Extension.swift b/macos/Sources/Helpers/Extensions/EventModifiers+Extension.swift index 8d379bd9966..cc8d49cf8ef 100644 --- a/macos/Sources/Helpers/Extensions/EventModifiers+Extension.swift +++ b/macos/Sources/Helpers/Extensions/EventModifiers+Extension.swift @@ -5,11 +5,13 @@ import SwiftUI extension EventModifiers { init(nsFlags: NSEvent.ModifierFlags) { var result: SwiftUI.EventModifiers = [] + // swiftlint:disable opening_brace if nsFlags.contains(.shift) { result.insert(.shift) } if nsFlags.contains(.control) { result.insert(.control) } if nsFlags.contains(.option) { result.insert(.option) } if nsFlags.contains(.command) { result.insert(.command) } if nsFlags.contains(.capsLock) { result.insert(.capsLock) } + // swiftlint:enable opening_brace self = result } } @@ -17,11 +19,13 @@ extension EventModifiers { extension NSEvent.ModifierFlags { init(swiftUIFlags: SwiftUI.EventModifiers) { var result: NSEvent.ModifierFlags = [] + // swiftlint:disable opening_brace if swiftUIFlags.contains(.shift) { result.insert(.shift) } if swiftUIFlags.contains(.control) { result.insert(.control) } if swiftUIFlags.contains(.option) { result.insert(.option) } if swiftUIFlags.contains(.command) { result.insert(.command) } if swiftUIFlags.contains(.capsLock) { result.insert(.capsLock) } + // swiftlint:enable opening_brace self = result } } diff --git a/macos/Sources/Helpers/Extensions/NSAppearance+Extension.swift b/macos/Sources/Helpers/Extensions/NSAppearance+Extension.swift index 28edb1a3515..c45f37a62ed 100644 --- a/macos/Sources/Helpers/Extensions/NSAppearance+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSAppearance+Extension.swift @@ -9,7 +9,7 @@ extension NSAppearance { /// Initialize a desired NSAppearance for the Ghostty configuration. convenience init?(ghosttyConfig config: Ghostty.Config) { guard let theme = config.windowTheme else { return nil } - switch (theme) { + switch theme { case "dark": self.init(named: .darkAqua) diff --git a/macos/Sources/Helpers/Extensions/NSApplication+Extension.swift b/macos/Sources/Helpers/Extensions/NSApplication+Extension.swift index 0bc79fb6af8..2d3bc2cba04 100644 --- a/macos/Sources/Helpers/Extensions/NSApplication+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSApplication+Extension.swift @@ -18,7 +18,7 @@ extension NSApplication { func releasePresentationOption(_ option: NSApplication.PresentationOptions.Element) { guard let value = Self.presentationOptionCounts[option] else { return } guard value > 0 else { return } - if (value == 1) { + if value == 1 { presentationOptions.remove(option) Self.presentationOptionCounts.removeValue(forKey: option) } else { diff --git a/macos/Sources/Helpers/Extensions/NSColor+Extension.swift b/macos/Sources/Helpers/Extensions/NSColor+Extension.swift index 63cf02ed4d6..ed2177325df 100644 --- a/macos/Sources/Helpers/Extensions/NSColor+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSColor+Extension.swift @@ -24,6 +24,14 @@ extension NSColor { appleColorList?.allKeys.map { $0.lowercased() } ?? [] } + /// Returns a new color with its saturation multiplied by the given factor, clamped to [0, 1]. + func adjustingSaturation(by factor: CGFloat) -> NSColor { + var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 + let hsbColor = self.usingColorSpace(.sRGB) ?? self + hsbColor.getHue(&h, saturation: &s, brightness: &b, alpha: &a) + return NSColor(hue: h, saturation: min(max(s * factor, 0), 1), brightness: b, alpha: a) + } + /// Calculates the perceptual distance to another color in RGB space. func distance(to other: NSColor) -> Double { guard let a = self.usingColorSpace(.sRGB), diff --git a/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift b/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift index a036f02b4f1..a54735fde21 100644 --- a/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift @@ -13,14 +13,14 @@ extension NSPasteboard.PasteboardType { default: break } - + // Try to get UTType from MIME type guard let utType = UTType(mimeType: mimeType) else { // Fallback: use the MIME type directly as identifier self.init(mimeType) return } - + // Use the UTType's identifier self.init(utType.identifier) } @@ -50,7 +50,7 @@ extension NSPasteboard { /// The pasteboard for the Ghostty enum type. static func ghostty(_ clipboard: ghostty_clipboard_e) -> NSPasteboard? { - switch (clipboard) { + switch clipboard { case GHOSTTY_CLIPBOARD_STANDARD: return Self.general diff --git a/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift b/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift index a8eb7b87651..84553ed346c 100644 --- a/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift @@ -5,7 +5,7 @@ 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 } @@ -18,8 +18,8 @@ extension NSScreen { // AND present on this screen. var hasDock: Bool { // If the dock autohides then we don't have a dock ever. - if let dockAutohide = UserDefaults.standard.persistentDomain(forName: "com.apple.dock")?["autohide"] as? Bool { - if (dockAutohide) { return false } + if let dockAutohide = UserDefaults.ghostty.persistentDomain(forName: "com.apple.dock")?["autohide"] as? Bool { + if dockAutohide { return false } } // There is no public API to directly ask about dock visibility, so we have to figure it out @@ -29,7 +29,7 @@ extension NSScreen { // which triggers showing the dock. // If our visible width is less than the frame we assume its the dock. - if (visibleFrame.width < frame.width) { + if visibleFrame.width < frame.width { return true } @@ -48,7 +48,7 @@ extension NSScreen { // know any other situation this is true. return safeAreaInsets.top > 0 } - + /// Converts top-left offset coordinates to bottom-left origin coordinates for window positioning. /// - Parameters: /// - x: X offset from top-left corner @@ -57,11 +57,11 @@ extension NSScreen { /// - Returns: CGPoint suitable for setFrameOrigin that positions the window as requested func origin(fromTopLeftOffsetX x: CGFloat, offsetY y: CGFloat, windowSize: CGSize) -> CGPoint { let vf = visibleFrame - + // Convert top-left coordinates to bottom-left origin let originX = vf.minX + x let originY = vf.maxY - y - windowSize.height - + return CGPoint(x: originX, y: originY) } } diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift index fb209e4ac43..2546caa3810 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -52,10 +52,8 @@ extension NSView { return true } - for subview in subviews { - if subview.contains(view) { - return true - } + for subview in subviews where subview.contains(view) { + return true } return false @@ -67,10 +65,8 @@ extension NSView { return true } - for subview in subviews { - if subview.contains(className: name) { - return true - } + for subview in subviews where subview.contains(className: name) { + return true } return false @@ -131,12 +127,12 @@ extension NSView { /// This includes private views like title bar views. func firstViewFromRoot(withClassName name: String) -> NSView? { let root = rootView - + // Check if the root view itself matches if String(describing: type(of: root)) == name { return root } - + // Otherwise search descendants return root.firstDescendant(withClassName: name) } @@ -155,67 +151,67 @@ extension NSView { print("View Hierarchy from Root:") print(root.viewHierarchyDescription()) } - + /// Returns a string representation of the view hierarchy in a tree-like format. func viewHierarchyDescription(indent: String = "", isLast: Bool = true) -> String { var result = "" - + // Add the tree branch characters result += indent if !indent.isEmpty { result += isLast ? "└── " : "├── " } - + // Add the class name and optional identifier let className = String(describing: type(of: self)) result += className - + // Add identifier if present if let identifier = self.identifier { result += " (id: \(identifier.rawValue))" } - + // Add frame info result += " [frame: \(frame)]" - + // Add visual properties var properties: [String] = [] - + // Hidden status if isHidden { properties.append("hidden") } - + // Opaque status properties.append(isOpaque ? "opaque" : "transparent") - + // Layer backing if wantsLayer { properties.append("layer-backed") if let bgColor = layer?.backgroundColor { let color = NSColor(cgColor: bgColor) if let rgb = color?.usingColorSpace(.deviceRGB) { - properties.append(String(format: "bg:rgba(%.0f,%.0f,%.0f,%.2f)", - rgb.redComponent * 255, - rgb.greenComponent * 255, - rgb.blueComponent * 255, + properties.append(String(format: "bg:rgba(%.0f,%.0f,%.0f,%.2f)", + rgb.redComponent * 255, + rgb.greenComponent * 255, + rgb.blueComponent * 255, rgb.alphaComponent)) } else { properties.append("bg:\(bgColor)") } } } - + result += " [\(properties.joined(separator: ", "))]" result += "\n" - + // Process subviews for (index, subview) in subviews.enumerated() { let isLastSubview = index == subviews.count - 1 let newIndent = indent + (isLast ? " " : "│ ") result += subview.viewHierarchyDescription(indent: newIndent, isLast: isLastSubview) } - + return result } } diff --git a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift index 5d1831f261c..46758a42db4 100644 --- a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift @@ -39,6 +39,23 @@ extension NSWindow { guard let firstWindow = tabGroup?.windows.first else { return true } return firstWindow === self } + + /// Wraps `addTabbedWindow` with an Objective-C exception catcher because AppKit can + /// throw NSExceptions in visual tab picker flows. Swift cannot safely recover from + /// those exceptions, so we route through Obj-C and log a recoverable failure. + @discardableResult + func addTabbedWindowSafely( + _ child: NSWindow, + ordered: NSWindow.OrderingMode + ) -> Bool { + var error: NSError? + let success = GhosttyAddTabbedWindowSafely(self, child, ordered.rawValue, &error) + if let error { + Ghostty.logger.error("addTabbedWindow failed: \(error.localizedDescription)") + } + + return success + } } /// Native tabbing private API usage. :( @@ -52,31 +69,43 @@ extension NSWindow { guard themeFrameView.responds(to: Selector(("titlebarView"))) else { return nil } return themeFrameView.value(forKey: "titlebarView") as? NSView } - + /// Returns the [private] NSTabBar view, if it exists. var tabBarView: NSView? { titlebarView?.firstDescendant(withClassName: "NSTabBar") } - - /// Returns the index of the tab button at the given screen point, if any. - func tabIndex(atScreenPoint screenPoint: NSPoint) -> Int? { - guard let tabBarView else { return nil } - let locationInWindow = convertPoint(fromScreen: screenPoint) - let locationInTabBar = tabBarView.convert(locationInWindow, from: nil) + + /// Returns tab button views in visual order from left to right. + func tabButtonsInVisualOrder() -> [NSView] { + guard let tabBarView else { return [] } + return tabBarView + .descendants(withClassName: "NSTabButton") + .sorted { $0.frame.minX < $1.frame.minX } + } + + /// Returns the visual tab index and matching tab button at the given screen point. + func tabButtonHit(atScreenPoint screenPoint: NSPoint) -> (index: Int, tabButton: NSView)? { + guard let tabBarView, let tabBarWindow = tabBarView.window else { return nil } + + // In fullscreen, AppKit can host the titlebar and tab bar in a separate + // NSToolbarFullScreenWindow. Hit testing has to use that window's base + // coordinate space or content clicks can be misinterpreted as tab clicks. + let locationInTabBarWindow = tabBarWindow.convertPoint(fromScreen: screenPoint) + let locationInTabBar = tabBarView.convert(locationInTabBarWindow, from: nil) guard tabBarView.bounds.contains(locationInTabBar) else { return nil } - - // Find all tab buttons and sort by x position to get visual order. - // The view hierarchy order doesn't match the visual tab order. - let tabItemViews = tabBarView.descendants(withClassName: "NSTabButton") - .sorted { $0.frame.origin.x < $1.frame.origin.x } - - for (index, tabItemView) in tabItemViews.enumerated() { - let locationInTab = tabItemView.convert(locationInWindow, from: nil) - if tabItemView.bounds.contains(locationInTab) { - return index + + for (index, tabButton) in tabButtonsInVisualOrder().enumerated() { + let locationInTabButton = tabButton.convert(locationInTabBarWindow, from: nil) + if tabButton.bounds.contains(locationInTabButton) { + return (index, tabButton) } } - + return nil } + + /// Returns the index of the tab button at the given screen point, if any. + func tabIndex(atScreenPoint screenPoint: NSPoint) -> Int? { + tabButtonHit(atScreenPoint: screenPoint)?.index + } } diff --git a/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift b/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift index bc2d028b530..c4f7ca5c16a 100644 --- a/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift @@ -7,7 +7,13 @@ extension NSWorkspace { var defaultTextEditor: URL? { defaultApplicationURL(forContentType: UTType.plainText.identifier) } - + + /// Returns the URL of the default terminal (Unix Executable) application. + /// - Returns: The URL of the default terminal, or nil if no default terminal is found. + var defaultTerminal: URL? { + defaultApplicationURL(forContentType: UTType.unixExecutable.identifier) + } + /// Returns the URL of the default application for opening files with the specified content type. /// - Parameter contentType: The content type identifier (UTI) to find the default application for. /// - Returns: The URL of the default application, or nil if no default application is found. @@ -18,7 +24,7 @@ extension NSWorkspace { nil )?.takeRetainedValue() as? URL } - + /// Returns the URL of the default application for opening files with the specified file extension. /// - Parameter ext: The file extension to find the default application for. /// - Returns: The URL of the default application, or nil if no default application is found. diff --git a/macos/Sources/Helpers/Extensions/OSColor+Extension.swift b/macos/Sources/Helpers/Extensions/OSColor+Extension.swift index 54b3e1fab42..67246bcf528 100644 --- a/macos/Sources/Helpers/Extensions/OSColor+Extension.swift +++ b/macos/Sources/Helpers/Extensions/OSColor+Extension.swift @@ -1,5 +1,7 @@ import Foundation +#if !DOCK_TILE_PLUGIN import GhosttyKit +#endif extension OSColor { var isLightColor: Bool { @@ -92,7 +94,7 @@ extension OSColor { } // MARK: Ghostty Types - +#if !DOCK_TILE_PLUGIN extension OSColor { /// Create a color from a Ghostty color. convenience init(ghostty: ghostty_config_color_s) { @@ -102,3 +104,4 @@ extension OSColor { self.init(red: red, green: green, blue: blue, alpha: 1) } } +#endif diff --git a/macos/Sources/Helpers/Extensions/ObjectIdentifier+Extension.swift b/macos/Sources/Helpers/Extensions/ObjectIdentifier+Extension.swift new file mode 100644 index 00000000000..d2440f1d49c --- /dev/null +++ b/macos/Sources/Helpers/Extensions/ObjectIdentifier+Extension.swift @@ -0,0 +1,7 @@ +import Foundation + +extension ObjectIdentifier { + var hexString: String { + String(UInt(bitPattern: self), radix: 16) + } +} diff --git a/macos/Sources/Helpers/Extensions/String+Extension.swift b/macos/Sources/Helpers/Extensions/String+Extension.swift index 03f715fd8d6..4fa61cd78b7 100644 --- a/macos/Sources/Helpers/Extensions/String+Extension.swift +++ b/macos/Sources/Helpers/Extensions/String+Extension.swift @@ -27,5 +27,13 @@ extension String { } #endif - + /// Converts a four-character ASCII string to its `FourCharCode` (`UInt32`) value. + var fourCharCode: UInt32 { + assert(count <= 4, "FourCharCode string must be at most 4 characters") + var result: UInt32 = 0 + for byte in utf8.prefix(4) { + result = (result << 8) | UInt32(byte) + } + return result + } } diff --git a/macos/Sources/Helpers/Extensions/Transferable+Extension.swift b/macos/Sources/Helpers/Extensions/Transferable+Extension.swift index 3bcc9057f31..a45cdc7a4ed 100644 --- a/macos/Sources/Helpers/Extensions/Transferable+Extension.swift +++ b/macos/Sources/Helpers/Extensions/Transferable+Extension.swift @@ -40,16 +40,16 @@ private final class TransferableDataProvider: NSObject, NSPasteboardItemDataProv // to block until the async load completes. This is safe because AppKit // calls this method on a background thread during drag operations. let semaphore = DispatchSemaphore(value: 0) - + var result: Data? itemProvider.loadDataRepresentation(forTypeIdentifier: type.rawValue) { data, _ in result = data semaphore.signal() } - + // Wait for the data to load semaphore.wait() - + // Set it. I honestly don't know what happens here if this fails. if let data = result { item.setData(data, forType: type) diff --git a/macos/Sources/Helpers/Extensions/UserDefaults+Extension.swift b/macos/Sources/Helpers/Extensions/UserDefaults+Extension.swift new file mode 100644 index 00000000000..7cd0e12edce --- /dev/null +++ b/macos/Sources/Helpers/Extensions/UserDefaults+Extension.swift @@ -0,0 +1,15 @@ +import Foundation + +extension UserDefaults { + static var ghosttySuite: String? { + #if DEBUG + ProcessInfo.processInfo.environment["GHOSTTY_USER_DEFAULTS_SUITE"] + #else + nil + #endif + } + + static var ghostty: UserDefaults { + ghosttySuite.flatMap(UserDefaults.init(suiteName:)) ?? .standard + } +} diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index 8ab47626723..139059190d5 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -204,12 +204,12 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // We must hide the dock FIRST then hide the menu: // If you specify autoHideMenuBar, it must be accompanied by either hideDock or autoHideDock. // https://developer.apple.com/documentation/appkit/nsapplication/presentationoptions-swift.struct - if (savedState.dock) { + if savedState.dock { hideDock() } // Hide the menu if requested - if (properties.hideMenu && savedState.menu) { + if properties.hideMenu && savedState.menu { hideMenu() } @@ -261,7 +261,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { if savedState.dock { unhideDock() } - if (properties.hideMenu && savedState.menu) { + if properties.hideMenu && savedState.menu { unhideMenu() } @@ -296,13 +296,13 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { if tabIndex == 0 { // We were previously the first tab. Add it before ("below") // the first window in the tab group currently. - tabGroup.windows.first!.addTabbedWindow(window, ordered: .below) + tabGroup.windows.first!.addTabbedWindowSafely(window, ordered: .below) } else if tabIndex <= tabGroup.windows.count { // We were somewhere in the middle - tabGroup.windows[tabIndex - 1].addTabbedWindow(window, ordered: .above) + tabGroup.windows[tabIndex - 1].addTabbedWindowSafely(window, ordered: .above) } else { // We were at the end - tabGroup.windows.last!.addTabbedWindow(window, ordered: .below) + tabGroup.windows.last!.addTabbedWindowSafely(window, ordered: .below) } } @@ -328,8 +328,8 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // calculate this ourselves. var frame = screen.frame - if (!NSApp.presentationOptions.contains(.autoHideMenuBar) && - !NSApp.presentationOptions.contains(.hideMenuBar)) { + if !NSApp.presentationOptions.contains(.autoHideMenuBar) && + !NSApp.presentationOptions.contains(.hideMenuBar) { // We need to subtract the menu height since we're still showing it. frame.size.height -= NSApp.mainMenu?.menuBarHeight ?? 0 @@ -339,7 +339,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // put an #available check, but it was in a bug fix release so I think // if a bug is reported to Ghostty we can just advise the user to // update. - } else if (properties.paddedNotch) { + } else if properties.paddedNotch { // We are hiding the menu, we may need to avoid the notch. frame.size.height -= screen.safeAreaInsets.top } @@ -413,7 +413,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { self.toolbarStyle = window.toolbarStyle self.dock = window.screen?.hasDock ?? false - self.titlebarAccessoryViewControllers = if (window.hasTitleBar) { + self.titlebarAccessoryViewControllers = if window.hasTitleBar { // Accessing titlebarAccessoryViewControllers without a titlebar triggers a crash. window.titlebarAccessoryViewControllers } else { diff --git a/macos/Sources/Helpers/LastWindowPosition.swift b/macos/Sources/Helpers/LastWindowPosition.swift index a0dfa90dda5..c7989b6faf3 100644 --- a/macos/Sources/Helpers/LastWindowPosition.swift +++ b/macos/Sources/Helpers/LastWindowPosition.swift @@ -6,24 +6,57 @@ class LastWindowPosition { private let positionKey = "NSWindowLastPosition" - func save(_ window: NSWindow) { - let origin = window.frame.origin - let point = [origin.x, origin.y] - UserDefaults.standard.set(point, forKey: positionKey) + @discardableResult + func save(_ window: NSWindow?) -> Bool { + // We should only save the frame if the window is visible. + // This avoids overriding the previously saved one + // with the wrong one when window decorations change while creating, + // e.g. adding a toolbar affects the window's frame. + guard let window, window.isVisible else { return false } + let frame = window.frame + let rect = [frame.origin.x, frame.origin.y, frame.size.width, frame.size.height] + UserDefaults.ghostty.set(rect, forKey: positionKey) + return true } - func restore(_ window: NSWindow) -> Bool { - guard let points = UserDefaults.standard.array(forKey: positionKey) as? [Double], - points.count == 2 else { return false } + /// Restores a previously saved window frame (or parts of it) onto the given window. + /// + /// - Parameters: + /// - window: The window whose frame should be updated. + /// - restoreOrigin: Whether to restore the saved position. Pass `false` when the + /// config specifies an explicit `window-position-x`/`window-position-y`. + /// - restoreSize: Whether to restore the saved size. Pass `false` when the config + /// specifies an explicit `window-width`/`window-height`. + /// - Returns: `true` if the frame was modified, `false` if there was nothing to restore. + @discardableResult + func restore(_ window: NSWindow, origin restoreOrigin: Bool = true, size restoreSize: Bool = true) -> Bool { + guard restoreOrigin || restoreSize else { return false } + + guard let values = UserDefaults.ghostty.array(forKey: positionKey) as? [Double], + values.count >= 2 else { return false } - let lastPosition = CGPoint(x: points[0], y: points[1]) + let lastPosition = CGPoint(x: values[0], y: values[1]) guard let screen = window.screen ?? NSScreen.main else { return false } let visibleFrame = screen.visibleFrame var newFrame = window.frame - newFrame.origin = lastPosition - if !visibleFrame.contains(newFrame.origin) { + if restoreOrigin { + newFrame.origin = lastPosition + } + + if restoreSize, values.count >= 4 { + newFrame.size.width = min(values[2], visibleFrame.width) + newFrame.size.height = min(values[3], visibleFrame.height) + } + + // If the new frame is not constrained to the visible screen, + // we need to shift it a little bit before AppKit does this for us, + // so that we can save the correct size beforehand. + // This fixes restoration while running UI tests, + // where config is modified without switching apps, + // which will not trigger `windowDidBecomeMain`. + if restoreOrigin, !visibleFrame.contains(newFrame) { newFrame.origin.x = max(visibleFrame.minX, min(visibleFrame.maxX - newFrame.width, newFrame.origin.x)) newFrame.origin.y = max(visibleFrame.minY, min(visibleFrame.maxY - newFrame.height, newFrame.origin.y)) } diff --git a/macos/Sources/Helpers/MetalView.swift b/macos/Sources/Helpers/MetalView.swift index 6579f886347..e8c27b52b8e 100644 --- a/macos/Sources/Helpers/MetalView.swift +++ b/macos/Sources/Helpers/MetalView.swift @@ -10,7 +10,7 @@ struct MetalView: View { } } -fileprivate struct MetalViewRepresentable: NSViewRepresentable { +private struct MetalViewRepresentable: NSViewRepresentable { @Binding var metalView: V func makeNSView(context: Context) -> some NSView { diff --git a/macos/Sources/Helpers/ObjCExceptionCatcher.h b/macos/Sources/Helpers/ObjCExceptionCatcher.h new file mode 100644 index 00000000000..7906b59457f --- /dev/null +++ b/macos/Sources/Helpers/ObjCExceptionCatcher.h @@ -0,0 +1,13 @@ +#import + +/// This file contains wrappers around various ObjC functions so we can catch +/// exceptions, since you can't natively catch ObjC exceptions from Swift +/// (at least at the time of writing this comment). + +/// NSWindow.addTabbedWindow wrapper +FOUNDATION_EXPORT BOOL GhosttyAddTabbedWindowSafely( + id _Nonnull parent, + id _Nonnull child, + NSInteger ordered, + NSError * _Nullable * _Nullable error +); diff --git a/macos/Sources/Helpers/ObjCExceptionCatcher.m b/macos/Sources/Helpers/ObjCExceptionCatcher.m new file mode 100644 index 00000000000..e91fb14a766 --- /dev/null +++ b/macos/Sources/Helpers/ObjCExceptionCatcher.m @@ -0,0 +1,32 @@ +#import "ObjCExceptionCatcher.h" + +#import + +BOOL GhosttyAddTabbedWindowSafely( + id parent, + id child, + NSInteger ordered, + NSError * _Nullable * _Nullable error +) { + // AppKit occasionally throws NSException while adding tabbed windows, + // in particular when creating tabs from the tab overview page since some + // macOS update recently in 2025/2026 (unclear). + // + // We must catch it in Objective-C; letting this cross into Swift is unsafe. + @try { + [((NSWindow *)parent) addTabbedWindow:(NSWindow *)child ordered:(NSWindowOrderingMode)ordered]; + return YES; + } @catch (NSException *exception) { + if (error != NULL) { + NSString *reason = exception.reason ?: @"Unknown Objective-C exception"; + *error = [NSError errorWithDomain:@"Ghostty.ObjCException" + code:1 + userInfo:@{ + NSLocalizedDescriptionKey: reason, + @"exception_name": exception.name, + }]; + } + + return NO; + } +} diff --git a/macos/Sources/Helpers/PermissionRequest.swift b/macos/Sources/Helpers/PermissionRequest.swift index 9c16c7163ee..0308a02042e 100644 --- a/macos/Sources/Helpers/PermissionRequest.swift +++ b/macos/Sources/Helpers/PermissionRequest.swift @@ -40,7 +40,7 @@ class PermissionRequest { completion(storedResult) return } - + let alert = NSAlert() alert.messageText = message alert.informativeText = informative @@ -59,7 +59,7 @@ class PermissionRequest { target: nil, action: nil) checkbox!.state = .off - + // Set checkbox as accessory view alert.accessoryView = checkbox } @@ -74,7 +74,7 @@ class PermissionRequest { handleResponse(response, rememberDecision: checkbox?.state == .on, key: key, allowDuration: allowDuration, rememberDuration: rememberDuration, completion: completion) } } - + /// Handles the alert response and processes caching logic /// - Parameters: /// - response: The alert response from the user @@ -90,7 +90,7 @@ class PermissionRequest { allowDuration: AllowDuration, rememberDuration: Duration?, completion: @escaping (Bool) -> Void) { - + let result: Bool switch response { case .alertFirstButtonReturn: // Allow @@ -100,7 +100,7 @@ class PermissionRequest { default: result = false } - + // Store the result if checkbox is checked or if "Allow" was selected and allowDuration is set if rememberDecision, let rememberDuration = rememberDuration { storeResult(result, for: key, duration: rememberDuration) @@ -118,30 +118,30 @@ class PermissionRequest { storeResult(result, for: key, duration: duration) } } - + completion(result) } - + /// Retrieves a cached permission decision if it hasn't expired /// - Parameter key: The UserDefaults key to check /// - Returns: The cached decision, or nil if no valid cached decision exists private static func getStoredResult(for key: String) -> Bool? { - let userDefaults = UserDefaults.standard + let userDefaults = UserDefaults.ghostty guard let data = userDefaults.data(forKey: key), let storedPermission = try? NSKeyedUnarchiver.unarchivedObject( ofClass: StoredPermission.self, from: data) else { return nil } - + if Date() > storedPermission.expiry { // Decision has expired, remove stored value userDefaults.removeObject(forKey: key) return nil } - + return storedPermission.result } - + /// Stores a permission decision in UserDefaults with an expiration date /// - Parameters: /// - result: The permission decision to store @@ -151,7 +151,7 @@ class PermissionRequest { let expiryDate = Date().addingTimeInterval(duration.timeInterval) let storedPermission = StoredPermission(result: result, expiry: expiryDate) if let data = try? NSKeyedArchiver.archivedData(withRootObject: storedPermission, requiringSecureCoding: true) { - let userDefaults = UserDefaults.standard + let userDefaults = UserDefaults.ghostty userDefaults.set(data, forKey: key) } } @@ -180,7 +180,7 @@ class PermissionRequest { return "Remember my decision for \(days) day\(days == 1 ? "" : "s")" } } - + /// Internal class for storing permission decisions with expiration dates in UserDefaults /// Conforms to NSSecureCoding for safe archiving/unarchiving @objc(StoredPermission) diff --git a/macos/Sources/Helpers/TabTitleEditor.swift b/macos/Sources/Helpers/TabTitleEditor.swift new file mode 100644 index 00000000000..4be2c5306f1 --- /dev/null +++ b/macos/Sources/Helpers/TabTitleEditor.swift @@ -0,0 +1,437 @@ +import AppKit + +/// Delegate used by ``TabTitleEditor`` to resolve tab-specific behavior. +protocol TabTitleEditorDelegate: AnyObject { + /// Returns whether inline rename should be allowed for the given tab window. + func tabTitleEditor( + _ editor: TabTitleEditor, + canRenameTabFor targetWindow: NSWindow + ) -> Bool + + /// Returns the current title value to seed into the inline editor. + func tabTitleEditor( + _ editor: TabTitleEditor, + titleFor targetWindow: NSWindow + ) -> String + + /// Called when inline editing commits a title for a target tab window. + func tabTitleEditor( + _ editor: TabTitleEditor, + didCommitTitle editedTitle: String, + for targetWindow: NSWindow + ) + + /// Called when inline editing could not start and the host should show a fallback flow. + func tabTitleEditor( + _ editor: TabTitleEditor, + performFallbackRenameFor targetWindow: NSWindow + ) + + /// Called after inline editing finishes (whether committed or cancelled). + /// Use this to restore focus to the appropriate responder. + func tabTitleEditor( + _ editor: TabTitleEditor, + didFinishEditing targetWindow: NSWindow) +} + +/// Handles inline tab title editing for native AppKit window tabs. +final class TabTitleEditor: NSObject, NSTextFieldDelegate { + /// Host window containing the tab bar where editing occurs. + private weak var hostWindow: NSWindow? + /// Delegate that provides and commits title data for target tab windows. + private weak var delegate: TabTitleEditorDelegate? + /// Local event monitor so fullscreen titlebar-window clicks can also trigger rename. + private var eventMonitor: Any? + + /// Active inline editor view, if editing is in progress. + private weak var inlineTitleEditor: NSTextField? + /// Tab window currently being edited. + private weak var inlineTitleTargetWindow: NSWindow? + /// Original state of the tab bar. + private var previousTabState: TabUIState? + /// Deferred begin-editing work used to avoid visual flicker on double-click. + private var pendingEditWorkItem: DispatchWorkItem? + + /// Creates a coordinator bound to a host window and rename delegate. + init(hostWindow: NSWindow, delegate: TabTitleEditorDelegate) { + super.init() + + self.hostWindow = hostWindow + self.delegate = delegate + + // This is needed so that fullscreen clicks can register since they won't + // event on the NSWindow. We may want to tighten this up in the future by + // only doing this if we're fullscreen. + self.eventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown]) { [weak self] event in + guard let self else { return event } + return handleMouseDown(event) ? nil : event + } + } + + deinit { + if let eventMonitor { + NSEvent.removeMonitor(eventMonitor) + } + } + + /// Handles leftMouseDown events from the host window and begins inline edit if possible. If this + /// returns true then the event was handled by the coordinator. + func handleMouseDown(_ event: NSEvent) -> Bool { + guard event.type == .leftMouseDown else { return false } + + // If we don't have a host window to look up the click, we do nothing. + guard let hostWindow else { return false } + + // In native fullscreen, AppKit can route titlebar clicks through a detached + // NSToolbarFullScreenWindow. Only allow clicks from the host window or its + // fullscreen tab bar window so rename handling stays scoped to this tab strip. + let sourceWindow = event.window ?? hostWindow + guard sourceWindow === hostWindow || sourceWindow === hostWindow.tabBarView?.window + else { return false } + + // Find the tab window that is being clicked. + let locationInScreen = sourceWindow.convertPoint(toScreen: event.locationInWindow) + guard let tabIndex = hostWindow.tabIndex(atScreenPoint: locationInScreen), + let targetWindow = hostWindow.tabbedWindows?[safe: tabIndex], + delegate?.tabTitleEditor(self, canRenameTabFor: targetWindow) == true + else { return false } + + guard !isMouseEventWithinEditor(event) else { + // If the click lies within the editor, + // we should forward the event to the editor + inlineTitleEditor?.mouseDown(with: event) + return true + } + // We only want double-clicks to enable editing + guard event.clickCount == 2 else { return false } + // We need to start editing in a separate event loop tick, so set that up. + pendingEditWorkItem?.cancel() + let workItem = DispatchWorkItem { [weak self, weak targetWindow] in + guard let self, let targetWindow else { return } + if self.beginEditing(for: targetWindow) { + return + } + + // Inline editing failed, so trigger fallback rename whatever it is. + self.delegate?.tabTitleEditor(self, performFallbackRenameFor: targetWindow) + } + + pendingEditWorkItem = workItem + DispatchQueue.main.async(execute: workItem) + return true + } + + /// Handles rightMouseDown events from the host window. + /// + /// If this returns true then the event was handled by the coordinator. + func handleRightMouseDown(_ event: NSEvent) -> Bool { + if isMouseEventWithinEditor(event) { + inlineTitleEditor?.rightMouseDown(with: event) + return true + } else { + return false + } + } + + /// Begins editing the given target tab window title. Returns true if we're able to start the + /// inline edit. + @discardableResult + func beginEditing(for targetWindow: NSWindow) -> Bool { + // Resolve the visual tab button for the target tab window. We rely on visual order + // since native tab view hierarchy order does not necessarily match what is on screen. + guard let hostWindow, + let tabbedWindows = hostWindow.tabbedWindows, + let tabIndex = tabbedWindows.firstIndex(of: targetWindow), + let tabButton = hostWindow.tabButtonsInVisualOrder()[safe: tabIndex], + delegate?.tabTitleEditor(self, canRenameTabFor: targetWindow) == true + else { return false } + + // If we have a pending edit, we need to cancel it because we got + // called to start edit explicitly. + pendingEditWorkItem?.cancel() + pendingEditWorkItem = nil + finishEditing(commit: true) + + let tabState = TabUIState(tabButton: tabButton) + + // Build the editor using title text and style derived from the tab's existing label. + let editedTitle = delegate?.tabTitleEditor(self, titleFor: targetWindow) ?? targetWindow.title + let sourceLabel = sourceTabTitleLabel(from: tabState.labels.map(\.label), matching: editedTitle) + let editorFrame = tabTitleEditorFrame(for: tabButton, sourceLabel: sourceLabel) + guard editorFrame.width >= 20, editorFrame.height >= 14 else { return false } + + let editor = NSTextField(frame: editorFrame) + editor.delegate = self + editor.stringValue = editedTitle + editor.alignment = sourceLabel?.alignment ?? .center + editor.isBordered = false + editor.isBezeled = false + editor.drawsBackground = false + editor.focusRingType = .none + editor.lineBreakMode = .byClipping + if let editorCell = editor.cell as? NSTextFieldCell { + editorCell.wraps = false + editorCell.usesSingleLineMode = true + editorCell.isScrollable = true + } + if let sourceLabel { + applyTextStyle(to: editor, from: sourceLabel, title: editedTitle) + } + + // Hide it until the tab button has finished layout so we can avoid flicker. + editor.isHidden = true + + inlineTitleEditor = editor + inlineTitleTargetWindow = targetWindow + previousTabState = tabState + // Temporarily hide native title label views while editing so only the text field is visible. + CATransaction.begin() + CATransaction.setDisableActions(true) + tabState.hide() + + tabButton.layoutSubtreeIfNeeded() + tabButton.displayIfNeeded() + tabButton.addSubview(editor) + CATransaction.commit() + + // Focus after insertion so AppKit has created the field editor for this text field. + DispatchQueue.main.async { [weak hostWindow, weak editor] in + guard let editor else { return } + let responderWindow = editor.window ?? hostWindow + guard let responderWindow else { return } + editor.isHidden = false + responderWindow.makeFirstResponder(editor) + if let fieldEditor = editor.currentEditor() as? NSTextView, + let editorFont = editor.font { + fieldEditor.font = editorFont + var typingAttributes = fieldEditor.typingAttributes + typingAttributes[.font] = editorFont + fieldEditor.typingAttributes = typingAttributes + } + editor.currentEditor()?.selectAll(nil) + } + + return true + } + + /// Finishes any in-flight inline edit and optionally commits the edited title. + func finishEditing(commit: Bool) { + // If we're pending starting a new edit, cancel it. + pendingEditWorkItem?.cancel() + pendingEditWorkItem = nil + + // To finish editing we need a current editor. + guard let editor = inlineTitleEditor else { return } + let editedTitle = editor.stringValue + let targetWindow = inlineTitleTargetWindow + + // Clear coordinator references first so re-entrant paths don't see stale state. + editor.delegate = nil + inlineTitleEditor = nil + inlineTitleTargetWindow = nil + + // Make sure the window grabs focus again + if let responderWindow = editor.window ?? hostWindow { + if let currentEditor = editor.currentEditor(), responderWindow.firstResponder === currentEditor { + responderWindow.makeFirstResponder(nil) + } else if responderWindow.firstResponder === editor { + responderWindow.makeFirstResponder(nil) + } + } + + editor.removeFromSuperview() + + previousTabState?.restore() + previousTabState = nil + + // Delegate owns title persistence semantics (including empty-title handling). + guard let targetWindow else { return } + + if commit { + delegate?.tabTitleEditor(self, didCommitTitle: editedTitle, for: targetWindow) + } + + // Notify delegate that editing is done so it can restore focus. + delegate?.tabTitleEditor(self, didFinishEditing: targetWindow) + } + + /// Chooses an editor frame that aligns with the tab title within the tab button. + private func tabTitleEditorFrame(for tabButton: NSView, sourceLabel: NSTextField?) -> NSRect { + let bounds = tabButton.bounds + let horizontalInset: CGFloat = 6 + var frame = bounds.insetBy(dx: horizontalInset, dy: 0) + + if let sourceLabel { + let labelFrame = tabButton.convert(sourceLabel.bounds, from: sourceLabel) + /// The `labelFrame.minY` value changes unexpectedly after double clicking selected text, + /// I don't know exactly why, but `tabButton.bounds` appears stable enough to calculate the correct position reliably. + frame.origin.y = bounds.midY - labelFrame.height * 0.5 + frame.size.height = labelFrame.height + } + + return frame.integral + } + + /// Selects the best title label candidate from private tab button subviews. + private func sourceTabTitleLabel(from labels: [NSTextField], matching title: String) -> NSTextField? { + let expected = title.trimmingCharacters(in: .whitespacesAndNewlines) + if !expected.isEmpty { + // Prefer a visible exact title match when we can find one. + if let exactVisible = labels.first(where: { + !$0.isHidden && + $0.alphaValue > 0.01 && + $0.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) == expected + }) { + return exactVisible + } + + // Fall back to any exact match, including hidden labels. + if let exactAny = labels.first(where: { + $0.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) == expected + }) { + return exactAny + } + } + + // Otherwise heuristically choose the largest visible, centered label first. + let visibleNonEmpty = labels.filter { + !$0.isHidden && + $0.alphaValue > 0.01 && + !$0.stringValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } + + if let centeredVisible = visibleNonEmpty + .filter({ $0.alignment == .center }) + .max(by: { $0.bounds.width < $1.bounds.width }) { + return centeredVisible + } + + if let visible = visibleNonEmpty.max(by: { $0.bounds.width < $1.bounds.width }) { + return visible + } + + return labels.max(by: { $0.bounds.width < $1.bounds.width }) + } + + /// Copies text styling from the source tab label onto the inline editor. + private func applyTextStyle(to editor: NSTextField, from label: NSTextField, title: String) { + var attributes: [NSAttributedString.Key: Any] = [:] + if label.attributedStringValue.length > 0 { + attributes = label.attributedStringValue.attributes(at: 0, effectiveRange: nil) + } + + if attributes[.font] == nil, let font = label.font { + attributes[.font] = font + } + + if attributes[.foregroundColor] == nil { + attributes[.foregroundColor] = label.textColor + } + + if let font = attributes[.font] as? NSFont { + editor.font = font + } + + if let textColor = attributes[.foregroundColor] as? NSColor { + editor.textColor = textColor + } + + if !attributes.isEmpty { + editor.attributedStringValue = NSAttributedString(string: title, attributes: attributes) + } else { + editor.stringValue = title + } + } + + // MARK: NSTextFieldDelegate + + func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { + guard control === inlineTitleEditor else { return false } + + // Enter commits and exits inline edit. + if commandSelector == #selector(NSResponder.insertNewline(_:)) { + finishEditing(commit: true) + return true + } + + // Escape cancels and restores the previous tab title. + if commandSelector == #selector(NSResponder.cancelOperation(_:)) { + finishEditing(commit: false) + return true + } + + return false + } + + func controlTextDidEndEditing(_ obj: Notification) { + guard let inlineTitleEditor, + let finishedEditor = obj.object as? NSTextField, + finishedEditor === inlineTitleEditor + else { return } + + // Blur/end-edit commits, matching standard NSTextField behavior. + finishEditing(commit: true) + } +} + +private extension TabTitleEditor { + func isMouseEventWithinEditor(_ event: NSEvent) -> Bool { + guard let editor = inlineTitleEditor?.currentEditor() else { + return false + } + return editor.convert(editor.bounds, to: nil).contains(event.locationInWindow) + } +} + +private extension TabTitleEditor { + struct TabUIState { + /// Original hidden state for title labels that are temporarily hidden while editing. + let labels: [(label: NSTextField, wasHidden: Bool)] + /// Original hidden state for buttons that are temporarily hidden while editing. + let buttons: [(button: NSButton, wasHidden: Bool)] + /// Original button title state restored once editing finishes. + let titleButton: (button: NSButton, title: String, attributedTitle: NSAttributedString?)? + + init(tabButton: NSView) { + labels = tabButton + .descendants(withClassName: "NSTextField") + .compactMap { $0 as? NSTextField } + .map { ($0, $0.isHidden) } + buttons = tabButton + .descendants(withClassName: "NSButton") + .compactMap { $0 as? NSButton } + .map { ($0, $0.isHidden) } + if let button = tabButton as? NSButton { + titleButton = (button, button.title, button.attributedTitle) + } else { + titleButton = nil + } + } + + func hide() { + for (label, _) in labels { + label.isHidden = true + } + for (btn, _) in buttons { + btn.isHidden = true + } + titleButton?.button.title = "" + titleButton?.button.attributedTitle = NSAttributedString(string: "") + } + + func restore() { + for (label, wasHidden) in labels { + label.isHidden = wasHidden + } + for (btn, wasHidden) in buttons { + btn.isHidden = wasHidden + } + if let titleButton { + titleButton.button.title = titleButton.title + if let attributedTitle = titleButton.attributedTitle { + titleButton.button.attributedTitle = attributedTitle + } + } + } + } +} diff --git a/macos/Tests/ColorizedGhosttyIconTests.swift b/macos/Tests/ColorizedGhosttyIconTests.swift new file mode 100644 index 00000000000..bf2963f3385 --- /dev/null +++ b/macos/Tests/ColorizedGhosttyIconTests.swift @@ -0,0 +1,144 @@ +import AppKit +import Foundation +import Testing +@testable import Ghostty + +struct ColorizedGhosttyIconTests { + private func makeIcon( + screenColors: [NSColor] = [ + NSColor(hex: "#112233")!, + NSColor(hex: "#AABBCC")!, + ], + ghostColor: NSColor = NSColor(hex: "#445566")!, + frame: Ghostty.MacOSIconFrame = .aluminum + ) -> ColorizedGhosttyIcon { + .init(screenColors: screenColors, ghostColor: ghostColor, frame: frame) + } + + // MARK: - Codable + + @Test func codableRoundTripPreservesIcon() throws { + let icon = makeIcon(frame: .chrome) + let data = try JSONEncoder().encode(icon) + let decoded = try JSONDecoder().decode(ColorizedGhosttyIcon.self, from: data) + + #expect(decoded == icon) + #expect(decoded.screenColors.compactMap(\.hexString) == ["#112233", "#AABBCC"]) + #expect(decoded.ghostColor.hexString == "#445566") + #expect(decoded.frame == .chrome) + } + + @Test func encodingWritesVersionAndHexColors() throws { + let icon = makeIcon(frame: .plastic) + let data = try JSONEncoder().encode(icon) + + let payload = try #require(JSONSerialization.jsonObject(with: data) as? [String: Any]) + #expect(payload["version"] as? Int == 1) + #expect(payload["screenColors"] as? [String] == ["#112233", "#AABBCC"]) + #expect(payload["ghostColor"] as? String == "#445566") + #expect(payload["frame"] as? String == "plastic") + } + + @Test func decodesLegacyV0PayloadWithoutVersion() throws { + let data = Data(""" + { + "screenColors": ["#112233", "#AABBCC"], + "ghostColor": "#445566", + "frame": "beige" + } + """.utf8) + + let decoded = try JSONDecoder().decode(ColorizedGhosttyIcon.self, from: data) + #expect(decoded.screenColors.compactMap(\.hexString) == ["#112233", "#AABBCC"]) + #expect(decoded.ghostColor.hexString == "#445566") + #expect(decoded.frame == .beige) + } + + @Test func decodingUnsupportedVersionThrowsDataCorrupted() { + let data = Data(""" + { + "version": 99, + "screenColors": ["#112233", "#AABBCC"], + "ghostColor": "#445566", + "frame": "chrome" + } + """.utf8) + + do { + _ = try JSONDecoder().decode(ColorizedGhosttyIcon.self, from: data) + Issue.record("Expected decode to fail for unsupported version") + } catch let DecodingError.dataCorrupted(context) { + #expect(context.debugDescription.contains("Unsupported ColorizedGhosttyIcon version")) + } catch { + Issue.record("Expected DecodingError.dataCorrupted, got: \(error)") + } + } + + @Test func decodingInvalidGhostColorThrows() { + let data = Data(""" + { + "version": 1, + "screenColors": ["#112233", "#AABBCC"], + "ghostColor": "not-a-color", + "frame": "chrome" + } + """.utf8) + + do { + _ = try JSONDecoder().decode(ColorizedGhosttyIcon.self, from: data) + Issue.record("Expected decode to fail for invalid ghost color") + } catch let DecodingError.dataCorrupted(context) { + #expect(context.debugDescription.contains("Failed to decode ghost color")) + } catch { + Issue.record("Expected DecodingError.dataCorrupted, got: \(error)") + } + } + + @Test func decodingInvalidScreenColorsDropsInvalidEntries() throws { + let data = Data(""" + { + "version": 1, + "screenColors": ["#112233", "invalid", "#AABBCC"], + "ghostColor": "#445566", + "frame": "chrome" + } + """.utf8) + + let decoded = try JSONDecoder().decode(ColorizedGhosttyIcon.self, from: data) + #expect(decoded.screenColors.compactMap(\.hexString) == ["#112233", "#AABBCC"]) + } + + // MARK: - Equatable + + @Test func equatableUsesHexColorAndFrameValues() { + let lhs = makeIcon( + screenColors: [ + NSColor(red: 0x11 / 255.0, green: 0x22 / 255.0, blue: 0x33 / 255.0, alpha: 1.0), + NSColor(red: 0xAA / 255.0, green: 0xBB / 255.0, blue: 0xCC / 255.0, alpha: 1.0), + ], + ghostColor: NSColor(red: 0x44 / 255.0, green: 0x55 / 255.0, blue: 0x66 / 255.0, alpha: 1.0), + frame: .chrome + ) + let rhs = makeIcon(frame: .chrome) + + #expect(lhs == rhs) + } + + @Test func equatableReturnsFalseForDifferentFrame() { + let lhs = makeIcon(frame: .aluminum) + let rhs = makeIcon(frame: .chrome) + #expect(lhs != rhs) + } + + @Test func equatableReturnsFalseForDifferentScreenColors() { + let lhs = makeIcon(screenColors: [NSColor(hex: "#112233")!, NSColor(hex: "#AABBCC")!]) + let rhs = makeIcon(screenColors: [NSColor(hex: "#112233")!, NSColor(hex: "#CCBBAA")!]) + #expect(lhs != rhs) + } + + @Test func equatableReturnsFalseForDifferentGhostColor() { + let lhs = makeIcon(ghostColor: NSColor(hex: "#445566")!) + let rhs = makeIcon(ghostColor: NSColor(hex: "#665544")!) + #expect(lhs != rhs) + } +} diff --git a/macos/Tests/Ghostty/ConfigTests.swift b/macos/Tests/Ghostty/ConfigTests.swift new file mode 100644 index 00000000000..b9c9d6a4a02 --- /dev/null +++ b/macos/Tests/Ghostty/ConfigTests.swift @@ -0,0 +1,244 @@ +import Testing +@testable import Ghostty +@testable import GhosttyKit +import SwiftUI + +@Suite +struct ConfigTests { + // MARK: - Boolean Properties + + @Test func initialWindowDefaultsToTrue() throws { + let config = try TemporaryConfig("") + #expect(config.initialWindow == true) + } + + @Test func initialWindowSetToFalse() throws { + let config = try TemporaryConfig("initial-window = false") + #expect(config.initialWindow == false) + } + + @Test func quitAfterLastWindowClosedDefaultsToFalse() throws { + let config = try TemporaryConfig("") + #expect(config.shouldQuitAfterLastWindowClosed == false) + } + + @Test func quitAfterLastWindowClosedSetToTrue() throws { + let config = try TemporaryConfig("quit-after-last-window-closed = true") + #expect(config.shouldQuitAfterLastWindowClosed == true) + } + + @Test func windowStepResizeDefaultsToFalse() throws { + let config = try TemporaryConfig("") + #expect(config.windowStepResize == false) + } + + @Test func focusFollowsMouseDefaultsToFalse() throws { + let config = try TemporaryConfig("") + #expect(config.focusFollowsMouse == false) + } + + @Test func focusFollowsMouseSetToTrue() throws { + let config = try TemporaryConfig("focus-follows-mouse = true") + #expect(config.focusFollowsMouse == true) + } + + @Test func windowDecorationsDefaultsToTrue() throws { + let config = try TemporaryConfig("") + #expect(config.windowDecorations == true) + } + + @Test func windowDecorationsNone() throws { + let config = try TemporaryConfig("window-decoration = none") + #expect(config.windowDecorations == false) + } + + @Test func macosWindowShadowDefaultsToTrue() throws { + let config = try TemporaryConfig("") + #expect(config.macosWindowShadow == true) + } + + @Test func maximizeDefaultsToFalse() throws { + let config = try TemporaryConfig("") + #expect(config.maximize == false) + } + + @Test func maximizeSetToTrue() throws { + let config = try TemporaryConfig("maximize = true") + #expect(config.maximize == true) + } + + // MARK: - String / Optional String Properties + + @Test func titleDefaultsToNil() throws { + let config = try TemporaryConfig("") + #expect(config.title == nil) + } + + @Test func titleSetToCustomValue() throws { + let config = try TemporaryConfig("title = My Terminal") + #expect(config.title == "My Terminal") + } + + @Test func windowTitleFontFamilyDefaultsToNil() throws { + let config = try TemporaryConfig("") + #expect(config.windowTitleFontFamily == nil) + } + + @Test func windowTitleFontFamilySetToValue() throws { + let config = try TemporaryConfig("window-title-font-family = Menlo") + #expect(config.windowTitleFontFamily == "Menlo") + } + + // MARK: - Enum Properties + + @Test func macosTitlebarStyleDefaultsToTransparent() throws { + let config = try TemporaryConfig("") + #expect(config.macosTitlebarStyle == .transparent) + } + + @Test(arguments: [ + ("native", Ghostty.Config.MacOSTitlebarStyle.native), + ("transparent", Ghostty.Config.MacOSTitlebarStyle.transparent), + ("tabs", Ghostty.Config.MacOSTitlebarStyle.tabs), + ("hidden", Ghostty.Config.MacOSTitlebarStyle.hidden), + ]) + func macosTitlebarStyleValues(raw: String, expected: Ghostty.Config.MacOSTitlebarStyle) throws { + let config = try TemporaryConfig("macos-titlebar-style = \(raw)") + #expect(config.macosTitlebarStyle == expected) + } + + @Test func resizeOverlayDefaultsToAfterFirst() throws { + let config = try TemporaryConfig("") + #expect(config.resizeOverlay == .after_first) + } + + @Test(arguments: [ + ("always", Ghostty.Config.ResizeOverlay.always), + ("never", Ghostty.Config.ResizeOverlay.never), + ("after-first", Ghostty.Config.ResizeOverlay.after_first), + ]) + func resizeOverlayValues(raw: String, expected: Ghostty.Config.ResizeOverlay) throws { + let config = try TemporaryConfig("resize-overlay = \(raw)") + #expect(config.resizeOverlay == expected) + } + + @Test func resizeOverlayPositionDefaultsToCenter() throws { + let config = try TemporaryConfig("") + #expect(config.resizeOverlayPosition == .center) + } + + @Test func macosIconDefaultsToOfficial() throws { + let config = try TemporaryConfig("") + #expect(config.macosIcon == .official) + } + + @Test func macosIconFrameDefaultsToAluminum() throws { + let config = try TemporaryConfig("") + #expect(config.macosIconFrame == .aluminum) + } + + @Test func macosWindowButtonsDefaultsToVisible() throws { + let config = try TemporaryConfig("") + #expect(config.macosWindowButtons == .visible) + } + + @Test func scrollbarDefaultsToSystem() throws { + let config = try TemporaryConfig("") + #expect(config.scrollbar == .system) + } + + @Test func scrollbarSetToNever() throws { + let config = try TemporaryConfig("scrollbar = never") + #expect(config.scrollbar == .never) + } + + // MARK: - Numeric Properties + + @Test func backgroundOpacityDefaultsToOne() throws { + let config = try TemporaryConfig("") + #expect(config.backgroundOpacity == 1.0) + } + + @Test func backgroundOpacitySetToCustom() throws { + let config = try TemporaryConfig("background-opacity = 0.5") + #expect(config.backgroundOpacity == 0.5) + } + + @Test func windowPositionDefaultsToNil() throws { + let config = try TemporaryConfig("") + #expect(config.windowPositionX == nil) + #expect(config.windowPositionY == nil) + } + + // MARK: - Config Loading + + @Test func loadedIsTrueForValidConfig() throws { + let config = try TemporaryConfig("") + #expect(config.loaded == true) + } + + @Test func unfinalizedConfigIsLoaded() throws { + let config = try TemporaryConfig("", finalize: false) + #expect(config.loaded == true) + } + + @Test func defaultConfigIsLoaded() throws { + let config = try TemporaryConfig("") + #expect(config.optionalAutoUpdateChannel != nil) // release or tip + let config1 = try TemporaryConfig("", finalize: false) + #expect(config1.optionalAutoUpdateChannel == nil) + } + + @Test func errorsEmptyForValidConfig() throws { + let config = try TemporaryConfig("") + #expect(config.errors.isEmpty) + } + + @Test func errorsReportedForInvalidConfig() throws { + let config = try TemporaryConfig("not-a-real-key = value") + #expect(!config.errors.isEmpty) + } + + // MARK: - Multiple Config Lines + + @Test func multipleConfigValues() throws { + let config = try TemporaryConfig(""" + initial-window = false + quit-after-last-window-closed = true + maximize = true + focus-follows-mouse = true + """) + #expect(config.initialWindow == false) + #expect(config.shouldQuitAfterLastWindowClosed == true) + #expect(config.maximize == true) + #expect(config.focusFollowsMouse == true) + } +} + +/// Create a temporary config file and delete it when this is deallocated +class TemporaryConfig: Ghostty.Config { + let temporaryFile: URL + + init(_ configText: String, finalize: Bool = true) throws { + let temporaryFile = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString) + .appendingPathExtension("ghostty") + try configText.write(to: temporaryFile, atomically: true, encoding: .utf8) + self.temporaryFile = temporaryFile + super.init(config: Self.loadConfig(at: temporaryFile.path(), finalize: finalize)) + } + + var optionalAutoUpdateChannel: Ghostty.AutoUpdateChannel? { + guard let config = self.config else { return nil } + var v: UnsafePointer? + let key = "auto-update-channel" + guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } + guard let ptr = v else { return nil } + let str = String(cString: ptr) + return Ghostty.AutoUpdateChannel(rawValue: str) + } + + deinit { + try? FileManager.default.removeItem(at: temporaryFile) + } +} diff --git a/macos/Tests/Ghostty/ShellTests.swift b/macos/Tests/Ghostty/ShellTests.swift index c7b34b3d9ca..5990bedc7ca 100644 --- a/macos/Tests/Ghostty/ShellTests.swift +++ b/macos/Tests/Ghostty/ShellTests.swift @@ -2,6 +2,34 @@ import Testing @testable import Ghostty struct ShellTests { + @Test(arguments: [ + ("hello", "hello"), + ("", ""), + ("file name", "file\\ name"), + ("a\\b", "a\\\\b"), + ("(foo)", "\\(foo\\)"), + ("[bar]", "\\[bar\\]"), + ("{baz}", "\\{baz\\}"), + ("", "\\"), + ("say\"hi\"", "say\\\"hi\\\""), + ("it's", "it\\'s"), + ("`cmd`", "\\`cmd\\`"), + ("wow!", "wow\\!"), + ("#comment", "\\#comment"), + ("$HOME", "\\$HOME"), + ("a&b", "a\\&b"), + ("a;b", "a\\;b"), + ("a|b", "a\\|b"), + ("*.txt", "\\*.txt"), + ("file?.log", "file\\?.log"), + ("col1\tcol2", "col1\\\tcol2"), + ("$(echo 'hi')", "\\$\\(echo\\ \\'hi\\'\\)"), + ("/tmp/my file (1).txt", "/tmp/my\\ file\\ \\(1\\).txt"), + ]) + func escape(input: String, expected: String) { + #expect(Ghostty.Shell.escape(input) == expected) + } + @Test(arguments: [ ("", "''"), ("filename", "filename"), diff --git a/macos/Tests/NSPasteboardTests.swift b/macos/Tests/NSPasteboardTests.swift index d956ce733ed..9db17ca330d 100644 --- a/macos/Tests/NSPasteboardTests.swift +++ b/macos/Tests/NSPasteboardTests.swift @@ -16,14 +16,14 @@ struct NSPasteboardTypeExtensionTests { #expect(pasteboardType != nil) #expect(pasteboardType == .string) } - + /// Test text/html MIME type converts to .html @Test func testTextHtmlMimeType() async throws { let pasteboardType = NSPasteboard.PasteboardType(mimeType: "text/html") #expect(pasteboardType != nil) #expect(pasteboardType == .html) } - + /// Test image/png MIME type @Test func testImagePngMimeType() async throws { let pasteboardType = NSPasteboard.PasteboardType(mimeType: "image/png") diff --git a/macos/Tests/NSScreenTests.swift b/macos/Tests/NSScreenTests.swift index f7431bf05fe..6e67bb7e401 100644 --- a/macos/Tests/NSScreenTests.swift +++ b/macos/Tests/NSScreenTests.swift @@ -15,65 +15,65 @@ struct NSScreenExtensionTests { // Mock screen with 1000x800 visible frame starting at (0, 100) let mockScreenFrame = NSRect(x: 0, y: 100, width: 1000, height: 800) let mockScreen = MockNSScreen(visibleFrame: mockScreenFrame) - + // Mock window size let windowSize = CGSize(width: 400, height: 300) - + // Test top-left positioning: x=15, y=15 let origin = mockScreen.origin( fromTopLeftOffsetX: 15, offsetY: 15, windowSize: windowSize) - + // Expected: x = 0 + 15 = 15, y = (100 + 800) - 15 - 300 = 585 #expect(origin.x == 15) #expect(origin.y == 585) } - + /// Test zero coordinates (exact top-left corner) @Test func testZeroCoordinates() async throws { let mockScreenFrame = NSRect(x: 0, y: 100, width: 1000, height: 800) let mockScreen = MockNSScreen(visibleFrame: mockScreenFrame) let windowSize = CGSize(width: 400, height: 300) - + let origin = mockScreen.origin( fromTopLeftOffsetX: 0, offsetY: 0, windowSize: windowSize) - + // Expected: x = 0, y = (100 + 800) - 0 - 300 = 600 #expect(origin.x == 0) #expect(origin.y == 600) } - + /// Test with offset screen (not starting at origin) @Test func testOffsetScreen() async throws { // Secondary monitor at position (1440, 0) with 1920x1080 resolution let mockScreenFrame = NSRect(x: 1440, y: 0, width: 1920, height: 1080) let mockScreen = MockNSScreen(visibleFrame: mockScreenFrame) let windowSize = CGSize(width: 600, height: 400) - + let origin = mockScreen.origin( fromTopLeftOffsetX: 100, offsetY: 50, windowSize: windowSize) - + // Expected: x = 1440 + 100 = 1540, y = (0 + 1080) - 50 - 400 = 630 #expect(origin.x == 1540) #expect(origin.y == 630) } - + /// Test large coordinates @Test func testLargeCoordinates() async throws { let mockScreenFrame = NSRect(x: 0, y: 0, width: 1920, height: 1080) let mockScreen = MockNSScreen(visibleFrame: mockScreenFrame) let windowSize = CGSize(width: 400, height: 300) - + let origin = mockScreen.origin( fromTopLeftOffsetX: 500, offsetY: 200, windowSize: windowSize) - + // Expected: x = 0 + 500 = 500, y = (0 + 1080) - 200 - 300 = 580 #expect(origin.x == 500) #expect(origin.y == 580) @@ -83,16 +83,16 @@ struct NSScreenExtensionTests { /// Mock NSScreen class for testing coordinate conversion private class MockNSScreen: NSScreen { private let mockVisibleFrame: NSRect - + init(visibleFrame: NSRect) { self.mockVisibleFrame = visibleFrame super.init() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override var visibleFrame: NSRect { return mockVisibleFrame } diff --git a/macos/Tests/Terminal/TerminalViewContainerTests.swift b/macos/Tests/Terminal/TerminalViewContainerTests.swift new file mode 100644 index 00000000000..e3df8483ec9 --- /dev/null +++ b/macos/Tests/Terminal/TerminalViewContainerTests.swift @@ -0,0 +1,103 @@ +// +// TerminalViewContainerTests.swift +// Ghostty +// +// Created by Lukas on 26.02.2026. +// + +import SwiftUI +import Testing +@testable import Ghostty + +class MockTerminalViewContainer: TerminalViewContainer { + var _windowCornerRadius: CGFloat? + override var windowThemeFrameView: NSView? { + NSView() + } + + override var windowCornerRadius: CGFloat? { + _windowCornerRadius + } +} + +class MockConfig: Ghostty.Config { + internal init(backgroundBlur: Ghostty.Config.BackgroundBlur, backgroundColor: Color, backgroundOpacity: Double) { + self._backgroundBlur = backgroundBlur + self._backgroundColor = backgroundColor + self._backgroundOpacity = backgroundOpacity + super.init(config: nil) + } + + var _backgroundBlur: Ghostty.Config.BackgroundBlur + var _backgroundColor: Color + var _backgroundOpacity: Double + + override var backgroundBlur: Ghostty.Config.BackgroundBlur { + _backgroundBlur + } + + override var backgroundColor: Color { + _backgroundColor + } + + override var backgroundOpacity: Double { + _backgroundOpacity + } +} + +struct TerminalViewContainerTests { + @Test func glassAvailability() async throws { + let view = await MockTerminalViewContainer { + EmptyView() + } + + let config = MockConfig(backgroundBlur: .macosGlassRegular, backgroundColor: .clear, backgroundOpacity: 1) + await view.ghosttyConfigDidChange(config, preferredBackgroundColor: nil) + try await Task.sleep(nanoseconds: UInt64(1e8)) // wait for the view to be setup if needed + if #available(macOS 26.0, *) { + #expect(view.glassEffectView != nil) + } else { + #expect(view.glassEffectView == nil) + } + } + +#if compiler(>=6.2) + @Test func configChangeUpdatesGlass() async throws { + guard #available(macOS 26.0, *) else { return } + let view = await MockTerminalViewContainer { + EmptyView() + } + let config1 = MockConfig(backgroundBlur: .macosGlassRegular, backgroundColor: .clear, backgroundOpacity: 1) + await view.ghosttyConfigDidChange(config1, preferredBackgroundColor: nil) + let glassEffectView = await view.descendants(withClassName: "NSGlassEffectView").first as? NSGlassEffectView + let effectView = try #require(glassEffectView) + try await Task.sleep(nanoseconds: UInt64(1e8)) // wait for the view to be setup if needed + #expect(effectView.tintColor?.hexString == NSColor.clear.hexString) + + // Test with same config but with different preferredBackgroundColor + await view.ghosttyConfigDidChange(config1, preferredBackgroundColor: .red) + #expect(effectView.tintColor?.hexString == NSColor.red.hexString) + + // MARK: - Corner Radius + + #expect(effectView.cornerRadius == 0) + await MainActor.run { view._windowCornerRadius = 10 } + + // This won't change, unless ghosttyConfigDidChange is called + #expect(effectView.cornerRadius == 0) + + await view.ghosttyConfigDidChange(config1, preferredBackgroundColor: .red) + #expect(effectView.cornerRadius == 10) + + // MARK: - Glass Style + + #expect(effectView.style == .regular) + + let config2 = MockConfig(backgroundBlur: .macosGlassClear, backgroundColor: .clear, backgroundOpacity: 1) + await view.ghosttyConfigDidChange(config2, preferredBackgroundColor: .red) + + #expect(effectView.style == .clear) + + } +#endif // compiler(>=6.2) +} diff --git a/macos/Tests/Update/ReleaseNotesTests.swift b/macos/Tests/Update/ReleaseNotesTests.swift index b029fa6bc9e..6c7d43ed554 100644 --- a/macos/Tests/Update/ReleaseNotesTests.swift +++ b/macos/Tests/Update/ReleaseNotesTests.swift @@ -9,7 +9,7 @@ struct ReleaseNotesTests { 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") @@ -18,14 +18,14 @@ struct ReleaseNotesTests { 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") @@ -34,14 +34,14 @@ struct ReleaseNotesTests { 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") @@ -50,14 +50,14 @@ struct ReleaseNotesTests { 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") @@ -65,14 +65,14 @@ struct ReleaseNotesTests { 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") @@ -80,46 +80,46 @@ struct ReleaseNotesTests { 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") diff --git a/macos/Tests/Update/UpdateStateTests.swift b/macos/Tests/Update/UpdateStateTests.swift index 354d371c51a..6aefa22a267 100644 --- a/macos/Tests/Update/UpdateStateTests.swift +++ b/macos/Tests/Update/UpdateStateTests.swift @@ -5,25 +5,25 @@ import Sparkle 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(isAutoUpdate: false, retryTerminatingApplication: {}, dismiss: {})) let state2: UpdateState = .installing(.init(isAutoUpdate: false, retryTerminatingApplication: {}, dismiss: {})) @@ -31,7 +31,7 @@ struct UpdateStateTests { let state3: UpdateState = .installing(.init(isAutoUpdate: true, retryTerminatingApplication: {}, dismiss: {})) #expect(state3 != state2) } - + @Test func testPermissionRequestEquality() { let request1 = SPUUpdatePermissionRequest(systemProfile: []) let request2 = SPUUpdatePermissionRequest(systemProfile: []) @@ -39,43 +39,43 @@ struct UpdateStateTests { let state2: UpdateState = .permissionRequest(.init(request: request2, 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"]) @@ -83,7 +83,7 @@ struct UpdateStateTests { 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"]) @@ -91,20 +91,20 @@ struct UpdateStateTests { 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 index 529c2bc52fe..9b747f9ec68 100644 --- a/macos/Tests/Update/UpdateViewModelTests.swift +++ b/macos/Tests/Update/UpdateViewModelTests.swift @@ -6,50 +6,50 @@ import Sparkle 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 testInstallingText() { let viewModel = UpdateViewModel() viewModel.state = .installing(.init(isAutoUpdate: false, retryTerminatingApplication: {}, dismiss: {})) @@ -57,34 +57,34 @@ struct UpdateViewModelTests { viewModel.state = .installing(.init(isAutoUpdate: true, retryTerminatingApplication: {}, dismiss: {})) #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: {})) diff --git a/macos/build.nu b/macos/build.nu new file mode 100755 index 00000000000..8c456d9b61f --- /dev/null +++ b/macos/build.nu @@ -0,0 +1,32 @@ +#!/usr/bin/env nu + +# Build the macOS Ghostty app using xcodebuild with a clean environment +# to avoid Nix shell interference (NIX_LDFLAGS, NIX_CFLAGS_COMPILE, etc.). + +def main [ + --scheme: string = "Ghostty" # Xcode scheme (Ghostty, Ghostty-iOS, DockTilePlugin) + --configuration: string = "Debug" # Build configuration (Debug, Release, ReleaseLocal) + --action: string = "build" # xcodebuild action (build, test, clean, etc.) +] { + let project = ($env.FILE_PWD | path join "Ghostty.xcodeproj") + let build_dir = ($env.FILE_PWD | path join "build") + + # Skip UI tests for CLI-based invocations because it requires + # special permissions. + let skip_testing = if $action == "test" { + [-skip-testing GhosttyUITests] + } else { + [] + } + + (^env -i + $"HOME=($env.HOME)" + "PATH=/usr/bin:/bin:/usr/sbin:/sbin" + xcodebuild + -project $project + -scheme $scheme + -configuration $configuration + $"SYMROOT=($build_dir)" + ...$skip_testing + $action) +} diff --git a/nix/devShell.nix b/nix/devShell.nix index 90059a730a4..c78c9081bdc 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -66,6 +66,7 @@ poop, typos, shellcheck, + swiftlint, uv, wayland, wayland-scanner, @@ -198,6 +199,9 @@ in # for benchmarking poop + ] + ++ lib.optionals stdenv.hostPlatform.isDarwin [ + swiftlint ]; # This should be set onto the rpath of the ghostty binary if you want diff --git a/nix/package.nix b/nix/package.nix index 1efef416418..8287b088814 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -30,7 +30,7 @@ in stdenv.mkDerivation (finalAttrs: { pname = "ghostty"; - version = "1.3.0-dev"; + version = "1.3.2-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/nix/pkgs/blessed.nix b/nix/pkgs/blessed.nix index 8b6728f43f0..da5d6958d84 100644 --- a/nix/pkgs/blessed.nix +++ b/nix/pkgs/blessed.nix @@ -1,22 +1,24 @@ { lib, buildPythonPackage, - fetchPypi, + fetchFromGitHub, pythonOlder, flit-core, six, wcwidth, }: -buildPythonPackage rec { +buildPythonPackage (finalAttrs: { pname = "blessed"; - version = "1.23.0"; + version = "1.31"; pyproject = true; - disabled = pythonOlder "3.7"; + disabled = pythonOlder "3.8"; - src = fetchPypi { - inherit pname version; - hash = "sha256-VlkaMpZvcE9hMfFACvQVHZ6PX0FEEzpcoDQBl2Pe53s="; + src = fetchFromGitHub { + owner = "jquast"; + repo = "blessed"; + tag = finalAttrs.version; + hash = "sha256-Nn+aiDk0Qwk9xAvAqtzds/WlrLAozjPL1eSVNU75tJA="; }; build-system = [flit-core]; @@ -27,6 +29,7 @@ buildPythonPackage rec { ]; doCheck = false; + dontCheckRuntimeDeps = true; meta = with lib; { homepage = "https://github.com/jquast/blessed"; @@ -34,4 +37,4 @@ buildPythonPackage rec { maintainers = []; license = licenses.mit; }; -} +}) diff --git a/nix/pkgs/ucs-detect.nix b/nix/pkgs/ucs-detect.nix index 07ec6c2fc0d..5bbcdd0712d 100644 --- a/nix/pkgs/ucs-detect.nix +++ b/nix/pkgs/ucs-detect.nix @@ -1,36 +1,42 @@ { lib, buildPythonPackage, - fetchPypi, + fetchFromGitHub, pythonOlder, - setuptools, + hatchling, # Dependencies blessed, wcwidth, pyyaml, + prettytable, + requests, }: -buildPythonPackage rec { +buildPythonPackage (finalAttrs: { pname = "ucs-detect"; - version = "1.0.8"; + version = "2.0.2"; pyproject = true; disabled = pythonOlder "3.8"; - src = fetchPypi { - inherit version; - pname = "ucs_detect"; - hash = "sha256-ihB+tZCd6ykdeXYxc6V1Q6xALQ+xdCW5yqSL7oppqJc="; + src = fetchFromGitHub { + owner = "jquast"; + repo = "ucs-detect"; + tag = finalAttrs.version; + hash = "sha256-pCJNrJN+SO0pGveNJuISJbzOJYyxP9Tbljp8PwqbgYU="; }; dependencies = [ blessed wcwidth pyyaml + prettytable + requests ]; - nativeBuildInputs = [setuptools]; + nativeBuildInputs = [hatchling]; doCheck = false; + dontCheckRuntimeDeps = true; meta = with lib; { description = "Measures number of Terminal column cells of wide-character codes"; @@ -38,4 +44,4 @@ buildPythonPackage rec { license = licenses.mit; maintainers = []; }; -} +}) diff --git a/nix/pkgs/wcwidth.nix b/nix/pkgs/wcwidth.nix new file mode 100644 index 00000000000..4bbd1373b8e --- /dev/null +++ b/nix/pkgs/wcwidth.nix @@ -0,0 +1,27 @@ +{ + lib, + buildPythonPackage, + fetchPypi, + hatchling, +}: +buildPythonPackage rec { + pname = "wcwidth"; + version = "0.6.0"; + pyproject = true; + + src = fetchPypi { + inherit pname version; + hash = "sha256-zcTkJi1u+aGlfgGDhMvrEgjYq7xkF2An4sJFXIExMVk="; + }; + + build-system = [hatchling]; + + doCheck = false; + + meta = with lib; { + description = "Measures the displayed width of unicode strings in a terminal"; + homepage = "https://github.com/jquast/wcwidth"; + license = licenses.mit; + maintainers = []; + }; +} diff --git a/pkg/afl++/LICENSE b/pkg/afl++/LICENSE new file mode 100644 index 00000000000..5f5fc85e4de --- /dev/null +++ b/pkg/afl++/LICENSE @@ -0,0 +1,23 @@ +Based on zig-afl-kit: https://github.com/kristoff-it/zig-afl-kit + +MIT License + +Copyright (c) 2024 Loris Cro + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pkg/afl++/afl.c b/pkg/afl++/afl.c new file mode 100644 index 00000000000..61eb12c4aa7 --- /dev/null +++ b/pkg/afl++/afl.c @@ -0,0 +1,141 @@ +#include +#include +#include +#include +#include +#include +#include + +// AFL++ fuzzer harness for Zig fuzz targets. +// +// This file is the C "glue" that connects AFL++'s runtime to Zig-defined +// fuzz test functions. We can't use AFL++'s compiler wrappers (afl-clang, +// afl-gcc) because the code under test is compiled with Zig, so we manually +// expand the AFL macros (__AFL_INIT, __AFL_LOOP, __AFL_FUZZ_INIT, etc.) and +// wire up the sanitizer coverage symbols ourselves. + +// To ensure checks are not optimized out it is recommended to disable +// code optimization for the fuzzer harness main() +#pragma clang optimize off +#pragma GCC optimize("O0") + +// Zig-exported entry points. zig_fuzz_init() performs one-time setup and +// zig_fuzz_test() runs one fuzz iteration on the given input buffer. +// The Zig object should export these. +void zig_fuzz_init(); +void zig_fuzz_test(unsigned char*, size_t); + +// Linker-provided symbols marking the boundaries of the __sancov_guards +// section. These must be declared extern so the linker provides the actual +// section boundaries from the instrumented code, rather than creating new +// variables that shadow them. On macOS (Mach-O), the linker uses a different +// naming convention for section boundaries than Linux (ELF), so we use asm +// labels to reference them. +#ifdef __APPLE__ +extern uint32_t __start___sancov_guards __asm( + "section$start$__DATA$__sancov_guards"); +extern uint32_t __stop___sancov_guards __asm( + "section$end$__DATA$__sancov_guards"); +#else +extern uint32_t __start___sancov_guards; +extern uint32_t __stop___sancov_guards; +#endif + +// Provided by afl-compiler-rt; initializes the guard array used by +// SanitizerCoverage's trace-pc-guard instrumentation mode. +void __sanitizer_cov_trace_pc_guard_init(uint32_t*, uint32_t*); + +// Stubs for sanitizer coverage callbacks that the Zig-compiled code references +// but AFL's runtime (afl-compiler-rt) does not provide. Without these, linking +// would fail with undefined symbol errors. +__attribute__((visibility("default"))) __attribute__(( + tls_model("initial-exec"))) _Thread_local uintptr_t __sancov_lowest_stack; +void __sanitizer_cov_trace_pc_indir() {} +void __sanitizer_cov_8bit_counters_init() {} +void __sanitizer_cov_pcs_init() {} + +// Manual expansion of __AFL_FUZZ_INIT(). +// +// Enables shared-memory fuzzing: AFL++ writes test cases directly into +// shared memory (__afl_fuzz_ptr) instead of passing them via stdin, which +// is much faster. When not running under AFL++ (e.g. standalone execution), +// __afl_fuzz_ptr will be NULL and we fall back to reading from stdin into +// __afl_fuzz_alt (a 1 MB static buffer). +int __afl_sharedmem_fuzzing = 1; +extern __attribute__((visibility("default"))) unsigned int* __afl_fuzz_len; +extern __attribute__((visibility("default"))) unsigned char* __afl_fuzz_ptr; +unsigned char __afl_fuzz_alt[1048576]; +unsigned char* __afl_fuzz_alt_ptr = __afl_fuzz_alt; + +int main(int argc, char** argv) { + // Tell AFL's coverage runtime about our guard section so it can track + // which edges in the instrumented Zig code have been hit. + __sanitizer_cov_trace_pc_guard_init(&__start___sancov_guards, + &__stop___sancov_guards); + + // Manual expansion of __AFL_INIT() — deferred fork server mode. + // + // The magic string "##SIG_AFL_DEFER_FORKSRV##" is embedded in the binary + // so AFL++'s tooling can detect that this harness uses deferred fork + // server initialization. The `volatile` + `used` attributes prevent the + // compiler/linker from stripping it. We then call __afl_manual_init() to + // start the fork server at this point (after our setup) rather than at + // the very beginning of main(). + static volatile const char* _A __attribute__((used, unused)); + _A = (const char*)"##SIG_AFL_DEFER_FORKSRV##"; +#ifdef __APPLE__ + __attribute__((visibility("default"))) void _I(void) __asm__( + "___afl_manual_init"); +#else + __attribute__((visibility("default"))) void _I(void) __asm__( + "__afl_manual_init"); +#endif + _I(); + + zig_fuzz_init(); + + // Manual expansion of __AFL_FUZZ_TESTCASE_BUF. + // Use shared memory buffer if available, otherwise fall back to the + // static buffer (for standalone/non-AFL execution). + unsigned char* buf = __afl_fuzz_ptr ? __afl_fuzz_ptr : __afl_fuzz_alt_ptr; + + // Manual expansion of __AFL_LOOP(UINT_MAX) — persistent mode loop. + // + // Persistent mode keeps the process alive across many test cases instead + // of fork()'ing for each one, dramatically improving throughput. The magic + // string "##SIG_AFL_PERSISTENT##" signals to AFL++ that this binary + // supports persistent mode. __afl_persistent_loop() returns non-zero + // while there are more inputs to process. + // + // When connected to AFL++, we loop UINT_MAX times (essentially forever, + // AFL will restart us periodically). When running standalone, we loop + // once so the harness can be used for manual testing/reproduction. + while (({ + static volatile const char* _B __attribute__((used, unused)); + _B = (const char*)"##SIG_AFL_PERSISTENT##"; + extern __attribute__((visibility("default"))) int __afl_connected; +#ifdef __APPLE__ + __attribute__((visibility("default"))) int _L(unsigned int) __asm__( + "___afl_persistent_loop"); +#else + __attribute__((visibility("default"))) int _L(unsigned int) __asm__( + "__afl_persistent_loop"); +#endif + _L(__afl_connected ? UINT_MAX : 1); + })) { + // Manual expansion of __AFL_FUZZ_TESTCASE_LEN. + // In shared-memory mode, the length is provided directly by AFL++. + // In standalone mode, we read from stdin into the fallback buffer. + int len = + __afl_fuzz_ptr ? *__afl_fuzz_len + : (*__afl_fuzz_len = read(0, __afl_fuzz_alt_ptr, 1048576)) == 0xffffffff + ? 0 + : *__afl_fuzz_len; + + if (len >= 0) { + zig_fuzz_test(buf, len); + } + } + + return 0; +} diff --git a/pkg/afl++/build.zig b/pkg/afl++/build.zig new file mode 100644 index 00000000000..9de3ad01e56 --- /dev/null +++ b/pkg/afl++/build.zig @@ -0,0 +1,59 @@ +const std = @import("std"); + +/// Creates a build step that produces an AFL++-instrumented fuzzing +/// executable. +/// +/// Returns a `LazyPath` to the resulting fuzzing executable. +pub fn addInstrumentedExe( + b: *std.Build, + obj: *std.Build.Step.Compile, +) std.Build.LazyPath { + // Force the build system to produce the binary artifact even though we + // only consume the LLVM bitcode below. Without this, the dependency + // tracking doesn't wire up correctly. + _ = obj.getEmittedBin(); + + const pkg = b.dependencyFromBuildZig( + @This(), + .{}, + ); + + const afl_cc = b.addSystemCommand(&.{ + b.findProgram(&.{"afl-cc"}, &.{}) catch + @panic("Could not find 'afl-cc', which is required to build"), + "-O3", + }); + afl_cc.addArg("-o"); + const fuzz_exe = afl_cc.addOutputFileArg(obj.name); + afl_cc.addFileArg(pkg.path("afl.c")); + afl_cc.addFileArg(obj.getEmittedLlvmBc()); + return fuzz_exe; +} + +/// Creates a run step that invokes `afl-fuzz` with the given instrumented +/// executable, input corpus directory, and output directory. +/// +/// Returns the `Run` step so callers can wire it into a build step. +pub fn addFuzzerRun( + b: *std.Build, + exe: std.Build.LazyPath, + corpus_dir: std.Build.LazyPath, + output_dir: std.Build.LazyPath, +) *std.Build.Step.Run { + const run = b.addSystemCommand(&.{ + b.findProgram(&.{"afl-fuzz"}, &.{}) catch + @panic("Could not find 'afl-fuzz', which is required to run"), + "-i", + }); + run.addDirectoryArg(corpus_dir); + run.addArgs(&.{"-o"}); + run.addDirectoryArg(output_dir); + run.addArgs(&.{"--"}); + run.addFileArg(exe); + return run; +} + +// Required so `zig build` works although it does nothing. +pub fn build(b: *std.Build) !void { + _ = b; +} diff --git a/pkg/afl++/build.zig.zon b/pkg/afl++/build.zig.zon new file mode 100644 index 00000000000..1fd3d5a4b68 --- /dev/null +++ b/pkg/afl++/build.zig.zon @@ -0,0 +1,11 @@ +.{ + .name = .afl_plus_plus, + .fingerprint = 0x465bc4bebb188f16, + .version = "0.1.0", + .dependencies = .{}, + .paths = .{ + "build.zig", + "build.zig.zon", + "afl.c", + }, +} diff --git a/pkg/android-ndk/build.zig b/pkg/android-ndk/build.zig new file mode 100644 index 00000000000..5b005665bdc --- /dev/null +++ b/pkg/android-ndk/build.zig @@ -0,0 +1,207 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +pub fn build(_: *std.Build) !void {} + +// Configure the step to point to the Android NDK for libc and include +// paths. This requires the Android NDK installed in the system and +// setting the appropriate environment variables or installing the NDK +// in the default location. +// +// The environment variables can be set as follows: +// - `ANDROID_NDK_HOME`: Directly points to the NDK path, including the version. +// - `ANDROID_HOME` or `ANDROID_SDK_ROOT`: Points to the Android SDK path; +// latest available NDK will be automatically selected. +// +// NB: This is a workaround until zig natively supports bionic +// cross-compilation (ziglang/zig#23906). +pub fn addPaths(b: *std.Build, step: *std.Build.Step.Compile) !void { + const Cache = struct { + const Key = struct { + arch: std.Target.Cpu.Arch, + abi: std.Target.Abi, + api_level: u32, + }; + + var map: std.AutoHashMapUnmanaged(Key, ?struct { + libc: std.Build.LazyPath, + cpp_include: std.Build.LazyPath, + lib: std.Build.LazyPath, + }) = .empty; + }; + + const target = step.rootModuleTarget(); + const gop = try Cache.map.getOrPut(b.allocator, .{ + .arch = target.cpu.arch, + .abi = target.abi, + .api_level = target.os.version_range.linux.android, + }); + + if (!gop.found_existing) { + const ndk_path = findNDKPath(b) orelse return error.AndroidNDKNotFound; + + const ndk_triple = ndkTriple(target) orelse { + gop.value_ptr.* = null; + return error.AndroidNDKUnsupportedTarget; + }; + + const host = hostTag() orelse { + gop.value_ptr.* = null; + return error.AndroidNDKUnsupportedHost; + }; + + const sysroot = b.pathJoin(&.{ + ndk_path, + "toolchains", + "llvm", + "prebuilt", + host, + "sysroot", + }); + const include_dir = b.pathJoin(&.{ + sysroot, + "usr", + "include", + }); + const sys_include_dir = b.pathJoin(&.{ + sysroot, + "usr", + "include", + ndk_triple, + }); + const c_runtime_dir = b.pathJoin(&.{ + sysroot, + "usr", + "lib", + ndk_triple, + b.fmt("{d}", .{target.os.version_range.linux.android}), + }); + const lib = b.pathJoin(&.{ + sysroot, + "usr", + "lib", + ndk_triple, + }); + const cpp_include = b.pathJoin(&.{ + sysroot, + "usr", + "include", + "c++", + "v1", + }); + + const libc_txt = b.fmt( + \\include_dir={s} + \\sys_include_dir={s} + \\crt_dir={s} + \\msvc_lib_dir= + \\kernel32_lib_dir= + \\gcc_dir= + , .{ include_dir, sys_include_dir, c_runtime_dir }); + + const wf = b.addWriteFiles(); + const libc_path = wf.add("libc.txt", libc_txt); + + gop.value_ptr.* = .{ + .libc = libc_path, + .cpp_include = .{ .cwd_relative = cpp_include }, + .lib = .{ .cwd_relative = lib }, + }; + } + + const value = gop.value_ptr.* orelse return error.AndroidNDKNotFound; + + step.setLibCFile(value.libc); + step.root_module.addSystemIncludePath(value.cpp_include); + step.root_module.addLibraryPath(value.lib); +} + +fn findNDKPath(b: *std.Build) ?[]const u8 { + // Check if user has set the environment variable for the NDK path. + if (std.process.getEnvVarOwned(b.allocator, "ANDROID_NDK_HOME") catch null) |value| { + if (value.len == 0) return null; + var dir = std.fs.openDirAbsolute(value, .{}) catch return null; + defer dir.close(); + return value; + } + + // Check the common environment variables for the Android SDK path and look for the NDK inside it. + inline for (.{ "ANDROID_HOME", "ANDROID_SDK_ROOT" }) |env| { + if (std.process.getEnvVarOwned(b.allocator, env) catch null) |sdk| { + if (sdk.len > 0) { + if (findLatestNDK(b, sdk)) |ndk| return ndk; + } + } + } + + // As a fallback, we assume the most common/default SDK path based on the OS. + const home = std.process.getEnvVarOwned( + b.allocator, + if (builtin.os.tag == .windows) "LOCALAPPDATA" else "HOME", + ) catch return null; + + const default_sdk_path = b.pathJoin( + &.{ + home, + switch (builtin.os.tag) { + .linux => "Android/sdk", + .macos => "Library/Android/Sdk", + .windows => "Android/Sdk", + else => return null, + }, + }, + ); + + return findLatestNDK(b, default_sdk_path); +} + +fn findLatestNDK(b: *std.Build, sdk_path: []const u8) ?[]const u8 { + const ndk_dir = b.pathJoin(&.{ sdk_path, "ndk" }); + var dir = std.fs.openDirAbsolute(ndk_dir, .{ .iterate = true }) catch return null; + defer dir.close(); + + var latest_: ?struct { + name: []const u8, + version: std.SemanticVersion, + } = null; + var iterator = dir.iterate(); + + while (iterator.next() catch null) |file| { + if (file.kind != .directory) continue; + const version = std.SemanticVersion.parse(file.name) catch continue; + if (latest_) |latest| { + if (version.order(latest.version) != .gt) continue; + } + latest_ = .{ + .name = file.name, + .version = version, + }; + } + + const latest = latest_ orelse return null; + + return b.pathJoin(&.{ sdk_path, "ndk", latest.name }); +} + +fn hostTag() ?[]const u8 { + return switch (builtin.os.tag) { + .linux => "linux-x86_64", + // All darwin hosts use the same prebuilt binaries + // (https://developer.android.com/ndk/guides/other_build_systems). + .macos => "darwin-x86_64", + .windows => "windows-x86_64", + else => null, + }; +} + +// We must map the target architecture to the corresponding NDK triple following the NDK +// documentation: https://android.googlesource.com/platform/ndk/+/master/docs/BuildSystemMaintainers.md#architectures +fn ndkTriple(target: std.Target) ?[]const u8 { + return switch (target.cpu.arch) { + .arm => "arm-linux-androideabi", + .aarch64 => "aarch64-linux-android", + .x86 => "i686-linux-android", + .x86_64 => "x86_64-linux-android", + else => null, + }; +} diff --git a/pkg/android-ndk/build.zig.zon b/pkg/android-ndk/build.zig.zon new file mode 100644 index 00000000000..eb0de68201e --- /dev/null +++ b/pkg/android-ndk/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .name = .android_ndk, + .version = "0.0.2", + .fingerprint = 0xee68d62c5a97b68b, + .dependencies = .{}, + .paths = .{ + "build.zig", + "build.zig.zon", + }, +} diff --git a/pkg/dcimgui/build.zig b/pkg/dcimgui/build.zig index ae907dac089..2a13898342a 100644 --- a/pkg/dcimgui/build.zig +++ b/pkg/dcimgui/build.zig @@ -56,6 +56,9 @@ pub fn build(b: *std.Build) !void { if (freetype) try flags.appendSlice(b.allocator, &.{ "-DIMGUI_ENABLE_FREETYPE=1", }); + if (backend_opengl3) try flags.appendSlice(b.allocator, &.{ + "-DZIGPKG_IMGUI_ENABLE_OPENGL3=1", + }); if (target.result.os.tag == .windows) { try flags.appendSlice(b.allocator, &.{ "-DIMGUI_IMPL_API=extern\t\"C\"\t__declspec(dllexport)", diff --git a/pkg/dcimgui/ext.cpp b/pkg/dcimgui/ext.cpp index d4732e0fa16..b686c07f4d0 100644 --- a/pkg/dcimgui/ext.cpp +++ b/pkg/dcimgui/ext.cpp @@ -27,4 +27,27 @@ CIMGUI_API void ImGuiStyle_ImGuiStyle(cimgui::ImGuiStyle* self) ::ImGuiStyle defaults; *reinterpret_cast<::ImGuiStyle*>(self) = defaults; } + +// Perform the OpenGL3 backend shutdown and then zero out the imgl3w +// function pointer table. ImGui_ImplOpenGL3_Shutdown() calls +// imgl3wShutdown() which dlcloses the GL library handles but does not +// zero out the function pointers. A subsequent ImGui_ImplOpenGL3_Init() +// sees the stale (non-null) pointers, skips loader re-initialization, +// and crashes when calling through them. Zeroing the table forces the +// next Init to reload the GL function pointers via imgl3wInit(). +#ifndef IMGUI_DISABLE +#if __has_include("backends/imgui_impl_opengl3.h") +#ifdef ZIGPKG_IMGUI_ENABLE_OPENGL3 +#include "backends/imgui_impl_opengl3.h" +#include "backends/imgui_impl_opengl3_loader.h" + +CIMGUI_API void ImGui_ImplOpenGL3_ShutdownWithLoaderCleanup() +{ + ::ImGui_ImplOpenGL3_Shutdown(); + memset(&imgl3wProcs, 0, sizeof(imgl3wProcs)); +} +#endif // ZIGPKG_IMGUI_ENABLE_OPENGL3 +#endif // __has_include("backends/imgui_impl_opengl3.h") +#endif // IMGUI_DISABLE + } diff --git a/pkg/dcimgui/main.zig b/pkg/dcimgui/main.zig index 59bfca4f27b..40a4325c052 100644 --- a/pkg/dcimgui/main.zig +++ b/pkg/dcimgui/main.zig @@ -16,6 +16,10 @@ pub extern fn ImGui_ImplOpenGL3_Shutdown() callconv(.c) void; pub extern fn ImGui_ImplOpenGL3_NewFrame() callconv(.c) void; pub extern fn ImGui_ImplOpenGL3_RenderDrawData(draw_data: *c.ImDrawData) callconv(.c) void; +// Extension: shutdown the OpenGL3 backend and zero out the imgl3w function +// pointer table so a subsequent Init can re-initialize the loader. +pub extern fn ImGui_ImplOpenGL3_ShutdownWithLoaderCleanup() callconv(.c) void; + // Metal backend pub extern fn ImGui_ImplMetal_Init(device: *anyopaque) callconv(.c) bool; pub extern fn ImGui_ImplMetal_Shutdown() callconv(.c) void; diff --git a/pkg/highway/build.zig b/pkg/highway/build.zig index 3715baf4a54..b6e188b13ae 100644 --- a/pkg/highway/build.zig +++ b/pkg/highway/build.zig @@ -31,6 +31,11 @@ pub fn build(b: *std.Build) !void { try apple_sdk.addPaths(b, lib); } + if (target.result.abi.isAndroid()) { + const android_ndk = @import("android_ndk"); + try android_ndk.addPaths(b, lib); + } + var flags: std.ArrayList([]const u8) = .empty; defer flags.deinit(b.allocator); try flags.appendSlice(b.allocator, &.{ diff --git a/pkg/highway/build.zig.zon b/pkg/highway/build.zig.zon index 0777fcb7a9b..4870d1db53f 100644 --- a/pkg/highway/build.zig.zon +++ b/pkg/highway/build.zig.zon @@ -12,5 +12,6 @@ }, .apple_sdk = .{ .path = "../apple-sdk" }, + .android_ndk = .{ .path = "../android-ndk" }, }, } diff --git a/pkg/macos/animation.zig b/pkg/macos/animation.zig index 247f9760572..54bd3e20db1 100644 --- a/pkg/macos/animation.zig +++ b/pkg/macos/animation.zig @@ -4,6 +4,7 @@ pub const c = @import("animation/c.zig").c; pub extern "c" const kCAGravityTopLeft: *anyopaque; pub extern "c" const kCAGravityBottomLeft: *anyopaque; pub extern "c" const kCAGravityCenter: *anyopaque; +pub extern "c" const kCAGravityResize: *anyopaque; test { @import("std").testing.refAllDecls(@This()); diff --git a/pkg/oniguruma/main.zig b/pkg/oniguruma/main.zig index a8e415cfb39..2541cc35859 100644 --- a/pkg/oniguruma/main.zig +++ b/pkg/oniguruma/main.zig @@ -1,4 +1,5 @@ const initpkg = @import("init.zig"); +const match_param = @import("match_param.zig"); const regex = @import("regex.zig"); const region = @import("region.zig"); const types = @import("types.zig"); @@ -10,6 +11,7 @@ pub const errors = @import("errors.zig"); pub const init = initpkg.init; pub const deinit = initpkg.deinit; pub const Encoding = types.Encoding; +pub const MatchParam = match_param.MatchParam; pub const Regex = regex.Regex; pub const Region = region.Region; pub const Syntax = types.Syntax; diff --git a/pkg/oniguruma/match_param.zig b/pkg/oniguruma/match_param.zig new file mode 100644 index 00000000000..b28258ff08e --- /dev/null +++ b/pkg/oniguruma/match_param.zig @@ -0,0 +1,23 @@ +const c = @import("c.zig").c; +const errors = @import("errors.zig"); +const Error = errors.Error; + +pub const MatchParam = struct { + value: *c.OnigMatchParam, + + pub fn init() !MatchParam { + const value = c.onig_new_match_param() orelse return Error.Memory; + return .{ .value = value }; + } + + pub fn deinit(self: *MatchParam) void { + c.onig_free_match_param(self.value); + } + + pub fn setRetryLimitInSearch(self: *MatchParam, limit: usize) !void { + _ = try errors.convertError(c.onig_set_retry_limit_in_search_of_match_param( + self.value, + @intCast(limit), + )); + } +}; diff --git a/pkg/oniguruma/regex.zig b/pkg/oniguruma/regex.zig index a73c7fc1059..fd920e01af9 100644 --- a/pkg/oniguruma/regex.zig +++ b/pkg/oniguruma/regex.zig @@ -3,6 +3,7 @@ const c = @import("c.zig").c; const types = @import("types.zig"); const errors = @import("errors.zig"); const testEnsureInit = @import("testing.zig").ensureInit; +const MatchParam = @import("match_param.zig").MatchParam; const Region = @import("region.zig").Region; const Error = errors.Error; const ErrorInfo = errors.ErrorInfo; @@ -43,6 +44,17 @@ pub const Regex = struct { self: *Regex, str: []const u8, options: Option, + ) !Region { + return self.searchWithParam(str, options, null); + } + + /// Search an entire string for matches. This always returns a region + /// which may heap allocate (C allocator). + pub fn searchWithParam( + self: *Regex, + str: []const u8, + options: Option, + match_param: ?*MatchParam, ) !Region { var region: Region = .{}; @@ -51,7 +63,14 @@ pub const Regex = struct { // any errors to free that memory. errdefer region.deinit(); - _ = try self.searchAdvanced(str, 0, str.len, ®ion, options); + _ = try self.searchAdvancedWithParam( + str, + 0, + str.len, + ®ion, + options, + match_param, + ); return region; } @@ -64,15 +83,47 @@ pub const Regex = struct { region: *Region, options: Option, ) !usize { - const pos = try errors.convertError(c.onig_search( - self.value, - str.ptr, - str.ptr + str.len, - str.ptr + start, - str.ptr + end, - @ptrCast(region), - options.int(), - )); + return self.searchAdvancedWithParam( + str, + start, + end, + region, + options, + null, + ); + } + + /// onig_search_with_param directly + pub fn searchAdvancedWithParam( + self: *Regex, + str: []const u8, + start: usize, + end: usize, + region: *Region, + options: Option, + match_param: ?*MatchParam, + ) !usize { + const pos = try errors.convertError(if (match_param) |param| + c.onig_search_with_param( + self.value, + str.ptr, + str.ptr + str.len, + str.ptr + start, + str.ptr + end, + @ptrCast(region), + options.int(), + param.value, + ) + else + c.onig_search( + self.value, + str.ptr, + str.ptr + str.len, + str.ptr + start, + str.ptr + end, + @ptrCast(region), + options.int(), + )); return @intCast(pos); } @@ -90,4 +141,12 @@ test { try testing.expectEqual(@as(usize, 1), reg.count()); try testing.expectError(Error.Mismatch, re.search("hello", .{})); + + var match_param = try MatchParam.init(); + defer match_param.deinit(); + try match_param.setRetryLimitInSearch(1000); + + var reg_param = try re.searchWithParam("hello foo bar", .{}, &match_param); + defer reg_param.deinit(); + try testing.expectEqual(@as(usize, 1), reg_param.count()); } diff --git a/pkg/simdutf/build.zig b/pkg/simdutf/build.zig index 3123cab2125..8dcd141c1f9 100644 --- a/pkg/simdutf/build.zig +++ b/pkg/simdutf/build.zig @@ -20,6 +20,11 @@ pub fn build(b: *std.Build) !void { try apple_sdk.addPaths(b, lib); } + if (target.result.abi.isAndroid()) { + const android_ndk = @import("android_ndk"); + try android_ndk.addPaths(b, lib); + } + var flags: std.ArrayList([]const u8) = .empty; defer flags.deinit(b.allocator); // Zig 0.13 bug: https://github.com/ziglang/zig/issues/20414 diff --git a/pkg/simdutf/build.zig.zon b/pkg/simdutf/build.zig.zon index cd81c841ee3..afbef541821 100644 --- a/pkg/simdutf/build.zig.zon +++ b/pkg/simdutf/build.zig.zon @@ -5,5 +5,6 @@ .paths = .{""}, .dependencies = .{ .apple_sdk = .{ .path = "../apple-sdk" }, + .android_ndk = .{ .path = "../android-ndk" }, }, } diff --git a/pkg/utfcpp/build.zig b/pkg/utfcpp/build.zig index e06813b8353..08efb4ac862 100644 --- a/pkg/utfcpp/build.zig +++ b/pkg/utfcpp/build.zig @@ -19,6 +19,11 @@ pub fn build(b: *std.Build) !void { try apple_sdk.addPaths(b, lib); } + if (target.result.abi.isAndroid()) { + const android_ndk = @import("android_ndk"); + try android_ndk.addPaths(b, lib); + } + var flags: std.ArrayList([]const u8) = .empty; defer flags.deinit(b.allocator); diff --git a/pkg/utfcpp/build.zig.zon b/pkg/utfcpp/build.zig.zon index eff395a60e2..1077e9655b8 100644 --- a/pkg/utfcpp/build.zig.zon +++ b/pkg/utfcpp/build.zig.zon @@ -12,5 +12,6 @@ }, .apple_sdk = .{ .path = "../apple-sdk" }, + .android_ndk = .{ .path = "../android-ndk" }, }, } diff --git a/po/README_TRANSLATORS.md b/po/README_TRANSLATORS.md index 25b7cab5bdc..b985e067801 100644 --- a/po/README_TRANSLATORS.md +++ b/po/README_TRANSLATORS.md @@ -35,20 +35,31 @@ Written by Ulrich Drepper. With this, you're ready to localize! -## Editing translation files +## Locale names + +A locale name always consists of a [two letter language +code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) (e.g. +`de`, `es`, `fr`). Sometimes, for languages that have regional variations +(such as `zh` and `es`), the locale name includes a [two letter +country code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). +One example is `es_AR` for Spanish as spoken in Argentina. + +Full locale names are more complicated, but Ghostty does not use all parts. [The +`gettext` documentation](https://www.gnu.org/software/gettext/manual/gettext.html#Locale-Names-1) +has more information on locale names. + +## Translation file names All translation files lie in the `po/` directory, including the main _template_ file called `com.mitchellh.ghostty.pot`. **Do not edit this file.** The -template is generated automatically from Ghostty's code and resources, and are +template is generated automatically from Ghostty's code and resources, and is intended to be regenerated by code contributors. If there is a problem with the template file, please reach out to a code contributor. -Instead, only edit the translation file corresponding to your language/locale, -identified via its _locale name_: for example, `de_DE.UTF-8.po` would be the -translation file for German (language code `de`) as spoken in Germany (country -code `DE`). The GNU `gettext` manual contains -[further information about locale names](https://www.gnu.org/software/gettext/manual/gettext.html#Locale-Names-1), -including a list of language and country codes. +Translation file names consist of the locale name and the extension +`.po`. For example: `de.po`, `zh_CN.po`. + +## Editing translation files > [!NOTE] > @@ -56,7 +67,7 @@ including a list of language and country codes. > ["Creating new translation files" section](#creating-new-translation-files) > of this document on how to create one. -The `.po` file contains a list of entries that look like this: +The translation file contains a list of entries that look like this: ```po #. Translators: the category in the right-click context menu that contains split items for all directions @@ -86,84 +97,120 @@ Lines beginning with `#` are comments, of which there are several kinds: affect translators in other locales. The first entry of the `.po` file has an empty `msgid`. This entry is special -as it stores the metadata related to the `.po` file itself. You usually do -not need to modify it. +as it stores the metadata related to the `.po` file itself. You should update +`PO-Revision-Date` and `Last-Translator` once you have finished your edits, but +you normally do not need to modify other metadata. ## Creating new translation files You can use the `msginit` tool to create new translation files. -Run the command below, optionally replacing `$LANG` with the name of a locale -that is _different_ to your system locale, or if the `LANG` environmental -variable is not set. +Run the command below, replacing `X` with your [locale name](#locale-names). ```console -$ msginit -i po/com.mitchellh.ghostty.pot -l $LANG -o "po/$LANG.po" +$ msginit -i po/com.mitchellh.ghostty.pot -l X -o "po/X.po" ``` -> [!NOTE] -> -> Ghostty enforces the convention that all parts of the locale, including the -> language code, country code, encoding, and possible regional variants -> **must** be communicated in the file name. Files like `pt.po` are not -> acceptable, while `pt_BR.UTF-8.po` is. -> -> This is to allow us to more easily accommodate regional variants of a -> language in the future, and to reject translations that may not be applicable -> to all speakers of a language (e.g. an unqualified `zh.po` may contain -> terminology specific to Chinese speakers in Mainland China, which are not -> found in Taiwan. Using `zh_CN.UTF-8.po` would allow that difference to be -> communicated.) - -> [!WARNING] -> -> **Make sure your selected locale uses the UTF-8 encoding, as it is the sole -> encoding supported by Ghostty and its dependencies.** -> -> For backwards compatibility reasons, some locales may default to a non-UTF-8 -> encoding when an encoding is not specified. For instance, `de_DE` defaults -> to using the legacy ISO-8859-1 encoding, which is incompatible with UTF-8. -> You need to manually instruct `msginit` to use UTF-8 in these instances, -> by appending `.UTF-8` to the end of the locale name (e.g. `de_DE.UTF-8`). - `msginit` may prompt you for other information such as your email address, which should be filled in accordingly. You can then add your translations within the newly created translation file. Afterwards, you need to update the list of known locales within Ghostty's -build system. To do so, open `src/os/i18n_locales.zig` and find the list -of locales after the comments, then add the full locale name into the list. +build system. To do so, open `src/os/i18n_locales.zig` and find the list of +locale names after the comments, then add your locale name into the list. -The order matters, so make sure to place your locale in the correct position. -Read the comments present in the file for more details on the order. If you're -unsure, place it at the end of the list. +The order matters, so make sure to place your locale name in the correct +position. Read the comments present in the file for more details on the order. +If you're unsure, place it at the end of the list. ```zig const locales = [_][]const u8{ - "zh_CN.UTF-8", - // <- Add your locale here (probably) + "zh_CN", + // <- Add your locale name here (probably) } ``` -You should then be able to run `zig build` and see your translations in action! +You should then be able to run `zig build run` and see your translations in +action! See the ["Viewing translations" section](#viewing-translations) below. -Before opening a pull request with the new translation file, you should also add -your locale to the `CODEOWNERS` file. Find the `# Localization` section near the -bottom and add a line like so (where `xx_YY` is your locale): +Before opening a pull request with the new translation file, you should also +update the `CODEOWNERS` file. This is described in more detail in the +["Localization teams" section](#localization-teams)—don't forget to read that +section before submitting a pull request! + +## Viewing translations + +> [!NOTE] +> The localization system is not yet implemented for macOS, so it is not +> possible to view your translations there. + +Simply run `zig build run`. Ghostty uses your system language by default; if +your translations are of the language of your system, use +`zig build run -- --language=X` (where `X` is your locale name). You can +alternatively set the `LANGUAGE` environment variable to your locale name. + +On some desktop environments, such as KDE Plasma, Ghostty uses server-side +decorations by default. This hides many strings from the UI, which is +undesirable when viewing your translations. You can force Ghostty to use +client-side decorations with `zig build run -- --window-decoration=client`. + +Some strings are present in multiple places! A notable example is the context +menus: the hamburger menu in the header bar duplicates many strings present in +the right click menu. + +## Localization teams + +Every locale has a localization team consisting of the locale's maintainers. +These maintainers review contributions to their locale's translations, and are +responsible for translating new strings when requested: occasionally, all locale +maintainers are pinged and requested to translate missing strings. + +The primary purposes of being a locale maintainer are a declaration of +_commitment_ to their upkeep, and being _informed_ of updates or update requests +of the translations, via GitHub's review requests or @mentions. + +So that future updates to a locale are possible, each localization team must +have at least two members. If you are introducing a new language, please +**consider volunteering** to be a part of the localization team, by mentioning +that you are willing to be a part of it in the pull request description! You, +and all reviewers, will be offered to join the locale team before the pull +request to add the new language is merged, but this denotes your dedication +upfront—for a pull request adding a new language to be merged, it needs at least +one review from a speaker of that language _and_ at least two localization team +members. No one is _required_ to join a localization team, even if they +introduced support for the language. + +### `CODEOWNERS` + +Localization teams are represented as teams in the Ghostty GitHub organization. +GitHub reads a `CODEOWNERS` file, which maps files to teams, to identify +relevant maintainers. When **introducing support for a language**, you should +add the `.po` file to `CODEOWNERS`. + +To do this, find the `# Localization` section near the bottom of the file, and +add a line like so: ```diff # Localization /po/README_TRANSLATORS.md @ghostty-org/localization /po/com.mitchellh.ghostty.pot @ghostty-org/localization - /po/zh_CN.UTF-8.po @ghostty-org/zh_CN -+/po/xx_YY.UTF-8.po @ghostty-org/xx_YY + /po/zh_CN.po @ghostty-org/zh_CN ++/po/X.po @ghostty-org/yy_ZZ ``` -## Style Guide +`X.po` here is the name of the translation file you created. Unlike the +translation file's name, localization team names **always include a language and +country code**; `yy` here is the _language code_, and `ZZ` is the _country +code_. + +When adding a new entry, try to keep the list in **alphabetical order** if +possible. + +## Style guide These are general style guidelines for translations. Naturally, the specific -recommended standards will differ based on the specific language/locale, -but these should serve as a baseline for the tone and voice of any translation. +recommended standards differ based on the specific language/locale, but these +should serve as a baseline for the tone and voice of any translation. - **Prefer an instructive, yet professional tone.** @@ -196,3 +243,45 @@ but these should serve as a baseline for the tone and voice of any translation. [GNOME Human Interface Guidelines](https://developer.gnome.org/hig/guidelines/writing-style.html) on Linux, and [Apple's Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/writing) on macOS. + +## Common issues + +Some mistakes are frequently made during translation. The most common ones are +listed below. + +### Unicode ellipses + +English source strings use the ellipses character, `…`, instead of three full +stops, `...`. If your language uses ellipses, use the ellipses character instead +of three full stops in your translations. You can copy this character from the +English source string itself. + +### Title case + +Title case is a feature of English writing where most words start with a capital +letter: This Clause Is Written In Title Case. It is commonly found in titles, +hence its name; however, English is one of the only languages that uses title +case. If your language does not use title case, **do not use title case for the +sake of copying the English source**. Please use the casing conventions of your +language instead. + +### `X-Generator` field + +Many `.po` file editors add an `X-Generator` field to the metadata section. +These should be removed as other translators might overwrite them when using +a different editor, and some (such as Poedit) update the line when a different +_version_ is used—this adds unnecessary changes to the diff. + +You can remove the `X-Generator` field by simply deleting that line from the +file with a plain text editor. + +### Updating metadata (revision date) + +It is very easy to overlook the `PO-Revision-Date` field in the metadata at the +top of the file. Please update this when you are done modifying the +translations! + +Depending on who last translated the file, the `Last-Translator` field might +also need updating: make sure it has your name and email. Finally, if your name +and email are not present in the copyright comment at the top of the file, +consider adding it there. diff --git a/po/bg.po b/po/bg.po new file mode 100644 index 00000000000..ac4e8c24cbc --- /dev/null +++ b/po/bg.po @@ -0,0 +1,355 @@ +# Bulgarian translations for com.mitchellh.ghostty package. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Damyan Bogoev , 2025. +# reo101 , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-02-09 22:07+0200\n" +"Last-Translator: reo101 \n" +"Language-Team: Bulgarian \n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Отвори в Ghostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Разрешаване на доÑтъп до клипборда" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Откажи" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Позволи" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Запомни избора за това разделÑне" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "За да покажеш това Ñъобщение отново, презареди конфигурациÑта" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Отказ" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Затвори" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Грешки в конфигурациÑта" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Открити Ñа една или повече грешки в конфигурациÑта. МолÑ, прегледайте " +"грешките по-долу и или презаредете конфигурациÑта Ñи, или ги игнорирайте." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Игнорирай" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Презареди конфигурациÑта" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ Ð˜Ð·Ð¿Ð¾Ð»Ð·Ð²Ð°Ñ‚Ðµ дебъг верÑÐ¸Ñ Ð½Ð° Ghostty! ПроизводителноÑтта ще бъде намалена." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: ИнÑпектор на терминала" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Ðамери…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Предишно Ñъвпадение" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "Следващо Ñъвпадение" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "О, не." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "ÐеуÑпешно придобиване на OpenGL контекÑÑ‚ за рендиране." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Този терминал е режим Ñамо за четене. Ð’Ñе още можете да преглеждате, " +"Ñелектирате и превъртате Ñъдържанието, но към работещото приложение нÑма да " +"бъдат изпращани входни ÑъбитиÑ." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Само за четене" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Копирай" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "ПоÑтави" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "УведомÑване при завършване на Ñледващата команда" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "ИзчиÑти" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Ðулирай" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Раздели" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Промени заглавие…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Раздели нагоре" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Раздели надолу" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Раздели налÑво" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Раздели надÑÑно" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Раздел" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Смени името на таба…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Ðов раздел" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Затвори раздел" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Прозорец" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Ðов прозорец" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Затвори прозорец" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "КонфигурациÑ" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Отвори конфигурациÑта" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "ОÑтавете празно за възÑтановÑване на заглавието по подразбиране." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "ОК" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Ðово разделÑне" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Преглед на отворените раздели" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Главно меню" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Командна палитра" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "ИнÑпектор на терминала" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "За Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Изход" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Изпълни команда…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Приложение Ñе опитва да запише в клипборда. Текущото Ñъдържание на клипборда " +"е показано по-долу." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Приложение Ñе опитва да чете от клипборда. Текущото Ñъдържание на клипборда " +"е показано по-долу." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Предупреждение: Потенциално опаÑно поÑтавÑне" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"ПоÑтавÑнето на този текÑÑ‚ в терминала може да е опаÑно, тъй като изглежда, " +"че може да бъдат изпълнени нÑкои команди." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Изход от Ghostty?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "ЗатварÑне на раздела?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "ЗатварÑне на прозореца?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "ЗатварÑне на разделÑнето?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Ð’Ñички терминални ÑеÑии ще бъдат прекратени." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Ð’Ñички терминални ÑеÑии в този раздел ще бъдат прекратени." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Ð’Ñички терминални ÑеÑии в този прозорец ще бъдат прекратени." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "ТекущиÑÑ‚ Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð² това разделÑне ще бъде прекратен." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Командата завърши" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Командата завърши уÑпешно" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Командата завърши неуÑпешно" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Командата завърши уÑпешно" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Командата завърши неуÑпешно" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "ПромÑна на заглавието на терминала" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Смени името на таба" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "КонфигурациÑта е презаредена" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Копирано в клипборда" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "Клипбордът е изчиÑтен" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Разработчици на Ghostty" diff --git a/po/bg_BG.UTF-8.po b/po/bg_BG.UTF-8.po deleted file mode 100644 index 3f7d55913aa..00000000000 --- a/po/bg_BG.UTF-8.po +++ /dev/null @@ -1,355 +0,0 @@ -# Bulgarian translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto -# This file is distributed under the same license as the com.mitchellh.ghostty package. -# Damyan Bogoev , 2025. -# reo101 , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-09 22:07+0200\n" -"Last-Translator: reo101 \n" -"Language-Team: Bulgarian \n" -"Language: bg\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Разрешаване на доÑтъп до клипборда" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "Откажи" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Позволи" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "Запомни избора за това разделÑне" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "За да покажеш това Ñъобщение отново, презареди конфигурациÑта" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "Отказ" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Затвори" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "Грешки в конфигурациÑта" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"Открити Ñа една или повече грешки в конфигурациÑта. МолÑ, прегледайте " -"грешките по-долу и или презаредете конфигурациÑта Ñи, или ги игнорирайте." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Игнорирай" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "Презареди конфигурациÑта" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"âš ï¸ Ð˜Ð·Ð¿Ð¾Ð»Ð·Ð²Ð°Ñ‚Ðµ дебъг верÑÐ¸Ñ Ð½Ð° Ghostty! ПроизводителноÑтта ще бъде намалена." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: ИнÑпектор на терминала" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "Ðамери…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "Предишно Ñъвпадение" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "Следващо Ñъвпадение" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "О, не." - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "ÐеуÑпешно придобиване на OpenGL контекÑÑ‚ за рендиране." - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"Този терминал е режим Ñамо за четене. Ð’Ñе още можете да преглеждате, " -"Ñелектирате и превъртате Ñъдържанието, но към работещото приложение нÑма да " -"бъдат изпращани входни ÑъбитиÑ." - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "Само за четене" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Копирай" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "ПоÑтави" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "УведомÑване при завършване на Ñледващата команда" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "ИзчиÑти" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "Ðулирай" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "Раздели" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "Промени заглавие…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Раздели нагоре" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "Раздели надолу" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Раздели налÑво" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "Раздели надÑÑно" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Раздел" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Ðов раздел" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Затвори раздел" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Прозорец" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Ðов прозорец" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Затвори прозорец" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "КонфигурациÑ" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Отвори конфигурациÑта" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "ОÑтавете празно за възÑтановÑване на заглавието по подразбиране." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "ОК" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Ðово разделÑне" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "Преглед на отворените раздели" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "Главно меню" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Командна палитра" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "ИнÑпектор на терминала" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "За Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "Изход" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "Изпълни команда…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Приложение Ñе опитва да запише в клипборда. Текущото Ñъдържание на клипборда " -"е показано по-долу." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Приложение Ñе опитва да чете от клипборда. Текущото Ñъдържание на клипборда " -"е показано по-долу." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Предупреждение: Потенциално опаÑно поÑтавÑне" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"ПоÑтавÑнето на този текÑÑ‚ в терминала може да е опаÑно, тъй като изглежда, " -"че може да бъдат изпълнени нÑкои команди." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Изход от Ghostty?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "ЗатварÑне на раздела?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "ЗатварÑне на прозореца?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "ЗатварÑне на разделÑнето?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Ð’Ñички терминални ÑеÑии ще бъдат прекратени." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Ð’Ñички терминални ÑеÑии в този раздел ще бъдат прекратени." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Ð’Ñички терминални ÑеÑии в този прозорец ще бъдат прекратени." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "ТекущиÑÑ‚ Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð² това разделÑне ще бъде прекратен." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "Командата завърши" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "Командата завърши уÑпешно" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "Командата завърши неуÑпешно" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Командата завърши уÑпешно" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Командата завърши неуÑпешно" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "ПромÑна на заглавието на терминала" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "КонфигурациÑта е презаредена" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "Копирано в клипборда" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "Клипбордът е изчиÑтен" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Разработчици на Ghostty" diff --git a/po/ca.po b/po/ca.po new file mode 100644 index 00000000000..0d97e9066f4 --- /dev/null +++ b/po/ca.po @@ -0,0 +1,357 @@ +# Catalan 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. +# Francesc Arpi , 2025. +# Kristofer Soler <31729650+KristoferSoler@users.noreply.github.com>, 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2025-08-24 19:22+0200\n" +"Last-Translator: Kristofer Soler " +"<31729650+KristoferSoler@users.noreply.github.com>\n" +"Language-Team: \n" +"Language: ca\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Obre amb Ghostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Autoritza l'accés al porta-retalls" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Denegar" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Permet" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Recorda l’opció per a aquest panell dividit" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "Recarrega la configuració per tornar a mostrar aquest missatge" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Cancel·la" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Tanca" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Errors de configuració" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"S'han trobat un o més errors de configuració. Si us plau, revisa els errors " +"a continuació i torna a carregar la configuració o ignora aquests errors." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Ignora" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Carrega la configuració" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ Estàs executant una versió de depuració de Ghostty! El rendiment es veurà " +"afectat." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspector de terminal" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Cerca…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Coincidència anterior" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "Coincidència següent" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "Oh, no." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "No s'ha pogut obtenir un context OpenGL per al renderitzat." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Aquest terminal és en mode de només lectura. Encara pots veure, seleccionar " +"i desplaçar-te pel contingut, però no s'enviaran esdeveniments d'entrada a " +"l'aplicació en execució." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Només lectura" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Copia" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Enganxa" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "Notifica en finalitzar la propera comanda" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "Neteja" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Reinicia" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Divideix" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Canvia el títol…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Divideix cap amunt" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Divideix cap avall" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Divideix a l'esquerra" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Divideix a la dreta" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Pestanya" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Canvia el títol de la pestanya…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Nova pestanya" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Tanca la pestanya" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Finestra" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Nova finestra" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Tanca la finestra" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "Configuració" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Obre la configuració" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "Deixa en blanc per restaurar el títol per defecte." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "D'acord" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Nova divisió" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Mostra les pestanyes obertes" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Menú principal" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Paleta de comandes" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "Inspector de terminal" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "Sobre Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Surt" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Executa una ordre…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Una aplicació està intentant escriure al porta-retalls. El contingut actual " +"del porta-retalls es mostra a continuació." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Una aplicació està intentant llegir del porta-retalls. El contingut actual " +"del porta-retalls es mostra a continuació." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Avís: Enganxament potencialment insegur" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Enganxar aquest text al terminal pot ser perillós, ja que sembla que es " +"podrien executar algunes ordres." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Surt de Ghostty?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "Tanca la pestanya?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "Tanca la finestra?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "Tanca la divisió?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Totes les sessions del terminal es tancaran." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Totes les sessions del terminal en aquesta pestanya es tancaran." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Totes les sessions del terminal en aquesta finestra es tancaran." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "El procés actualment en execució en aquesta divisió es tancarà." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Comanda finalitzada" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Comanda completada amb èxit" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Comanda fallida" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Comanda completada amb èxit" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Comanda fallida" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Canvia el títol del terminal" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Canvia el títol de la pestanya" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "S'ha tornat a carregar la configuració" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Copiat al porta-retalls" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "Porta-retalls netejat" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Desenvolupadors de Ghostty" diff --git a/po/ca_ES.UTF-8.po b/po/ca_ES.UTF-8.po deleted file mode 100644 index 1b3ba1a0e55..00000000000 --- a/po/ca_ES.UTF-8.po +++ /dev/null @@ -1,354 +0,0 @@ -# Catalan 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. -# Francesc Arpi , 2025. -# Kristofer Soler <31729650+KristoferSoler@users.noreply.github.com>, 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2025-08-24 19:22+0200\n" -"Last-Translator: Kristofer Soler " -"<31729650+KristoferSoler@users.noreply.github.com>\n" -"Language-Team: \n" -"Language: ca\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Autoritza l'accés al porta-retalls" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "Denegar" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Permet" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "Recorda l’opció per a aquest panell dividit" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "Recarrega la configuració per tornar a mostrar aquest missatge" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "Cancel·la" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Tanca" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "Errors de configuració" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"S'han trobat un o més errors de configuració. Si us plau, revisa els errors " -"a continuació i torna a carregar la configuració o ignora aquests errors." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Ignora" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "Carrega la configuració" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"âš ï¸ Estàs executant una versió de depuració de Ghostty! El rendiment es veurà " -"afectat." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Inspector de terminal" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Copia" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "Enganxa" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "Neteja" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "Reinicia" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "Divideix" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "Canvia el títol…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Divideix cap amunt" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "Divideix cap avall" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Divideix a l'esquerra" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "Divideix a la dreta" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Pestanya" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Nova pestanya" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Tanca la pestanya" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Finestra" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Nova finestra" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Tanca la finestra" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "Configuració" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Obre la configuració" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "Deixa en blanc per restaurar el títol per defecte." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "D'acord" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Nova divisió" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "Mostra les pestanyes obertes" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "Menú principal" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Paleta de comandes" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "Inspector de terminal" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "Sobre Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "Surt" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "Executa una ordre…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Una aplicació està intentant escriure al porta-retalls. El contingut actual " -"del porta-retalls es mostra a continuació." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Una aplicació està intentant llegir del porta-retalls. El contingut actual " -"del porta-retalls es mostra a continuació." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Avís: Enganxament potencialment insegur" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"Enganxar aquest text al terminal pot ser perillós, ja que sembla que es " -"podrien executar algunes ordres." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Surt de Ghostty?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "Tanca la pestanya?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "Tanca la finestra?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "Tanca la divisió?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Totes les sessions del terminal es tancaran." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Totes les sessions del terminal en aquesta pestanya es tancaran." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Totes les sessions del terminal en aquesta finestra es tancaran." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "El procés actualment en execució en aquesta divisió es tancarà." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Comanda completada amb èxit" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Comanda fallida" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "Canvia el títol del terminal" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "S'ha tornat a carregar la configuració" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "Copiat al porta-retalls" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "Porta-retalls netejat" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Desenvolupadors de Ghostty" diff --git a/po/de.po b/po/de.po new file mode 100644 index 00000000000..da6a08ebc20 --- /dev/null +++ b/po/de.po @@ -0,0 +1,360 @@ +# German translations for com.mitchellh.ghostty package +# German translation for com.mitchellh.ghostty. +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Robin Pfäffle , 2025. +# Jan Klass , 2026. +# Klaus Hipp , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-02-13 08:05+0100\n" +"Last-Translator: Klaus Hipp \n" +"Language-Team: German \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "In Ghostty öffnen" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Zugriff auf die Zwischenablage gewähren" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Nicht erlauben" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Erlauben" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Auswahl für dieses geteilte Fenster beibehalten" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "" +"Lade die Konfiguration erneut, um diese Eingabeaufforderung erneut anzuzeigen" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Abbrechen" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Schließen" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Konfigurationsfehler" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Ein oder mehrere Konfigurationsfehler wurden gefunden. Bitte überprüfe die " +"untenstehenden Fehler und lade entweder deine Konfiguration erneut oder " +"ignoriere die Fehler." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Ignorieren" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Konfiguration neu laden" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ Du verwendest einen Debug Build von Ghostty! Die Leistung wird reduziert " +"sein." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Terminalinspektor" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Suchen…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Vorherige Übereinstimmung" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "Nächste Übereinstimmung" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "Oh nein." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "Es kann kein OpenGL-Kontext für das Rendering abgerufen werden." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Dieses Terminal befindet sich im schreibgeschützten Modus. Du kannst den " +"Inhalt weiterhin anzeigen, auswählen und durchscrollen, es werden jedoch " +"keine Eingabeereignisse an die laufende Anwendung gesendet." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Schreibgeschützt" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Kopieren" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Einfügen" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "Bei Abschluss des nächsten Befehls benachrichtigen" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "Leeren" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Zurücksetzen" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Fenster teilen" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Titel bearbeiten…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Fenster nach oben teilen" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Fenster nach unten teilen" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Fenter nach links teilen" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Fenster nach rechts teilen" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Tab" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Tab-Titel ändern…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Neuer Tab" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Tab schließen" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Fenster" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Neues Fenster" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Fenster schließen" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "Konfiguration" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Konfiguration öffnen" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "Leer lassen, um den Standardtitel wiederherzustellen." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Neues geteiltes Fenster" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Offene Tabs einblenden" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Hauptmenü" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Befehlspalette" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "Terminalinspektor" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "Über Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Beenden" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Einen Befehl ausführen…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Eine Anwendung versucht in die Zwischenablage zu schreiben. Der aktuelle " +"Inhalt der Zwischenablage wird unten angezeigt." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Eine Anwendung versucht von der Zwischenablage zu lesen. Der aktuelle Inhalt " +"der Zwischenablage wird unten angezeigt." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Achtung: Möglicherweise unsicheres Einfügen" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Diesen Text in das Terminal einzufügen könnte möglicherweise gefährlich " +"sein. Es scheint, dass Anweisungen ausgeführt werden könnten." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Ghostty schließen?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "Tab schließen?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "Fenster schließen?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "Geteiltes Fenster schließen?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Alle Terminalsitzungen werden beendet." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Alle Terminalsitzungen in diesem Tab werden beendet." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Alle Terminalsitzungen in diesem Fenster werden beendet." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "Der aktuell laufende Prozess in diesem geteilten Fenster wird beendet." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Befehl abgeschlossen" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Befehl erfolgreich" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Befehl fehlgeschlagen" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Befehl erfolgreich" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Befehl fehlgeschlagen" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Terminaltitel bearbeiten" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Tab-Titel ändern" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "Konfiguration wurde neu geladen" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "In die Zwischenablage kopiert" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "Zwischenablage geleert" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Ghostty-Entwickler" diff --git a/po/de_DE.UTF-8.po b/po/de_DE.UTF-8.po deleted file mode 100644 index 6e7d5039393..00000000000 --- a/po/de_DE.UTF-8.po +++ /dev/null @@ -1,360 +0,0 @@ -# German translations for com.mitchellh.ghostty package -# German translation for com.mitchellh.ghostty. -# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors -# This file is distributed under the same license as the com.mitchellh.ghostty package. -# Robin Pfäffle , 2025. -# Jan Klass , 2026. -# Klaus Hipp , 2026. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-13 08:05+0100\n" -"Last-Translator: Klaus Hipp \n" -"Language-Team: German \n" -"Language: de\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Zugriff auf die Zwischenablage gewähren" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "Nicht erlauben" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Erlauben" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "Auswahl für dieses geteilte Fenster beibehalten" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "" -"Lade die Konfiguration erneut, um diese Eingabeaufforderung erneut anzuzeigen" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "Abbrechen" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Schließen" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "Konfigurationsfehler" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"Ein oder mehrere Konfigurationsfehler wurden gefunden. Bitte überprüfe die " -"untenstehenden Fehler und lade entweder deine Konfiguration erneut oder " -"ignoriere die Fehler." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Ignorieren" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "Konfiguration neu laden" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"âš ï¸ Du verwendest einen Debug Build von Ghostty! Die Leistung wird reduziert " -"sein." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Terminalinspektor" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "Suchen…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "Vorherige Übereinstimmung" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "Nächste Übereinstimmung" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "Oh nein." - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "Es kann kein OpenGL-Kontext für das Rendering abgerufen werden." - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"Dieses Terminal befindet sich im schreibgeschützten Modus. Du kannst den " -"Inhalt weiterhin anzeigen, auswählen und durchscrollen, es werden jedoch " -"keine Eingabeereignisse an die laufende Anwendung gesendet." - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "Schreibgeschützt" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Kopieren" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "Einfügen" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "Bei Abschluss des nächsten Befehls benachrichtigen" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "Leeren" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "Zurücksetzen" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "Fenster teilen" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "Titel bearbeiten…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Fenster nach oben teilen" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "Fenster nach unten teilen" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Fenter nach links teilen" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "Fenster nach rechts teilen" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Tab" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Neuer Tab" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Tab schließen" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Fenster" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Neues Fenster" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Fenster schließen" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "Konfiguration" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Konfiguration öffnen" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "Leer lassen, um den Standardtitel wiederherzustellen." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "OK" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Neues geteiltes Fenster" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "Offene Tabs einblenden" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "Hauptmenü" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Befehlspalette" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "Terminalinspektor" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "Über Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "Beenden" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "Einen Befehl ausführen…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Eine Anwendung versucht in die Zwischenablage zu schreiben. Der aktuelle " -"Inhalt der Zwischenablage wird unten angezeigt." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Eine Anwendung versucht von der Zwischenablage zu lesen. Der aktuelle Inhalt " -"der Zwischenablage wird unten angezeigt." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Achtung: Möglicherweise unsicheres Einfügen" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"Diesen Text in das Terminal einzufügen könnte möglicherweise gefährlich " -"sein. Es scheint, dass Anweisungen ausgeführt werden könnten." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Ghostty schließen?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "Tab schließen?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "Fenster schließen?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "Geteiltes Fenster schließen?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Alle Terminalsitzungen werden beendet." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Alle Terminalsitzungen in diesem Tab werden beendet." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Alle Terminalsitzungen in diesem Fenster werden beendet." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "Der aktuell laufende Prozess in diesem geteilten Fenster wird beendet." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "Befehl abgeschlossen" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "Befehl erfolgreich" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "Befehl fehlgeschlagen" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Befehl erfolgreich" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Befehl fehlgeschlagen" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "Terminaltitel bearbeiten" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "Konfiguration wurde neu geladen" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "In die Zwischenablage kopiert" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "Zwischenablage geleert" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Ghostty-Entwickler" diff --git a/po/es_AR.UTF-8.po b/po/es_AR.UTF-8.po deleted file mode 100644 index 930d8ada5c9..00000000000 --- a/po/es_AR.UTF-8.po +++ /dev/null @@ -1,355 +0,0 @@ -# Spanish translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto -# This file is distributed under the same license as the com.mitchellh.ghostty package. -# Alan Moyano , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-09 17:50-0300\n" -"Last-Translator: Alan Moyano \n" -"Language-Team: Argentinian \n" -"Language: es_AR\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Autorizar acceso al portapapeles" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "Denegar" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Permitir" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "Recordar elección para esta división" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "Recargar la configuración para volver a mostrar este mensaje" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "Cancelar" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Cerrar" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "Errores de configuración" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"Se encontraron uno o más errores de configuración. Por favor revisá los " -"errores a continuación, y recargá tu configuración o ignorá estos errores." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Ignorar" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "Recargar configuración" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"âš ï¸ Estás ejecutando una versión de depuración de Ghostty. El rendimiento no " -"será óptimo." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Inspector de la terminal" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "Buscar…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "Coincidencia anterior" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "Coincidencia siguiente" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "Uy, no." - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "No se puedo obtener un contexto de OpenGL para el renderizado" - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"Esta terminal está en modo solo lectura. Aún puedes ver, seleccionar y " -"desplazarte por el contenido, pero no se enviarán los eventos de entrada a " -"la aplicación en ejecución." - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "Solo lectura" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Copiar" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "Pegar" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "Notificar al finalizar el siguiente comando" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "Limpiar" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "Reiniciar" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "Dividir" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "Cambiar título…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Dividir arriba" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "Dividir abajo" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Dividir a la izquierda" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "Dividir a la derecha" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Pestaña" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Nueva pestaña" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Cerrar pestaña" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Ventana" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Nueva ventana" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Cerrar ventana" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "Configuración" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Abrir configuración" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "Dejar en blanco para restaurar el título predeterminado." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "Aceptar" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Nueva división" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "Ver pestañas abiertas" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "Menú principal" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Paleta de comandos" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "Inspector de la terminal" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "Acerca de Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "Salir" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "Ejecutar un comando…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Una aplicación está intentando escribir en el portapapeles. El contenido " -"actual del portapapeles se muestra a continuación." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Una aplicación está intentando leer desde el portapapeles. El contenido " -"actual del portapapeles se muestra a continuación." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Advertencia: Pegado potencialmente inseguro" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"Pegar este texto en la terminal puede ser peligroso ya que parece que " -"algunos comandos podrían ejecutarse." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "¿Salir de Ghostty?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "¿Cerrar pestaña?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "¿Cerrar ventana?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "¿Cerrar división?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Todas las sesiones de terminal serán terminadas." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Todas las sesiones de terminal en esta pestaña serán terminadas." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Todas las sesiones de terminal en esta ventana serán terminadas." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "El proceso actualmente en ejecución en esta división será terminado." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "Comando finalizado" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "Comando ejecutado correctamente" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "Comando fallido" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Comando ejecutado correctamente" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Comando fallido" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "Cambiar el título de la terminal" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "Configuración recargada" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "Copiado al portapapeles" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "Portapapeles limpiado" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Desarrolladores de Ghostty" diff --git a/po/es_AR.po b/po/es_AR.po new file mode 100644 index 00000000000..a5dd60d02a8 --- /dev/null +++ b/po/es_AR.po @@ -0,0 +1,355 @@ +# Spanish translations for com.mitchellh.ghostty package. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Alan Moyano , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-02-19 13:34-0300\n" +"Last-Translator: Alan Moyano \n" +"Language-Team: Argentinian \n" +"Language: es_AR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Abrir en Ghostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Autorizar acceso al portapapeles" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Denegar" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Permitir" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Recordar elección para esta división" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "Recargar la configuración para volver a mostrar este mensaje" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Cancelar" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Cerrar" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Errores de configuración" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Se encontraron uno o más errores de configuración. Por favor revisá los " +"errores a continuación, y recargá tu configuración o ignorá estos errores." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Ignorar" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Recargar configuración" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ Estás ejecutando una versión de depuración de Ghostty. El rendimiento no " +"será óptimo." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspector de la terminal" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Buscar…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Coincidencia anterior" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "Coincidencia siguiente" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "Uy, no." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "No se pudo obtener un contexto de OpenGL para el renderizado" + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Esta terminal está en modo solo lectura. Aún puedes ver, seleccionar y " +"desplazarte por el contenido, pero no se enviarán los eventos de entrada a " +"la aplicación en ejecución." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Solo lectura" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Copiar" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Pegar" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "Notificar al finalizar el siguiente comando" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "Limpiar" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Reiniciar" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Dividir" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Cambiar título…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Dividir arriba" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Dividir abajo" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Dividir a la izquierda" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Dividir a la derecha" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Pestaña" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Cambiar título de la pestaña…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Nueva pestaña" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Cerrar pestaña" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Ventana" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Nueva ventana" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Cerrar ventana" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "Configuración" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Abrir configuración" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "Dejar en blanco para restaurar el título predeterminado." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "Aceptar" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Nueva división" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Ver pestañas abiertas" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Menú principal" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Paleta de comandos" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "Inspector de la terminal" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "Acerca de Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Salir" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Ejecutar un comando…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Una aplicación está intentando escribir en el portapapeles. El contenido " +"actual del portapapeles se muestra a continuación." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Una aplicación está intentando leer desde el portapapeles. El contenido " +"actual del portapapeles se muestra a continuación." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Advertencia: Pegado potencialmente inseguro" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Pegar este texto en la terminal puede ser peligroso ya que parece que " +"algunos comandos podrían ejecutarse." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "¿Salir de Ghostty?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "¿Cerrar pestaña?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "¿Cerrar ventana?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "¿Cerrar división?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Todas las sesiones de terminal serán terminadas." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Todas las sesiones de terminal en esta pestaña serán terminadas." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Todas las sesiones de terminal en esta ventana serán terminadas." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "El proceso actualmente en ejecución en esta división será terminado." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Comando finalizado" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Comando ejecutado correctamente" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Comando fallido" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Comando ejecutado correctamente" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Comando fallido" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Cambiar título de la terminal" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Cambiar título de la pestaña" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "Configuración recargada" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Copiado al portapapeles" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "Portapapeles limpiado" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Desarrolladores de Ghostty" diff --git a/po/es_BO.UTF-8.po b/po/es_BO.UTF-8.po deleted file mode 100644 index 7f103f960ae..00000000000 --- a/po/es_BO.UTF-8.po +++ /dev/null @@ -1,355 +0,0 @@ -# Spanish 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. -# Miguel Peredo , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-12 17:46+0200\n" -"Last-Translator: Miguel Peredo \n" -"Language-Team: Spanish \n" -"Language: es_BO\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Autorizar acceso al portapapeles" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "Denegar" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Permitir" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "Recordar su elección para esta división de ventana" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "Recargar configuración para mostrar este aviso nuevamente" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "Cancelar" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Cerrar" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "Errores de configuración" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"Se encontraron uno o más errores de configuración. Por favor revise los " -"errores a continuación, y recargue su configuración o ignore estos errores." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Ignorar" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "Recargar configuración" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"âš ï¸ Está ejecutando una versión de depuración de Ghostty. El rendimiento no " -"será óptimo." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Inspector de la terminal" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "Encontrar…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "Resultado anterior" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "Resultado siguiente" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "¡Epa!" - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "No se puede iniciar OpenGL para rendering." - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"La terminal está en modo de lectura. Puedes ver, seleccionar, y desplazar a " -"través del contenido, pero ninguna entrada (evento) va a ser enviada a la " -"aplicación que se está ejecutando." - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "Solo lectura" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Copiar" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "Pegar" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "Notificar cuando el próximo comando finalice" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "Limpiar" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "Reiniciar" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "Dividir" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "Cambiar título…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Dividir arriba" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "Dividir abajo" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Dividir a la izquierda" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "Dividir a la derecha" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Pestaña" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Nueva pestaña" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Cerrar pestaña" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Ventana" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Nueva ventana" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Cerrar ventana" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "Configuración" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Abrir configuración" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "Dejar en blanco para restaurar el título predeterminado." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "Aceptar" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Nueva ventana dividida" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "Ver pestañas abiertas" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "Menú principal" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Paleta de comandos" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "Inspector de la terminal" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "Acerca de Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "Salir" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "Ejecutar comando…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Una aplicación está intentando escribir en el portapapeles. El contenido " -"actual del portapapeles se muestra a continuación." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Una aplicación está intentando leer desde el portapapeles. El contenido " -"actual del portapapeles se muestra a continuación." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Advertencia: Pegado potencialmente inseguro" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"Pegar este texto en la terminal puede ser peligroso ya que parece que " -"algunos comandos podrían ejecutarse." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "¿Salir de Ghostty?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "¿Cerrar pestaña?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "¿Cerrar ventana?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "¿Cerrar división?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Todas las sesiones de terminal serán terminadas." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Todas las sesiones de terminal en esta pestaña serán terminadas." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Todas las sesiones de terminal en esta ventana serán terminadas." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "El proceso actualmente en ejecución en esta división será terminado." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "Comando finalizado" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "Comando exitoso" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "Comando fallido" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Comando ejecutado con éxito" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Comando fallido" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "Cambiar el título de la terminal" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "Configuración recargada" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "Copiado al portapapeles" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "El portapapeles está limpio" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Desarrolladores de Ghostty" diff --git a/po/es_BO.po b/po/es_BO.po new file mode 100644 index 00000000000..d0b271d9e55 --- /dev/null +++ b/po/es_BO.po @@ -0,0 +1,355 @@ +# Spanish 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. +# Miguel Peredo , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-02-12 17:46+0200\n" +"Last-Translator: Miguel Peredo \n" +"Language-Team: Spanish \n" +"Language: es_BO\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Abrir en Ghostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Autorizar acceso al portapapeles" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Denegar" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Permitir" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Recordar su elección para esta división de ventana" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "Recargar configuración para mostrar este aviso nuevamente" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Cancelar" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Cerrar" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Errores de configuración" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Se encontraron uno o más errores de configuración. Por favor revise los " +"errores a continuación, y recargue su configuración o ignore estos errores." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Ignorar" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Recargar configuración" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ Está ejecutando una versión de depuración de Ghostty. El rendimiento no " +"será óptimo." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspector de la terminal" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Encontrar…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Resultado anterior" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "Resultado siguiente" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "¡Epa!" + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "No se puede iniciar OpenGL para rendering." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"La terminal está en modo de lectura. Puedes ver, seleccionar, y desplazar a " +"través del contenido, pero ninguna entrada (evento) va a ser enviada a la " +"aplicación que se está ejecutando." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Solo lectura" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Copiar" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Pegar" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "Notificar cuando el próximo comando finalice" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "Limpiar" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Reiniciar" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Dividir" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Cambiar título…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Dividir arriba" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Dividir abajo" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Dividir a la izquierda" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Dividir a la derecha" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Pestaña" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Cambiar el título de la pestaña…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Nueva pestaña" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Cerrar pestaña" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Ventana" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Nueva ventana" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Cerrar ventana" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "Configuración" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Abrir configuración" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "Dejar en blanco para restaurar el título predeterminado." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "Aceptar" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Nueva ventana dividida" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Ver pestañas abiertas" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Menú principal" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Paleta de comandos" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "Inspector de la terminal" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "Acerca de Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Salir" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Ejecutar comando…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Una aplicación está intentando escribir en el portapapeles. El contenido " +"actual del portapapeles se muestra a continuación." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Una aplicación está intentando leer desde el portapapeles. El contenido " +"actual del portapapeles se muestra a continuación." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Advertencia: Pegado potencialmente inseguro" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Pegar este texto en la terminal puede ser peligroso ya que parece que " +"algunos comandos podrían ejecutarse." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "¿Salir de Ghostty?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "¿Cerrar pestaña?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "¿Cerrar ventana?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "¿Cerrar división?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Todas las sesiones de terminal serán terminadas." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Todas las sesiones de terminal en esta pestaña serán terminadas." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Todas las sesiones de terminal en esta ventana serán terminadas." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "El proceso actualmente en ejecución en esta división será terminado." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Comando finalizado" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Comando exitoso" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Comando fallido" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Comando ejecutado con éxito" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Comando fallido" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Cambiar el título de la terminal" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Cambiar el título de la pestaña" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "Configuración recargada" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Copiado al portapapeles" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "El portapapeles está limpio" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Desarrolladores de Ghostty" diff --git a/po/es_ES.po b/po/es_ES.po new file mode 100644 index 00000000000..364da14c9a0 --- /dev/null +++ b/po/es_ES.po @@ -0,0 +1,355 @@ +# Spanish translations for com.mitchellh.ghostty package +# Traducciones al español para el paquete com.mitchellh.ghostty. +# Copyright (C) 2026 "Mitchell Hashimoto, Ghostty contributors" +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# José Miguel Sarasola , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-05 10:23+0800\n" +"PO-Revision-Date: 2026-02-18 10:57+0100\n" +"Last-Translator: José Miguel Sarasola \n" +"Language-Team: \n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Abrir en Ghostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Autorizar acceso al portapapeles" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Denegar" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Permitir" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Recordar elección para esta división" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "Recargar configuración para volver a mostrar este aviso" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Cancelar" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Cerrar" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Errores en la configuración" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Se detectaron uno o más errores de configuración. Por favor, revisa los " +"errores más abajo y recarga la configuración o ignora estos errores." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Ignorar" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Recargar configuración" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ Â¡Estás ejecutando una versión de depuración de Ghostty! El rendimiento se verá perjudicado." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspector de terminal" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Buscar…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Resultado previo" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "Resultado siguiente" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "Oh, no." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "No se pudo obtener un contexto de OpenGL para el renderizado." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Este terminal está en modo de solo lectura. Puedes ver, seleccionar y hacer " +"scroll del contenido, pero no se enviarán eventos de entrada a la aplicación " +"en ejecución." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Solo lectura" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Copiar" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Pegar" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "Notificar al finalizar el siguiente comando" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "Limpiar" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Reiniciar" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Dividir" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Cambiar título…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Dividir arriba" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Dividir abajo" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Dividir a la izquierda" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Dividir a la derecha" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Pestaña" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Cambiar título de la pestaña…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Nueva pestaña" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Cerrar pestaña" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Ventana" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Nueva ventana" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Cerrar ventana" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "Configuración" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Abrir configuración" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "Dejar en blanco para restaurar el título predeterminado." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "Aceptar" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Nueva división" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Ver pestañas abiertas" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Menú principal" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Paleta de comandos" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "Inspector de terminal" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "Acerca de Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Salir" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Ejecutar un comando…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Una aplicación está intentando escribir en el portapapeles. El contenido " +"actual del portapapeles se muestra a continuación." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Una aplicación está intentando leer desde el portapapeles. El contenido " +"actual del portapapeles se muestra a continuación." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Advertencia: Pegado potencialmente inseguro" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Pegar este texto en el terminal puede ser peligroso ya que parece que " +"algunos comandos podrían ejecutarse." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "¿Salir de Ghostty?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "¿Cerrar pestaña?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "¿Cerrar ventana?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "¿Cerrar división?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Todas las sesiones del terminal serán finalizadas." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Todas las sesiones del terminal en esta pestaña serán finalizadas." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Todas las sesiones del terminal en esta ventana serán finalizadas." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "El proceso ejecutándose en esta división será finalizado." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Comando finalizado" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Comando completado" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Comando fallido" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Comando completado" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Comando fallido" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Cambiar el título del terminal" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Cambiar el título de la pestaña" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "Configuración recargada" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Copiado al portapapeles" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "Portapapeles vaciado" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Desarrolladores de Ghostty" diff --git a/po/fr.po b/po/fr.po new file mode 100644 index 00000000000..9e9bf8dff5f --- /dev/null +++ b/po/fr.po @@ -0,0 +1,356 @@ +# French 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. +# Kirwiisp , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-02-18 15:03+0200\n" +"Last-Translator: Pangoraw \n" +"Language-Team: French \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Ouvrir dans Ghostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Autoriser l'accès au presse-papiers" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Refuser" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Autoriser" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Se rappeler du choix pour ce panneau" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "Recharger la configuration pour afficher à nouveau ce message" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Annuler" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Fermer" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Erreurs de configuration" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Une ou plusieurs erreurs de configuration ont été trouvées. Veuillez lire " +"les erreurs ci-dessous, et recharger votre configuration ou bien ignorer ces " +"erreurs." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Ignorer" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Recharger la configuration" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ Vous utilisez une version de débogage de Ghostty ! Les performances seront " +"dégradées." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspecteur" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Chercher…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Résultat précédent" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "Résultat suivant" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "Oh, non." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "Impossible d'obtenir un contexte OpenGL pour le rendu." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Ce terminal est en mode lecture seule. Vous pouvez encore voir, " +"sélectionner, et naviguer dans son contenu, mais aucune entrée ne sera " +"envoyée à l'application en cours." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Lecture seule" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Copier" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Coller" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "Notifier à la complétion de la prochaine commande" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "Tout effacer" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Réinitialiser" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Créer panneau" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Changer le titre…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Panneau en haut" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Panneau en bas" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Panneau à gauche" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Panneau à droite" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Onglet" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Changer le titre de l'onglet…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Nouvel onglet" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Fermer l'onglet" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Fenêtre" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Nouvelle fenêtre" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Fermer la fenêtre" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "Config" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Ouvrir la configuration" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "Laisser vide pour restaurer le titre par défaut." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Nouveau panneau" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Voir les onglets ouverts" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Menu principal" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Palette de commandes" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "Inspecteur de terminal" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "À propos de Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Quitter" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Exécuter une commande…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Une application essaie d'écrire dans le presse-papiers. Le contenu actuel du " +"presse-papiers est affiché ci-dessous." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Une application essaie de lire depuis le presse-papiers. Le contenu actuel " +"du presse-papiers est affiché ci-dessous." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Attention: Collage potentiellement dangereux" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Coller ce texte dans le terminal pourrait être dangereux, il semblerait que " +"certaines commandes pourraient être exécutées." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Quitter Ghostty ?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "Fermer l'onglet ?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "Fermer la fenêtre ?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "Fermer le panneau ?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Toutes les sessions vont être arrêtées." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Toutes les sessions de cet onglet vont être arrêtées." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Toutes les sessions de cette fenêtre vont être arrêtées." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "Le processus en cours dans ce panneau va être arrêté." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Commande terminée" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Commande réussie" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "La commande a échoué" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Commande réussie" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "La commande a échoué" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Changer le nom du terminal" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Changer le titre de l'onglet" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "Configuration rechargée" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Copié dans le presse-papiers" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "Presse-papiers vidé" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Les développeurs de Ghostty" diff --git a/po/fr_FR.UTF-8.po b/po/fr_FR.UTF-8.po deleted file mode 100644 index 1687b490958..00000000000 --- a/po/fr_FR.UTF-8.po +++ /dev/null @@ -1,356 +0,0 @@ -# French 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. -# Kirwiisp , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-09 21:18+0200\n" -"Last-Translator: Gerry Agbobada \n" -"Language-Team: French \n" -"Language: fr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Autoriser l'accès au presse-papiers" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "Refuser" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Autoriser" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "Se rappeler du choix pour ce panneau" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "Recharger la configuration pour afficher à nouveau ce message" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "Annuler" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Fermer" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "Erreurs de configuration" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"Une ou plusieurs erreurs de configuration ont été trouvées. Veuillez lire " -"les erreurs ci-dessous, et recharger votre configuration ou bien ignorer ces " -"erreurs." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Ignorer" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "Recharger la configuration" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"âš ï¸ Vous utilisez une version de débogage de Ghostty ! Les performances seront " -"dégradées." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Inspecteur" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "Chercher…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "Résultat précédent" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "Résultat suivant" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "Oh, non." - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "Impossible d'obtenir un contexte OpenGL pour le rendu." - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"Ce terminal est en mode lecture seule. Vous pouvez encore voir, " -"sélectionner, et naviguer dans son contenu, mais aucune entrée ne sera " -"envoyée à l'application en cours." - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "Lecture seule" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Copier" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "Coller" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "Notifier à la complétion de la prochaine commande" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "Tout effacer" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "Réinitialiser" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "Créer panneau" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "Changer le titre…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Panneau en haut" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "Panneau en bas" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Panneau à gauche" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "Panneau à droite" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Onglet" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Nouvel onglet" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Fermer l'onglet" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Fenêtre" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Nouvelle fenêtre" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Fermer la fenêtre" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "Config" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Ouvrir la configuration" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "Laisser vide pour restaurer le titre par défaut." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "OK" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Nouveau panneau" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "Voir les onglets ouverts" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "Menu principal" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Palette de commandes" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "Inspecteur de terminal" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "À propos de Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "Quitter" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "Exécuter une commande…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Une application essaie d'écrire dans le presse-papiers. Le contenu actuel du " -"presse-papiers est affiché ci-dessous." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Une application essaie de lire depuis le presse-papiers. Le contenu actuel " -"du presse-papiers est affiché ci-dessous." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Attention: Collage potentiellement dangereux" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"Coller ce texte dans le terminal pourrait être dangereux, il semblerait que " -"certaines commandes pourraient être exécutées." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Quitter Ghostty ?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "Fermer l'onglet ?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "Fermer la fenêtre ?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "Fermer le panneau ?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Toutes les sessions vont être arrêtées." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Toutes les sessions de cet onglet vont être arrêtées." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Toutes les sessions de cette fenêtre vont être arrêtées." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "Le processus en cours dans ce panneau va être arrêté." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "Commande terminée" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "Commande réussie" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "La commande a échoué" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Commande réussie" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "La commande a échoué" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "Changer le nom du terminal" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "Configuration rechargée" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "Copié dans le presse-papiers" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "Presse-papiers vidé" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Les développeurs de Ghostty" diff --git a/po/ga.po b/po/ga.po new file mode 100644 index 00000000000..575774f6fc3 --- /dev/null +++ b/po/ga.po @@ -0,0 +1,356 @@ +# Irish 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. +# Aindriú Mac Giolla Eoin , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-05 10:23+0800\n" +"PO-Revision-Date: 2026-02-18 14:32+0000\n" +"Last-Translator: Aindriú Mac Giolla Eoin \n" +"Language-Team: Irish \n" +"Language: ga\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;\n" + + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Oscail i nGhostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Údarú rochtain ar an ngearrthaisce" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Diúltaigh" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Ceadaigh" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Sábháil an rogha don scoilt seo" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "Athlódáil an chumraíocht chun an teachtaireacht seo a thaispeáint arís" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Cealaigh" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Dún" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Earráidí cumraíochta" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Fuarthas earráid chumraíochta amháin nó níos mó. Athbhreithnigh na hearráidí " +"thíos, agus athlódáil do chumraíocht nó déan neamhaird de na hearráidí seo." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Déan neamhaird de" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Athlódáil cumraíocht" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ Tá leagan dífhabhtaithe de Ghostty á rith agat! Laghdófar an fheidhmíocht." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Cigire teirminéil" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Cuardaigh…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "An toradh roimhe seo" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "An chéad toradh eile" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "Ó, fadbh." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "Ní féidir comhthéacs OpenGL a fháil le haghaidh rindreála." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Tá an teirminéal seo i mód inléite amháin. Is féidir leat fós féachaint, " +"roghnú agus scroláil tríd an ábhar, ach ní seolfar aon teagmhais ionchuir " +"chuig an bhfeidhmchlár atá ag rith." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "InleÌite amhaÌin" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Cóipeáil" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Greamaigh" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "Seol fógra nuair a chríochnaíonn an chéad ordú eile" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "Glan" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Athshocraigh" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Scoilt" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Athraigh teideal…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Scoilt suas" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Scoilt síos" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Scoilt ar chlé" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Scoilt ar dheis" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Táb" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Athraigh teideal an táb…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Táb nua" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Dún táb" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Fuinneog" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Fuinneog nua" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Dún fuinneog" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "Cumraíocht" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Oscail cumraíocht" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "Fág bán chun an teideal réamhshocraithe a athbhunú." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "Ceart go leor" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Scoilt nua" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Féach ar na táib oscailte" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Príomh-Roghchlár" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Pailéad ordaithe" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "Cigire teirminéil" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "Maidir le Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Scoir" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Rith ordú…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Tá feidhmchlár ag iarraidh scríobh chuig an ngearrthaisce. Taispeántar ábhar " +"reatha an ghearrthaisce thíos." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Tá feidhmchlár ag iarraidh léamh ón ngearrthaisce. Taispeántar ábhar reatha " +"an ghearrthaisce thíos." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Rabhadh: Greamaigh a d'fhéadfadh a bheith neamhshábháilte" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"D’fhéadfadh sé a bheith contúirteach an téacs seo a ghreamú isteach sa " +"teirminéal, toisc go d'fhéadfadh roinnt orduithe a fhorghníomhú." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Scoir Ghostty?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "Dún táb?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "Dún fuinneog?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "Dún an scoilt?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Cuirfear deireadh le gach seisiún teirminéil." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Cuirfear deireadh le gach seisiún teirminéil sa táb seo." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Cuirfear deireadh le gach seisiún teirminéil san fhuinneog seo." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "" +"Cuirfear deireadh leis an bpróiseas atá ar siúl faoi láthair sa scoilt seo." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Ordú críochnaithe" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "D’éirigh leis an ordú" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Theip ar an ordú" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "D'éirigh leis an ordú" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Theip ar an ordú" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Athraigh teideal teirminéil" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Athraigh teideal an táb" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "Tá an chumraíocht athlódáilte" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Cóipeáilte chuig an ghearrthaisce" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "Gearrthaisce glanta" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Forbróirí Ghostty" diff --git a/po/ga_IE.UTF-8.po b/po/ga_IE.UTF-8.po deleted file mode 100644 index 72f237f8624..00000000000 --- a/po/ga_IE.UTF-8.po +++ /dev/null @@ -1,353 +0,0 @@ -# Irish 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. -# Aindriú Mac Giolla Eoin , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2025-08-26 15:46+0100\n" -"Last-Translator: Aindriú Mac Giolla Eoin \n" -"Language-Team: Irish \n" -"Language: ga\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;\n" -"X-Generator: Poedit 3.4.2\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Údarú rochtain ar an ngearrthaisce" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "Diúltaigh" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Ceadaigh" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "Sábháil an rogha don scoilt seo" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "Athlódáil an chumraíocht chun an teachtaireacht seo a thaispeáint arís" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "Cealaigh" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Dún" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "Earráidí cumraíochta" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"Fuarthas earráid chumraíochta amháin nó níos mó. Athbhreithnigh na hearráidí " -"thíos, agus athlódáil do chumraíocht nó déan neamhaird de na hearráidí seo." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Déan neamhaird de" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "Athlódáil cumraíocht" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"âš ï¸ Tá leagan dífhabhtaithe de Ghostty á rith agat! Laghdófar an fheidhmíocht." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Cigire teirminéil" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Cóipeáil" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "Greamaigh" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "Glan" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "Athshocraigh" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "Scoilt" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "Athraigh teideal…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Scoilt suas" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "Scoilt síos" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Scoilt ar chlé" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "Scoilt ar dheis" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Táb" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Táb nua" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Dún táb" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Fuinneog" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Fuinneog nua" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Dún fuinneog" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "Cumraíocht" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Oscail cumraíocht" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "Fág bán chun an teideal réamhshocraithe a athbhunú." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "Ceart go leor" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Scoilt nua" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "Féach ar na táib oscailte" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "Príomh-Roghchlár" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Pailéad ordaithe" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "Cigire teirminéil" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "Maidir le Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "Scoir" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "Rith ordú…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Tá feidhmchlár ag iarraidh scríobh chuig an ngearrthaisce. Taispeántar ábhar " -"reatha an ghearrthaisce thíos." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Tá feidhmchlár ag iarraidh léamh ón ngearrthaisce. Taispeántar ábhar reatha " -"an ghearrthaisce thíos." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Rabhadh: Greamaigh a d'fhéadfadh a bheith neamhshábháilte" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"D’fhéadfadh sé a bheith contúirteach an téacs seo a ghreamú isteach sa " -"teirminéal, toisc go d'fhéadfadh roinnt orduithe a fhorghníomhú." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Scoir Ghostty?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "Dún táb?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "Dún fuinneog?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "Dún an scoilt?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Cuirfear deireadh le gach seisiún teirminéil." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Cuirfear deireadh le gach seisiún teirminéil sa táb seo." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Cuirfear deireadh le gach seisiún teirminéil san fhuinneog seo." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "" -"Cuirfear deireadh leis an bpróiseas atá ar siúl faoi láthair sa scoilt seo." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "D'éirigh leis an ordú" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Theip ar an ordú" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "Athraigh teideal teirminéil" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "Tá an chumraíocht athlódáilte" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "Cóipeáilte chuig an ghearrthaisce" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "Gearrthaisce glanta" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Forbróirí Ghostty" diff --git a/po/he.po b/po/he.po new file mode 100644 index 00000000000..5a04aae7b8a --- /dev/null +++ b/po/he.po @@ -0,0 +1,352 @@ +# Hebrew translations for com.mitchellh.ghostty. +# Copyright (C) 2026 "Mitchell Hashimoto, Ghostty contributors" +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Sl (Shahaf Levi), Sl's Repository Ltd , 2026. +# CraziestOwl , 2025. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-02-18 18:14+0300\n" +"Last-Translator: Sl (Shahaf Levi), Sl's Repository Ltd " +"\n" +"Language-Team: Hebrew \n" +"Language: he\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "פתח/×™ בGhostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "×שר/×™ גישה ללוח ההעתקה" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "דחייה" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "×ישור" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "זכור/×™ ×ת הבחירה עבור פיצול ×–×”" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "טען/×™ ×ת ההגדרות מחדש כדי להציג ×ת הבקשה הזו שוב" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "ביטול" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "סגירה" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "שגי×ות בהגדרות" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"נמצ×ו ×חת ×ו יותר שגי×ות בהגדרות. ×× × ×‘×“×•×§/×™ ×ת השגי×ות המופיעות מטה ול×חר " +"מכן טען/×™ ×ת ההגדרות מחדש ×ו התעל×/×™ מהשגי×ות." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "התעלמות" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "טעינה מחדש של ההגדרות" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "âš ï¸ ×ת/×” מריץ/×” גרסת ניפוי שגי×ות של Ghostty! ×”×‘×™×¦×•×¢×™× ×™×”×™×• ירודי×." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: בודק המסוף" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "חפש/י…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "ההת×מה הקודמת" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "ההת×מה הב××”" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "×וי, ל×" + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "×œ× × ×™×ª×Ÿ לקבל הקשר OpenGL לצורך רינדור." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"מסוף ×–×” × ×ž×¦× ×‘×ž×¦×‘ קרי××” בלבד. עדיין תוכל/×™ לצפות, לבחור ולגלול בתוכן, ×ך ×œ× " +"יישלחו ×ירועי קלט ל×פליקציה הפעילה." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "לקרי××” בלבד" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "העתקה" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "הדבקה" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "תזכורת ×‘×¡×™×•× ×”×¤×§×•×“×” הב××”" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "ניקוי" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "×יפוס" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "פיצול" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "שינוי כותרת…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "פיצול למעלה" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "פיצול למטה" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "פיצול שמ×לה" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "פיצול ימינה" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "כרטיסייה" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "שנה/×™ ×ת כותרת הכרטיסייה…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "כרטיסייה חדשה" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "סגור/×™ כרטיסייה" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "חלון" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "חלון חדש" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "סגור/×™ חלון" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "הגדרות" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "פתיחת ההגדרות" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "הש×ר/×™ ריק כדי לשחזר ×ת כותרת ברירת המחדל." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "×ישור" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "פיצול חדש" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "הצג/×™ כרטיסיות פתוחות" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "תפריט ר×שי" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "לוח פקודות" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "בודק המסוף" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "×ודות Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "יצי××”" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "הרץ/×™ פקודה…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"יש ×פליקציה שמנסה לכתוב לתוך לוח ההעתקה. התוכן הנוכחי של הלוח מופיע למטה." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "יש ×פליקציה שמנסה ×œ×§×¨×•× ×ž×œ×•×— ההעתקה. התוכן הנוכחי של הלוח מופיע למטה." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "×זהרה: ההדבקה עלולה להיות מסוכנת" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"הדבקת טקסט ×–×” במסוף עלולה להיות מסוכנת, מכיוון שככל הנר××” ×”×™× ×ª×•×‘×™×œ להרצה של " +"פקודות מסוימות." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "לצ×ת מGhostty?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "לסגור ×ת הכרטיסייה?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "לסגור ×ת החלון?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "לסגור ×ת הפיצול?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "כל הפעלות המסוף יסתיימו." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "כל הפעלות המסוף בכרטיסייה זו יסתיימו." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "כל הפעלות המסוף בחלון ×–×” יסתיימו." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "התהליך שרץ כרגע בפיצול ×–×” יסתיי×." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "הפקודה הסתיימה" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "הפקודה הצליחה" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "הפקודה נכשלה" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "הפקודה הצליחה" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "הפקודה נכשלה" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "שינוי כותרת המסוף" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "שינוי כותרת הכרטיסייה" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "ההגדרות הוטענו מחדש" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "הועתק ללוח ההעתקה" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "לוח ההעתקה רוקן" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "×”×ž×¤×ª×—×™× ×©×œ Ghostty" diff --git a/po/he_IL.UTF-8.po b/po/he_IL.UTF-8.po deleted file mode 100644 index 7d7c8d7cc94..00000000000 --- a/po/he_IL.UTF-8.po +++ /dev/null @@ -1,352 +0,0 @@ -# Hebrew translations for com.mitchellh.ghostty. -# Copyright (C) 2026 "Mitchell Hashimoto, Ghostty contributors" -# This file is distributed under the same license as the com.mitchellh.ghostty package. -# Sl (Shahaf Levi), Sl's Repository Ltd , 2026. -# CraziestOwl , 2025. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-11 22:45+0300\n" -"Last-Translator: Sl (Shahaf Levi), Sl's Repository Ltd " -"\n" -"Language-Team: Hebrew \n" -"Language: he\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "×שר/×™ גישה ללוח ההעתקה" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "דחייה" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "×ישור" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "זכור/×™ ×ת הבחירה עבור פיצול ×–×”" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "טען/×™ ×ת ההגדרות מחדש כדי להציג ×ת הבקשה הזו שוב" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "ביטול" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "סגירה" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "שגי×ות בהגדרות" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"נמצ×ו ×חת ×ו יותר שגי×ות בהגדרות. ×× × ×‘×“×•×§/×™ ×ת השגי×ות המופיעות מטה ול×חר " -"מכן טען/×™ ×ת ההגדרות מחדש ×ו התעל×/×™ מהשגי×ות." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "התעלמות" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "טעינה מחדש של ההגדרות" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "âš ï¸ ×ת/×” מריץ/×” גרסת ניפוי שגי×ות של Ghostty! ×”×‘×™×¦×•×¢×™× ×™×”×™×• ירודי×." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: בודק המסוף" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "חפש/י…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "ההת×מה הקודמת" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "ההת×מה הב××”" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "×וי, ל×" - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "×œ× × ×™×ª×Ÿ לקבל הקשר OpenGL לצורך רינדור." - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"מסוף ×–×” × ×ž×¦× ×‘×ž×¦×‘ קרי××” בלבד. עדיין תוכל/×™ לצפות, לבחור ולגלול בתוכן, ×ך ×œ× " -"יישלחו ×ירועי קלט ל×פליקציה הפעילה." - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "לקרי××” בלבד" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "העתקה" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "הדבקה" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "תזכורת ×‘×¡×™×•× ×”×¤×§×•×“×” הב××”" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "ניקוי" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "×יפוס" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "פיצול" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "שינוי כותרת…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "פיצול למעלה" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "פיצול למטה" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "פיצול שמ×לה" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "פיצול ימינה" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "כרטיסייה" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "כרטיסייה חדשה" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "סגור/×™ כרטיסייה" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "חלון" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "חלון חדש" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "סגור/×™ חלון" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "הגדרות" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "פתיחת ההגדרות" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "הש×ר/×™ ריק כדי לשחזר ×ת כותרת ברירת המחדל." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "×ישור" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "פיצול חדש" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "הצג/×™ כרטיסיות פתוחות" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "תפריט ר×שי" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "לוח פקודות" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "בודק המסוף" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "×ודות Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "יצי××”" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "הרץ/×™ פקודה…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"יש ×פליקציה שמנסה לכתוב לתוך לוח ההעתקה. התוכן הנוכחי של הלוח מופיע למטה." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "יש ×פליקציה שמנסה ×œ×§×¨×•× ×ž×œ×•×— ההעתקה. התוכן הנוכחי של הלוח מופיע למטה." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "×זהרה: ההדבקה עלולה להיות מסוכנת" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"הדבקת טקסט ×–×” במסוף עלולה להיות מסוכנת, מכיוון שככל הנר××” ×”×™× ×ª×•×‘×™×œ להרצה של " -"פקודות מסוימות." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "לצ×ת מGhostty?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "לסגור ×ת הכרטיסייה?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "לסגור ×ת החלון?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "לסגור ×ת הפיצול?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "כל הפעלות המסוף יסתיימו." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "כל הפעלות המסוף בכרטיסייה זו יסתיימו." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "כל הפעלות המסוף בחלון ×–×” יסתיימו." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "התהליך שרץ כרגע בפיצול ×–×” יסתיי×." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "הפקודה הסתיימה" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "הפקודה הצליחה" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "הפקודה נכשלה" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "הפקודה הצליחה" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "הפקודה נכשלה" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "שינוי כותרת המסוף" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "ההגדרות הוטענו מחדש" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "הועתק ללוח ההעתקה" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "לוח ההעתקה רוקן" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "×”×ž×¤×ª×—×™× ×©×œ Ghostty" diff --git a/po/hr.po b/po/hr.po new file mode 100644 index 00000000000..44a3a205052 --- /dev/null +++ b/po/hr.po @@ -0,0 +1,355 @@ +# Croatian translations for com.mitchellh.ghostty package +# Hrvatski prijevod za paket com.mitchellh.ghostty. +# Copyright (C) 2025 "Mitchell Hashimoto, Ghostty contributors" +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Filip , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-02-18 21:00+0200\n" +"Last-Translator: Filip7 \n" +"Language-Team: Croatian \n" +"Language: hr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Otvori u Ghosttyju" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Dopusti pristup meÄ‘uspremniku" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Odbij" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Dopusti" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Zapamti izbor za ovu podjelu" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "Ponovno uÄitaj postavke za prikaz ovog upita" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Otkaži" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Zatvori" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "GreÅ¡ke u postavkama" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"PronaÄ‘ena je greÅ¡ka (ili viÅ¡e njih) u postavkama. Pregledaj niže navedene " +"greÅ¡ke te ponovno uÄitaj postavke ili zanemari ove greÅ¡ke." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Zanemari" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Ponovno uÄitaj postavke" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "âš ï¸ Pokrenuta je debug verzija Ghosttyja! Performanse će biti smanjene." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: inspektor terminala" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Pretraži…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Prethodno podudaranje" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "Sljedeće podudaranje" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "Oh, ne." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "Neuspjelo dohvaćanje OpenGL konteksta za renderiranje." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Ovaj terminal je u naÄinu rada samo za Äitanje. I dalje je moguće gledati, " +"odabirati i skrolati kroz sadržaj, no unos neće biti poslan pokrenutoj " +"aplikaciji." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Samo za Äitanje" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Kopiraj" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Zalijepi" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "Obavijesti kada iduća naredba zavrÅ¡i" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "OÄisti" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Resetiraj" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Podijeli" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Promijeni naslov…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Podijeli gore" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Podijeli dolje" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Podijeli lijevo" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Podijeli desno" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Kartica" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Promijeni naslov kartice…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Nova kartica" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Zatvori karticu" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Prozor" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Novi prozor" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Zatvori prozor" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "Postavke" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Otvori postavke" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "Ostavi prazno za povratak zadanog naslova." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Nova podjela" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Pregledaj otvorene kartice" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Glavni izbornik" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Paleta naredbi" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "Inspektor terminala" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "O Ghosttyju" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "IzaÄ‘i" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "IzvrÅ¡i naredbu…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Aplikacija pokuÅ¡ava pisati u meÄ‘uspremnik. Trenutna vrijednost meÄ‘uspremnika " +"prikazana je niže." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Aplikacija pokuÅ¡ava proÄitati vrijednost meÄ‘uspremnika. Trenutna vrijednost " +"meÄ‘uspremnika je prikazana niže." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Upozorenje: Potencijalno opasno lijepljenje" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Lijepljenje ovog teksta u terminal može biti opasno jer se Äini da neke " +"naredbe mogu biti izvrÅ¡ene." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Zatvori Ghostty?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "Zatvori karticu?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "Zatvori prozor?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "Zatvori podjelu?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Sve sesije terminala će biti prekinute." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Sve sesije terminala u ovoj kartici će biti prekinute." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Sve sesije terminala u ovom prozoru će biti prekinute." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "Pokrenuti procesi u ovoj podjeli će biti prekinuti." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Naredba je zavrÅ¡ena" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Naredba je uspjela" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Naredba nije uspjela" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Naredba je uspjela" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Naredba nije uspjela" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Promijeni naslov terminala" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Promijeni naslov kartice" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "Ponovno uÄitane postavke" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Kopirano u meÄ‘uspremnik" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "OÄišćen meÄ‘uspremnik" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Razvijatelji Ghosttyja" diff --git a/po/hr_HR.UTF-8.po b/po/hr_HR.UTF-8.po deleted file mode 100644 index a73f8fee29a..00000000000 --- a/po/hr_HR.UTF-8.po +++ /dev/null @@ -1,355 +0,0 @@ -# Croatian translations for com.mitchellh.ghostty package -# Hrvatski prijevod za paket com.mitchellh.ghostty. -# Copyright (C) 2025 "Mitchell Hashimoto, Ghostty contributors" -# This file is distributed under the same license as the com.mitchellh.ghostty package. -# Filip , 2026. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-10 22:25+0200\n" -"Last-Translator: Filip7 \n" -"Language-Team: Croatian \n" -"Language: hr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " -"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Dopusti pristup meÄ‘uspremniku" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "Odbij" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Dopusti" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "Zapamti izbor za ovu podjelu" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "Ponovno uÄitaj postavke za prikaz ovog upita" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "Otkaži" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Zatvori" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "GreÅ¡ke u postavkama" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"PronaÄ‘ena je greÅ¡ka (ili viÅ¡e njih) u postavkama. Pregledaj niže navedene " -"greÅ¡ke te ponovno uÄitaj postavke ili zanemari ove greÅ¡ke." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Zanemari" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "Ponovno uÄitaj postavke" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "âš ï¸ Pokrenuta je debug verzija Ghosttyja! Performanse će biti smanjene." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: inspektor terminala" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "Pretraži…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "Prethodno podudaranje" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "Sljedeće podudaranje" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "Oh, ne." - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "Neuspjelo dohvaćanje OpenGL konteksta za renderiranje." - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"Ovaj terminal je u naÄinu rada samo za Äitanje. I dalje je moguće gledati, " -"odabirati i skrolati kroz sadržaj, no unos neće biti poslan pokrenutoj " -"aplikaciji." - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "Samo za Äitanje" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Kopiraj" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "Zalijepi" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "Obavijesti kada iduća naredba zavrÅ¡i" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "OÄisti" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "Resetiraj" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "Podijeli" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "Promijeni naslov…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Podijeli gore" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "Podijeli dolje" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Podijeli lijevo" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "Podijeli desno" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Kartica" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Nova kartica" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Zatvori karticu" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Prozor" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Novi prozor" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Zatvori prozor" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "Postavke" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Otvori postavke" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "Ostavi prazno za povratak zadanog naslova." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "OK" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Nova podjela" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "Pregledaj otvorene kartice" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "Glavni izbornik" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Paleta naredbi" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "Inspektor terminala" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "O Ghosttyju" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "IzaÄ‘i" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "IzvrÅ¡i naredbu…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Aplikacija pokuÅ¡ava pisati u meÄ‘uspremnik. Trenutna vrijednost meÄ‘uspremnika " -"prikazana je niže." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Aplikacija pokuÅ¡ava proÄitati vrijednost meÄ‘uspremnika. Trenutna vrijednost " -"meÄ‘uspremnika je prikazana niže." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Upozorenje: Potencijalno opasno lijepljenje" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"Lijepljenje ovog teksta u terminal može biti opasno jer se Äini da neke " -"naredbe mogu biti izvrÅ¡ene." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Zatvori Ghostty?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "Zatvori karticu?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "Zatvori prozor?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "Zatvori podjelu?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Sve sesije terminala će biti prekinute." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Sve sesije terminala u ovoj kartici će biti prekinute." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Sve sesije terminala u ovom prozoru će biti prekinute." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "Pokrenuti procesi u ovoj podjeli će biti prekinuti." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "Naredba je zavrÅ¡ena" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "Naredba je uspjela" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "Naredba nije uspjela" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Naredba je uspjela" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Naredba nije uspjela" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "Promijeni naslov terminala" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "Ponovno uÄitane postavke" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "Kopirano u meÄ‘uspremnik" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "OÄišćen meÄ‘uspremnik" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Razvijatelji Ghosttyja" diff --git a/po/hu.po b/po/hu.po new file mode 100644 index 00000000000..e7474e2c497 --- /dev/null +++ b/po/hu.po @@ -0,0 +1,355 @@ +# Hungarian translations for com.mitchellh.ghostty package. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Balázs Szücs , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-02-26 21:00+0100\n" +"Last-Translator: Balázs Szücs \n" +"Language-Team: Hungarian \n" +"Language: hu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Megnyitás a Ghostty alkalmazásban" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Vágólap-hozzáférés engedélyezése" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Elutasítás" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Engedélyezés" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Választás megjegyzése erre a felosztásra" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "Konfiguráció frissítése a kérdés újbóli megjelenítéséhez" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Mégse" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Bezárás" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Konfigurációs hibák" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Egy vagy több konfigurációs hiba található. Kérjük, ellenÅ‘rizze az alábbi " +"hibákat, és frissítse a konfigurációt, vagy hagyja figyelmen kívül ezeket a " +"hibákat." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Figyelmen kívül hagyás" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Konfiguráció frissítése" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ A Ghostty hibakeresÅ‘ verzióját futtatja! A teljesítmény csökkenni fog." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Terminálvizsgáló" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Keresés…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "ElÅ‘zÅ‘ találat" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "KövetkezÅ‘ találat" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "Jaj, ne." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "Nem sikerült OpenGL-környezetet létrehozni a megjelenítéshez." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Ez a terminál csak olvasható módban van. A tartalmat továbbra is " +"megtekintheti, kijelölheti és görgetheti, de nem küld bemeneti eseményeket a " +"futó alkalmazásnak." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Csak olvasható" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Másolás" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Beillesztés" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "Értesítés a következÅ‘ parancs befejezésekor" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "Törlés" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Visszaállítás" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Felosztás" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Cím módosítása…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Felosztás felfelé" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Felosztás lefelé" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Felosztás balra" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Felosztás jobbra" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Fül" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Fül címének módosítása…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Új fül" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Fül bezárása" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Ablak" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Új ablak" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Ablak bezárása" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "Konfiguráció" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Konfiguráció megnyitása" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "Hagyja üresen az alapértelmezett cím visszaállításához." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "Rendben" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Új felosztás" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Megnyitott fülek megtekintése" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "FÅ‘menü" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Parancspaletta" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "Terminálvizsgáló" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "A Ghostty névjegye" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Kilépés" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Parancs végrehajtása…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Egy alkalmazás megpróbál írni a vágólapra. A vágólap jelenlegi tartalma lent " +"látható." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Egy alkalmazás megpróbál olvasni a vágólapról. A vágólap jelenlegi tartalma " +"lent látható." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Figyelem: potenciálisan veszélyes beillesztés" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Ennek a szövegnek a terminálba való beillesztése veszélyes lehet, mivel " +"néhány parancs végrehajtásra kerülhet." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Kilép a Ghostty-ból?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "Fül bezárása?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "Ablak bezárása?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "Felosztás bezárása?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Minden terminál munkamenet lezárul." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Ezen a fülön minden terminál munkamenet lezárul." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Ebben az ablakban minden terminál munkamenet lezárul." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "Ebben a felosztásban a jelenleg futó folyamat lezárul." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Parancs befejezÅ‘dött" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Parancs sikeres" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Parancs sikertelen" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Parancs sikeres" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Parancs sikertelen" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Terminál címének módosítása" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Fül címének módosítása" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "Konfiguráció frissítve" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Vágólapra másolva" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "Vágólap törölve" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Ghostty fejlesztÅ‘k" diff --git a/po/hu_HU.UTF-8.po b/po/hu_HU.UTF-8.po deleted file mode 100644 index 6a3c618946a..00000000000 --- a/po/hu_HU.UTF-8.po +++ /dev/null @@ -1,355 +0,0 @@ -# Hungarian translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto -# This file is distributed under the same license as the com.mitchellh.ghostty package. -# Balázs Szücs , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-10 18:32+0200\n" -"Last-Translator: Balázs Szücs \n" -"Language-Team: Hungarian \n" -"Language: hu\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Vágólap-hozzáférés engedélyezése" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "Elutasítás" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Engedélyezés" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "Választás megjegyzése erre a felosztásra" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "Konfiguráció frissítése a kérdés újbóli megjelenítéséhez" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "Mégse" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Bezárás" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "Konfigurációs hibák" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"Egy vagy több konfigurációs hiba található. Kérjük, ellenÅ‘rizze az alábbi " -"hibákat, és frissítse a konfigurációt, vagy hagyja figyelmen kívül ezeket a " -"hibákat." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Figyelmen kívül hagyás" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "Konfiguráció frissítése" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"âš ï¸ A Ghostty hibakeresÅ‘ verzióját futtatja! A teljesítmény csökkenni fog." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Terminálvizsgáló" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "Keresés…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "ElÅ‘zÅ‘ találat" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "KövetkezÅ‘ találat" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "Jaj, ne." - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "Nem sikerült OpenGL-környezetet létrehozni a megjelenítéshez." - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"Ez a terminál csak olvasható módban van. A tartalmat továbbra is " -"megtekintheti, kijelölheti és görgetheti, de nem küld bemeneti eseményeket a " -"futó alkalmazásnak." - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "Csak olvasható" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Másolás" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "Beillesztés" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "Értesítés a következÅ‘ parancs befejezésekor" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "Törlés" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "Visszaállítás" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "Felosztás" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "Cím módosítása…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Felosztás felfelé" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "Felosztás lefelé" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Felosztás balra" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "Felosztás jobbra" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Fül" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Új fül" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Fül bezárása" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Ablak" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Új ablak" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Ablak bezárása" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "Konfiguráció" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Konfiguráció megnyitása" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "Hagyja üresen az alapértelmezett cím visszaállításához." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "Rendben" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Új felosztás" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "Megnyitott fülek megtekintése" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "FÅ‘menü" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Parancspaletta" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "Terminálvizsgáló" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "A Ghostty névjegye" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "Kilépés" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "Parancs végrehajtása…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Egy alkalmazás megpróbál írni a vágólapra. A vágólap jelenlegi tartalma lent " -"látható." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Egy alkalmazás megpróbál olvasni a vágólapról. A vágólap jelenlegi tartalma " -"lent látható." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Figyelem: potenciálisan veszélyes beillesztés" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"Ennek a szövegnek a terminálba való beillesztése veszélyes lehet, mivel " -"néhány parancs végrehajtásra kerülhet." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Kilép a Ghostty-ból?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "Fül bezárása?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "Ablak bezárása?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "Felosztás bezárása?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Minden terminál munkamenet lezárul." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Ezen a fülön minden terminál munkamenet lezárul." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Ebben az ablakban minden terminál munkamenet lezárul." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "Ebben a felosztásban a jelenleg futó folyamat lezárul." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "Parancs befejezÅ‘dött" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "Parancs sikeres" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "Parancs sikertelen" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Parancs sikeres" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Parancs sikertelen" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "Terminál címének módosítása" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "Konfiguráció frissítve" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "Vágólapra másolva" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "Vágólap törölve" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Ghostty fejlesztÅ‘k" diff --git a/po/id.po b/po/id.po new file mode 100644 index 00000000000..e5660440ad7 --- /dev/null +++ b/po/id.po @@ -0,0 +1,354 @@ +# Indonesian 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. +# Satrio Bayu Aji , 2026. +# Mikail Muzakki , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-03-08 20:23+0700\n" +"Last-Translator: Mikail Muzakki \n" +"Language-Team: Indonesian \n" +"Language: id\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Buka di Ghostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Mengesahkan akses papan klip" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Tolak" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Izinkan" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Ingat pilihan untuk belahan ini" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "Muat ulang konfigurasi untuk menampilkan pesan ini lagi" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Batal" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Tutup" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Kesalahan konfigurasi" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Ditemukan satu atau lebih kesalahan konfigurasi. Silakan tinjau kesalahan di " +"bawah ini, dan muat ulang konfigurasi anda atau abaikan kesalahan ini." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Abaikan" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Muat ulang konfigurasi" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ Anda sedang menjalankan versi debug dari Ghostty! Performa akan menurun." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspektur terminal" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Cari…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Hasil sebelumnya" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "Hasil berikutnya" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "Oh, tidak." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "Tidak dapat memperoleh konteks OpenGL untuk penampilan grafis." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Terminal ini sedang dalam model hanya baca. Anda hanya bisa melihat, memilih, " +"dan menggulir konten, tetapi peristiwa input tidak akan dikirim ke " +"aplikasi berjalan." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Hanya baca" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Salin" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Tempel" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "Beri tahu saat perintah berikutnya selesai" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "Bersihkan" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Atur ulang" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Belah" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Ubah judul…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Belah atas" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Belah bawah" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Belah kiri" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Belah kanan" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Tab" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Ubah judul tab…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Tab baru" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Tutup tab" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Jendela" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Jendela baru" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Tutup jendela" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "Konfigurasi" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Buka konfigurasi" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "Biarkan kosong untuk mengembalikan judul bawaan." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Belahan baru" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Lihat tab terbuka" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Menu utama" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Palet perintah" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "Inspektur terminal" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "Tentang Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Keluar" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Eksekusi perintah…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Aplikasi sedang mencoba menulis ke papan klip. Isi papan klip saat ini " +"ditampilkan di bawah ini." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Aplikasi sedang mencoba membaca dari papan klip. Isi papan klip saat ini " +"ditampilkan di bawah ini." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Peringatan: Tempelan berpotensi tidak aman" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Menempelkan teks ini ke terminal mungkin berbahaya karena sepertinya " +"beberapa perintah mungkin dijalankan." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Keluar dari Ghostty?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "Tutup tab?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "Tutup jendela?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "Tutup belahan?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Semua sesi terminal akan diakhiri." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Semua sesi terminal di tab ini akan diakhiri." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Semua sesi terminal di jendela ini akan diakhiri." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "Proses yang sedang berjalan dalam belahan ini akan diakhiri." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Perintah selesai" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Perintah berhasil" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Perintah gagal" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Perintah berhasil" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Perintah gagal" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Ubah judul terminal" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Ubah judul tab" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "Memuat ulang konfigurasi" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Disalin ke papan klip" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "Papan klip dibersihkan" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Pengembang Ghostty" diff --git a/po/id_ID.UTF-8.po b/po/id_ID.UTF-8.po deleted file mode 100644 index fc573563dab..00000000000 --- a/po/id_ID.UTF-8.po +++ /dev/null @@ -1,351 +0,0 @@ -# Indonesian 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. -# Satrio Bayu Aji , 2025. -# Mikail Muzakki , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2025-08-01 10:15+0700\n" -"Last-Translator: Mikail Muzakki \n" -"Language-Team: Indonesian \n" -"Language: id\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Mengesahkan akses papan klip" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "Tolak" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Izinkan" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "Ingat pilihan untuk belahan ini" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "Muat ulang konfigurasi untuk menampilkan pesan ini lagi" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "Batal" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Tutup" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "Kesalahan konfigurasi" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"Ditemukan satu atau lebih kesalahan konfigurasi. Silakan tinjau kesalahan di " -"bawah ini, dan muat ulang konfigurasi anda atau abaikan kesalahan ini." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Abaikan" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "Muat ulang konfigurasi" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"âš ï¸ Anda sedang menjalankan versi debug dari Ghostty! Performa akan menurun." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Inspektur terminal" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Salin" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "Tempel" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "Bersihkan" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "Atur ulang" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "Belah" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "Ubah judul…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Belah atas" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "Belah bawah" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Belah kiri" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "Belah kanan" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Tab" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Tab baru" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Tutup tab" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Jendela" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Jendela baru" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Tutup jendela" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "Konfigurasi" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Buka konfigurasi" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "Biarkan kosong untuk mengembalikan judul bawaan." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "OK" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Belahan baru" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "Lihat tab terbuka" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "Menu utama" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Palet perintah" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "Inspektur terminal" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "Tentang Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "Keluar" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "Eksekusi perintah…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Aplikasi sedang mencoba menulis ke papan klip. Isi papan klip saat ini " -"ditampilkan di bawah ini." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Aplikasi sedang mencoba membaca dari papan klip. Isi papan klip saat ini " -"ditampilkan di bawah ini." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Peringatan: Tempelan berpotensi tidak aman" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"Menempelkan teks ini ke terminal mungkin berbahaya karena sepertinya " -"beberapa perintah mungkin dijalankan." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Keluar dari Ghostty?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "Tutup tab?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "Tutup jendela?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "Tutup belahan?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Semua sesi terminal akan diakhiri." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Semua sesi terminal di tab ini akan diakhiri." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Semua sesi terminal di jendela ini akan diakhiri." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "Proses yang sedang berjalan dalam belahan ini akan diakhiri." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Perintah berhasil" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Perintah gagal" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "Ubah judul terminal" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "Memuat ulang konfigurasi" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "Disalin ke papan klip" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "Papan klip dibersihkan" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Pengembang Ghostty" diff --git a/po/it.po b/po/it.po new file mode 100644 index 00000000000..ea203138237 --- /dev/null +++ b/po/it.po @@ -0,0 +1,357 @@ +# Italian translations for com.mitchellh.ghostty package. +# Traduzioni italiane per il pacchetto com.mitchellh.ghostty. +# Copyright (C) 2025 "Mitchell Hashimoto, Ghostty contributors" +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Giacomo Bettini , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2025-09-06 19:40+0200\n" +"Last-Translator: Giacomo Bettini \n" +"Language-Team: Italian \n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Apri in Ghostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Consenti accesso agli Appunti" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Non consentire" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Consenti" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Ricorda scelta per questa divisione" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "" +"Ricarica la configurazione per visualizzare nuovamente questo messaggio" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Annulla" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Chiudi" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Errori di configurazione" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Sono stati trovati uno o più errori di configurazione. Controlla gli errori " +"seguenti, poi ricarica la tua configurazione o ignora quegli errori." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Ignora" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Ricarica configurazione" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ Stai usando una build di debug di Ghostty! Le prestazioni saranno ridotte." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Ispettore del terminale" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Cerca…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Corrispondenza precedente" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "Corrispondenza successiva" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "Oh no!" + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "Impossibile ottenere un contesto OpenGL per il rendering." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Questo terminale è in modalità di sola lettura. Puoi ancora vedere, " +"selezionare e scorrere il contenuto, ma non verrà inviato alcun evento " +"di input all'applicazione in esecuzione." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Sola lettura" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Copia" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Incolla" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "Notifica al termine del prossimo comando" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "Pulisci" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Reimposta" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Divisione" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Cambia titolo…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Dividi in alto" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Dividi in basso" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Dividi a sinistra" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Dividi a destra" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Scheda" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Cambia titolo scheda…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Nuova scheda" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Chiudi scheda" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Finestra" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Nuova finestra" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Chiudi finestra" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "Configurazione" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Apri configurazione" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "Lasciare vuoto per ripristinare il titolo predefinito." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Nuova divisione" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Vedi schede aperte" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Menù principale" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Riquadro comandi" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "Ispettore del terminale" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "Informazioni su Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Chiudi" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Esegui un comando…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Un'applicazione sta cercando di scrivere negli Appunti. Il contenuto attuale " +"degli Appunti è mostrato di seguito." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Un'applicazione sta cercando di leggere dagli Appunti. Il contenuto attuale " +"degli Appunti è mostrato di seguito." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Attenzione: Incolla potenzialmente pericoloso" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Incollare questo testo nel terminale potrebbe essere pericoloso poiché " +"sembra contenere comandi che potrebbero venire eseguiti." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Chiudere Ghostty?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "Chiudere la scheda?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "Chiudere la finestra?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "Chiudere la divisione?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Tutte le sessioni del terminale saranno terminate." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Tutte le sessioni del terminale in questa scheda saranno terminate." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Tutte le sessioni del terminale in questa finestra saranno terminate." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "" +"Il processo attualmente in esecuzione in questa divisione sarà terminato." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Comando terminato" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Comando riuscito" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Comando fallito" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Comando riuscito" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Comando fallito" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Cambia il titolo del terminale" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Cambia il titolo della scheda" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "Configurazione ricaricata" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Copiato negli Appunti" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "Appunti svuotati" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Sviluppatori di Ghostty" diff --git a/po/it_IT.UTF-8.po b/po/it_IT.UTF-8.po deleted file mode 100644 index 87fc0c756bb..00000000000 --- a/po/it_IT.UTF-8.po +++ /dev/null @@ -1,354 +0,0 @@ -# Italian translations for com.mitchellh.ghostty package -# Traduzioni italiane per il pacchetto com.mitchellh.ghostty. -# Copyright (C) 2025 "Mitchell Hashimoto, Ghostty contributors" -# This file is distributed under the same license as the com.mitchellh.ghostty package. -# Giacomo Bettini , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2025-09-06 19:40+0200\n" -"Last-Translator: Giacomo Bettini \n" -"Language-Team: Italian \n" -"Language: it\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Consenti accesso agli Appunti" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "Non consentire" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Consenti" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "Ricorda scelta per questa divisione" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "" -"Ricarica la configurazione per visualizzare nuovamente questo messaggio" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "Annulla" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Chiudi" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "Errori di configurazione" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"Sono stati trovati uno o più errori di configurazione. Controlla gli errori " -"seguenti, poi ricarica la tua configurazione o ignora quegli errori." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Ignora" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "Ricarica configurazione" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"âš ï¸ Stai usando una build di debug di Ghostty! Le prestazioni saranno ridotte." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Ispettore del terminale" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Copia" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "Incolla" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "Pulisci" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "Reimposta" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "Divisione" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "Cambia titolo…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Dividi in alto" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "Dividi in basso" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Dividi a sinistra" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "Dividi a destra" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Scheda" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Nuova scheda" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Chiudi scheda" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Finestra" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Nuova finestra" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Chiudi finestra" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "Configurazione" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Apri configurazione" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "Lasciare vuoto per ripristinare il titolo predefinito." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "OK" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Nuova divisione" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "Vedi schede aperte" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "Menù principale" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Riquadro comandi" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "Ispettore del terminale" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "Informazioni su Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "Chiudi" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "Esegui un comando…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Un'applicazione sta cercando di scrivere negli Appunti. Il contenuto attuale " -"degli Appunti è mostrato di seguito." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Un'applicazione sta cercando di leggere dagli Appunti. Il contenuto attuale " -"degli Appunti è mostrato di seguito." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Attenzione: Incolla potenzialmente pericoloso" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"Incollare questo testo nel terminale potrebbe essere pericoloso poiché " -"sembra contenere comandi che potrebbero venire eseguiti." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Chiudere Ghostty?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "Chiudere la scheda?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "Chiudere la finestra?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "Chiudere la divisione?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Tutte le sessioni del terminale saranno terminate." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Tutte le sessioni del terminale in questa scheda saranno terminate." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Tutte le sessioni del terminale in questa finestra saranno terminate." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "" -"Il processo attualmente in esecuzione in questa divisione sarà terminato." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Comando riuscito" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Comando fallito" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "Cambia il titolo del terminale" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "Configurazione ricaricata" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "Copiato negli Appunti" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "Appunti svuotati" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Sviluppatori di Ghostty" diff --git a/po/ja.po b/po/ja.po new file mode 100644 index 00000000000..299eb81bc0a --- /dev/null +++ b/po/ja.po @@ -0,0 +1,354 @@ +# Japanese translations for com.mitchellh.ghostty package +# com.mitchellh.ghostty パッケージã«å¯¾ã™ã‚‹å’Œè¨³. +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Lon Sagisawa , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-02-11 12:02+0900\n" +"Last-Translator: Takayuki Nagatomi \n" +"Language-Team: Japanese\n" +"Language: ja\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Ghosttyã§é–‹ã" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "クリップボードã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ã‚’承èª" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "æ‹’å¦" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "許å¯" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "ã“ã®åˆ†å‰²ã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ã«å¯¾ã—ã¦è¨­å®šã‚’記憶ã™ã‚‹" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "ã“ã®ãƒ—ロンプトをå†ã³è¡¨ç¤ºã™ã‚‹ã«ã¯è¨­å®šã‚’å†èª­ã¿è¾¼ã¿ã—ã¦ãã ã•ã„" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "キャンセル" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "é–‰ã˜ã‚‹" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "設定ファイルã«ã‚¨ãƒ©ãƒ¼ãŒã‚りã¾ã™" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"設定ファイルã«ã‚¨ãƒ©ãƒ¼ãŒã‚りã¾ã™ã€‚以下ã®ã‚¨ãƒ©ãƒ¼ã‚’確èªã—ã€è¨­å®šãƒ•ァイルã®å†èª­ã¿è¾¼" +"ã¿ã‚’ã™ã‚‹ã‹ã€ç„¡è¦–ã—ã¦ãã ã•ã„。" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "無視" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "設定ファイルã®å†èª­ã¿è¾¼ã¿" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ Ghostty ã®ãƒ‡ãƒãƒƒã‚°ãƒ“ルドを実行ã—ã¦ã„ã¾ã™! パフォーマンスãŒä½Žä¸‹ã—ã¦ã„ã¾ã™ã€‚" + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: 端末インスペクター" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "検索…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "å‰ã®ä¸€è‡´" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "次ã®ä¸€è‡´" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "ãŠã£ã¨ã€‚" + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "レンダリング用ã®OpenGLコンテキストをå–å¾—ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚" + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"ã“ã®ã‚¿ãƒ¼ãƒŸãƒŠãƒ«ã¯èª­ã¿å–り専用モードã§ã™ã€‚コンテンツã®è¡¨ç¤ºã€é¸æŠžã€ã‚¹ã‚¯ãƒ­ãƒ¼ãƒ«ã¯" +"å¯èƒ½ã§ã™ãŒã€å…¥åŠ›ã‚¤ãƒ™ãƒ³ãƒˆã¯å®Ÿè¡Œä¸­ã®ã‚¢ãƒ—リケーションã«é€ä¿¡ã•れã¾ã›ã‚“。" + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "読ã¿å–り専用" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "コピー" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "貼り付ã‘" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "次ã®ã‚³ãƒžãƒ³ãƒ‰å®Ÿè¡Œçµ‚了時ã«é€šçŸ¥ã™ã‚‹" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "クリア" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "リセット" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "分割" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "タイトルを変更…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "上ã«åˆ†å‰²" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "下ã«åˆ†å‰²" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "å·¦ã«åˆ†å‰²" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "å³ã«åˆ†å‰²" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "タブ" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "タブã®ã‚¿ã‚¤ãƒˆãƒ«ã‚’変更…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "æ–°ã—ã„タブ" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "タブを閉ã˜ã‚‹" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "ウィンドウ" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "æ–°ã—ã„ウィンドウ" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "ウィンドウを閉ã˜ã‚‹" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "設定" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "設定ファイルを開ã" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "空白ã«ã—ãŸå ´åˆã€ãƒ‡ãƒ•ォルトã®ã‚¿ã‚¤ãƒˆãƒ«ã‚’使用ã—ã¾ã™ã€‚" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "æ–°ã—ã„分割" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "é–‹ã„ã¦ã„ã‚‹ã™ã¹ã¦ã®ã‚¿ãƒ–を表示" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "メインメニュー" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "コマンドパレット" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "端末インスペクター" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "Ghostty ã«ã¤ã„ã¦" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "終了" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "コマンドを実行…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"アプリケーションãŒã‚¯ãƒªãƒƒãƒ—ãƒœãƒ¼ãƒ‰ã«æ›¸ã込もã†ã¨ã—ã¦ã„ã¾ã™ã€‚ç¾åœ¨ã®ã‚¯ãƒªãƒƒãƒ—ボー" +"ドã®å†…容ã¯ä»¥ä¸‹ã®é€šã‚Šã§ã™ã€‚" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"アプリケーションãŒã‚¯ãƒªãƒƒãƒ—ボードを読ã¿å–ã‚ã†ã¨ã—ã¦ã„ã¾ã™ã€‚ç¾åœ¨ã®ã‚¯ãƒªãƒƒãƒ—ボー" +"ドã®å†…容ã¯ä»¥ä¸‹ã®é€šã‚Šã§ã™ã€‚" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "警告: å±é™ºãªå¯èƒ½æ€§ã®ã‚る貼り付ã‘" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"ã“ã®ãƒ†ã‚­ã‚¹ãƒˆã«ã¯å®Ÿè¡Œå¯èƒ½ãªã‚³ãƒžãƒ³ãƒ‰ãŒå«ã¾ã‚Œã¦ãŠã‚Šã€ã‚¿ãƒ¼ãƒŸãƒŠãƒ«ã«è²¼ã‚Šä»˜ã‘ã‚‹ã®ã¯" +"å±é™ºãªå¯èƒ½æ€§ãŒã‚りã¾ã™ã€‚" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Ghostty を終了ã—ã¾ã™ã‹?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "タブを閉ã˜ã¾ã™ã‹?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "ウィンドウを閉ã˜ã¾ã™ã‹?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "分割ウィンドウを閉ã˜ã¾ã™ã‹?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "ã™ã¹ã¦ã®ã‚¿ãƒ¼ãƒŸãƒŠãƒ«ã‚»ãƒƒã‚·ãƒ§ãƒ³ãŒçµ‚了ã—ã¾ã™ã€‚" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "タブ内ã®ã™ã¹ã¦ã®ã‚¿ãƒ¼ãƒŸãƒŠãƒ«ã‚»ãƒƒã‚·ãƒ§ãƒ³ãŒçµ‚了ã—ã¾ã™ã€‚" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "ウィンドウ内ã®ã™ã¹ã¦ã®ã‚¿ãƒ¼ãƒŸãƒŠãƒ«ã‚»ãƒƒã‚·ãƒ§ãƒ³ãŒçµ‚了ã—ã¾ã™ã€‚" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "分割ウィンドウ内ã®ã™ã¹ã¦ã®ãƒ—ロセスãŒçµ‚了ã—ã¾ã™ã€‚" + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "コマンド実行終了" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "コマンド実行æˆåŠŸ" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "コマンド実行失敗" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "コマンド実行æˆåŠŸ" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "コマンド実行失敗" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "ターミナルã®ã‚¿ã‚¤ãƒˆãƒ«ã‚’変更ã™ã‚‹" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "タブã®ã‚¿ã‚¤ãƒˆãƒ«ã‚’変更ã™ã‚‹" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "設定をå†èª­ã¿è¾¼ã¿ã—ã¾ã—ãŸ" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "クリップボードã«ã‚³ãƒ”ーã—ã¾ã—ãŸ" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "クリップボードを空ã«ã—ã¾ã—ãŸ" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Ghostty 開発者" diff --git a/po/ja_JP.UTF-8.po b/po/ja_JP.UTF-8.po deleted file mode 100644 index 24670dfb928..00000000000 --- a/po/ja_JP.UTF-8.po +++ /dev/null @@ -1,354 +0,0 @@ -# Japanese translations for com.mitchellh.ghostty package -# com.mitchellh.ghostty パッケージã«å¯¾ã™ã‚‹å’Œè¨³. -# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors -# This file is distributed under the same license as the com.mitchellh.ghostty package. -# Lon Sagisawa , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-11 12:02+0900\n" -"Last-Translator: Takayuki Nagatomi \n" -"Language-Team: Japanese\n" -"Language: ja\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "クリップボードã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ã‚’承èª" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "æ‹’å¦" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "許å¯" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "ã“ã®åˆ†å‰²ã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ã«å¯¾ã—ã¦è¨­å®šã‚’記憶ã™ã‚‹" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "ã“ã®ãƒ—ロンプトをå†ã³è¡¨ç¤ºã™ã‚‹ã«ã¯è¨­å®šã‚’å†èª­ã¿è¾¼ã¿ã—ã¦ãã ã•ã„" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "キャンセル" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "é–‰ã˜ã‚‹" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "設定ファイルã«ã‚¨ãƒ©ãƒ¼ãŒã‚りã¾ã™" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"設定ファイルã«ã‚¨ãƒ©ãƒ¼ãŒã‚りã¾ã™ã€‚以下ã®ã‚¨ãƒ©ãƒ¼ã‚’確èªã—ã€è¨­å®šãƒ•ァイルã®å†èª­ã¿è¾¼" -"ã¿ã‚’ã™ã‚‹ã‹ã€ç„¡è¦–ã—ã¦ãã ã•ã„。" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "無視" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "設定ファイルã®å†èª­ã¿è¾¼ã¿" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"âš ï¸ Ghostty ã®ãƒ‡ãƒãƒƒã‚°ãƒ“ルドを実行ã—ã¦ã„ã¾ã™! パフォーマンスãŒä½Žä¸‹ã—ã¦ã„ã¾ã™ã€‚" - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: 端末インスペクター" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "検索…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "å‰ã®ä¸€è‡´" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "次ã®ä¸€è‡´" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "ãŠã£ã¨ã€‚" - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "レンダリング用ã®OpenGLコンテキストをå–å¾—ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚" - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"ã“ã®ã‚¿ãƒ¼ãƒŸãƒŠãƒ«ã¯èª­ã¿å–り専用モードã§ã™ã€‚コンテンツã®è¡¨ç¤ºã€é¸æŠžã€ã‚¹ã‚¯ãƒ­ãƒ¼ãƒ«ã¯" -"å¯èƒ½ã§ã™ãŒã€å…¥åŠ›ã‚¤ãƒ™ãƒ³ãƒˆã¯å®Ÿè¡Œä¸­ã®ã‚¢ãƒ—リケーションã«é€ä¿¡ã•れã¾ã›ã‚“。" - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "読ã¿å–り専用" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "コピー" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "貼り付ã‘" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "次ã®ã‚³ãƒžãƒ³ãƒ‰å®Ÿè¡Œçµ‚了時ã«é€šçŸ¥ã™ã‚‹" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "クリア" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "リセット" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "分割" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "タイトルを変更…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "上ã«åˆ†å‰²" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "下ã«åˆ†å‰²" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "å·¦ã«åˆ†å‰²" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "å³ã«åˆ†å‰²" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "タブ" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "æ–°ã—ã„タブ" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "タブを閉ã˜ã‚‹" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "ウィンドウ" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "æ–°ã—ã„ウィンドウ" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "ウィンドウを閉ã˜ã‚‹" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "設定" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "設定ファイルを開ã" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "空白ã«ã—ãŸå ´åˆã€ãƒ‡ãƒ•ォルトã®ã‚¿ã‚¤ãƒˆãƒ«ã‚’使用ã—ã¾ã™ã€‚" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "OK" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "æ–°ã—ã„分割" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "é–‹ã„ã¦ã„ã‚‹ã™ã¹ã¦ã®ã‚¿ãƒ–を表示" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "メインメニュー" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "コマンドパレット" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "端末インスペクター" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "Ghostty ã«ã¤ã„ã¦" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "終了" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "コマンドを実行…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"アプリケーションãŒã‚¯ãƒªãƒƒãƒ—ãƒœãƒ¼ãƒ‰ã«æ›¸ã込もã†ã¨ã—ã¦ã„ã¾ã™ã€‚ç¾åœ¨ã®ã‚¯ãƒªãƒƒãƒ—ボー" -"ドã®å†…容ã¯ä»¥ä¸‹ã®é€šã‚Šã§ã™ã€‚" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"アプリケーションãŒã‚¯ãƒªãƒƒãƒ—ボードを読ã¿å–ã‚ã†ã¨ã—ã¦ã„ã¾ã™ã€‚ç¾åœ¨ã®ã‚¯ãƒªãƒƒãƒ—ボー" -"ドã®å†…容ã¯ä»¥ä¸‹ã®é€šã‚Šã§ã™ã€‚" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "警告: å±é™ºãªå¯èƒ½æ€§ã®ã‚る貼り付ã‘" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"ã“ã®ãƒ†ã‚­ã‚¹ãƒˆã«ã¯å®Ÿè¡Œå¯èƒ½ãªã‚³ãƒžãƒ³ãƒ‰ãŒå«ã¾ã‚Œã¦ãŠã‚Šã€ã‚¿ãƒ¼ãƒŸãƒŠãƒ«ã«è²¼ã‚Šä»˜ã‘ã‚‹ã®ã¯" -"å±é™ºãªå¯èƒ½æ€§ãŒã‚りã¾ã™ã€‚" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Ghostty を終了ã—ã¾ã™ã‹?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "タブを閉ã˜ã¾ã™ã‹?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "ウィンドウを閉ã˜ã¾ã™ã‹?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "分割ウィンドウを閉ã˜ã¾ã™ã‹?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "ã™ã¹ã¦ã®ã‚¿ãƒ¼ãƒŸãƒŠãƒ«ã‚»ãƒƒã‚·ãƒ§ãƒ³ãŒçµ‚了ã—ã¾ã™ã€‚" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "タブ内ã®ã™ã¹ã¦ã®ã‚¿ãƒ¼ãƒŸãƒŠãƒ«ã‚»ãƒƒã‚·ãƒ§ãƒ³ãŒçµ‚了ã—ã¾ã™ã€‚" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "ウィンドウ内ã®ã™ã¹ã¦ã®ã‚¿ãƒ¼ãƒŸãƒŠãƒ«ã‚»ãƒƒã‚·ãƒ§ãƒ³ãŒçµ‚了ã—ã¾ã™ã€‚" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "分割ウィンドウ内ã®ã™ã¹ã¦ã®ãƒ—ロセスãŒçµ‚了ã—ã¾ã™ã€‚" - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "コマンド実行終了" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "コマンド実行æˆåŠŸ" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "コマンド実行失敗" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "コマンド実行æˆåŠŸ" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "コマンド実行失敗" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "ターミナルã®ã‚¿ã‚¤ãƒˆãƒ«ã‚’変更ã™ã‚‹" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "設定をå†èª­ã¿è¾¼ã¿ã—ã¾ã—ãŸ" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "クリップボードã«ã‚³ãƒ”ーã—ã¾ã—ãŸ" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "クリップボードを空ã«ã—ã¾ã—ãŸ" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Ghostty 開発者" diff --git a/po/kk.po b/po/kk.po new file mode 100644 index 00000000000..91be4a4b3eb --- /dev/null +++ b/po/kk.po @@ -0,0 +1,354 @@ +# Kazakh translation for Ghostty. +# Copyright (C) 2026 "Mitchell Hashimoto, Ghostty contributors" +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Baurzhan Muftakhidinov , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-03-04 21:16+0500\n" +"Last-Translator: Baurzhan Muftakhidinov \n" +"Language-Team: Kazakh \n" +"Language: kk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Ghostty ішінде ашу" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "ÐлмаÑу буферіне қол жеткізуді Ñ€Ò±Ò›Ñат ету" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Тыйым Ñалу" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "РұқÑат ету" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "ОÑÑ‹ бөлу үшін таңдауды еÑте Ñақтау" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "Бұл Ñұрауды қайта көрÑету үшін конфигурациÑны қайта жүктеңіз" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Ð‘Ð°Ñ Ñ‚Ð°Ñ€Ñ‚Ñƒ" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Жабу" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "ÐšÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ò›Ð°Ñ‚ÐµÐ»ÐµÑ€Ñ–" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Бір немеÑе бірнеше ÐºÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ò›Ð°Ñ‚ÐµÑÑ– табылды. Төмендегі қателерді қарап " +"шығып, конфигурациÑны қайта жүктеңіз немеÑе бұл қателерді елемеңіз." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Елемеу" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "КонфигурациÑны қайта жүктеу" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ Ð¡Ñ–Ð· Ghostty жөндеу құраÑтырылымын Ñ–Ñке қоÑып тұрÑыз! Өнімділік төмен болуы " +"мүмкін." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Терминал инÑпекторы" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Табу…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Ðлдыңғы ÑәйкеÑтік" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "КелеÑÑ– ÑәйкеÑтік" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "О, жоқ." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "Рендеринг үшін OpenGL контекÑтін алу мүмкін емеÑ." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Бұл терминал тек оқу режимінде. Сіз әлі де мазмұнды көре, таңдай және жылжыта " +"алаÑыз, бірақ Ñ–Ñке қоÑылған қолданбаға ешқандай енгізу оқиғалары жіберілмейді." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Тек оқу" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Көшіріп алу" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "КіріÑтіру" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "КелеÑÑ– команда аÑқталғанда хабарлау" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "Тазарту" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "ТаÑтау" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Бөлу" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Тақырыпты өзгерту…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Жоғарыға бөлу" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Төменге бөлу" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Сол жаққа бөлу" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Оң жаққа бөлу" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Бет" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Бет атауын өзгерту…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Жаңа бет" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Бетті жабу" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Терезе" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Жаңа терезе" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Терезені жабу" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "КонфигурациÑ" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "КонфигурациÑны ашу" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "БаÑтапқы атауды қалпына келтіру үшін Ð±Ð¾Ñ Ò›Ð°Ð»Ð´Ñ‹Ñ€Ñ‹Ò£Ñ‹Ð·." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "ОК" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Жаңа бөлу" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Ðшық беттерді көру" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Ð‘Ð°Ñ Ð¼Ó™Ð·Ñ–Ñ€" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Командалар палитраÑÑ‹" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "Терминал инÑпекторы" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "Ghostty туралы" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Шығу" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Команданы орындау…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current clipboard " +"contents are shown below." +msgstr "" +"Қолданба алмаÑу буферіне жазуға әрекеттенуде. Ðғымдағы алмаÑу буферінің " +"мазмұны төменде көрÑетілген." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Қолданба алмаÑу буферінен оқуға әрекеттенуде. Ðғымдағы алмаÑу буферінің " +"мазмұны төменде көрÑетілген." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "ЕÑкерту: ҚауіпÑіз ÐµÐ¼ÐµÑ Ð±Ð¾Ð»Ð° алатын кіріÑтіру" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Бұл мәтінді терминалға кіріÑтіру қауіпті болуы мүмкін, Ñебебі кейбір " +"командалар орындалуы мүмкін ÑиÑқты." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Ghostty-ден шығу керек пе?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "Бетті жабу керек пе?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "Терезені жабу керек пе?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "Бөлуді жабу керек пе?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Барлық терминал ÑеÑÑиÑлары тоқтатылады." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "ОÑÑ‹ беттегі барлық терминал ÑеÑÑиÑлары тоқтатылады." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "ОÑÑ‹ терезедегі барлық терминал ÑеÑÑиÑлары тоқтатылады." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "ОÑÑ‹ бөлудегі ағымдағы орындалып жатқан процеÑÑ Ñ‚Ð¾Ò›Ñ‚Ð°Ñ‚Ñ‹Ð»Ð°Ð´Ñ‹." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Команда аÑқталды" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Команда Ñәтті орындалды" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Команда ÑәтÑіз аÑқталды" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Команда Ñәтті орындалды" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Команда ÑәтÑіз аÑқталды" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Терминал атауын өзгерту" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Бет атауын өзгерту" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "ÐšÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ò›Ð°Ð¹Ñ‚Ð° жүктелді" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "ÐлмаÑу буферіне көшірілді" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "ÐлмаÑу буфері тазартылды" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Ghostty әзірлеушілері" diff --git a/po/ko_KR.UTF-8.po b/po/ko_KR.UTF-8.po deleted file mode 100644 index 478ab9c2018..00000000000 --- a/po/ko_KR.UTF-8.po +++ /dev/null @@ -1,352 +0,0 @@ -# Korean translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto -# This file is distributed under the same license as the com.mitchellh.ghostty package. -# Ruben Engelbrecht , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-11 12:50+0900\n" -"Last-Translator: GyuYong Jung \n" -"Language-Team: Korean \n" -"Language: ko\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "í´ë¦½ë³´ë“œ 액세스 권한 부여" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "ê±°ë¶€" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "허용" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "ì´ ë¶„í• ì— ëŒ€í•œ ì„ íƒ ê¸°ì–µí•˜ê¸°" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "ì´ ì°½ì„ ë‹¤ì‹œ 보려면 ì„¤ì •ì„ ë‹¤ì‹œ 불러오세요" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "취소" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "닫기" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "설정 오류" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"ì„¤ì •ì— í•˜ë‚˜ ì´ìƒì˜ 문제가 발견ë˜ì—ˆìŠµë‹ˆë‹¤. 아래 오류를 확ì¸í•œ 후 ì„¤ì •ì„ ë‹¤ì‹œ " -"불러오거나 무시하세요." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "무시" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "설정 ê°’ 다시 불러오기" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "âš ï¸ Ghostty 디버그 빌드로 실행 중입니다! ì„±ëŠ¥ì´ ì €í•˜ë©ë‹ˆë‹¤." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: í„°ë¯¸ë„ ì¸ìŠ¤íŽ™í„°" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "찾기…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "ì´ì „ ê²°ê³¼" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "ë‹¤ìŒ ê²°ê³¼" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "ì´ëŸ°!" - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "ë Œë”ë§ì„ 위한 OpenGL 컨í…스트를 가져올 수 없습니다." - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"ì´ í„°ë¯¸ë„ì€ ì½ê¸° ì „ìš© 모드입니다. 콘í…츠를 ë³´ê³  ì„ íƒí•˜ê³  스í¬ë¡¤í•  수는 있지" -"ë§Œ 실행 ì¤‘ì¸ ì• í”Œë¦¬ì¼€ì´ì…˜ìœ¼ë¡œ ìž…ë ¥ ì´ë²¤íŠ¸ê°€ 전송ë˜ì§€ 않습니다." - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "ì½ê¸° ì „ìš©" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "복사" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "붙여넣기" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "ë‹¤ìŒ ëª…ë ¹ 완료 시 알림" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "지우기" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "초기화" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "나누기" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "제목 변경…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "위로 ì°½ 나누기" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "아래로 ì°½ 나누기" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "왼쪽으로 ì°½ 나누기" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "오른쪽으로 ì°½ 나누기" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "탭" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "새 탭" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "탭 닫기" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "ì°½" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "새 ì°½" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "ì°½ 닫기" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "설정" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "설정 열기" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "ì œëª©ëž€ì„ ë¹„ì›Œ ë‘ë©´ 기본값으로 ë³µì›ë©ë‹ˆë‹¤." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "확ì¸" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "새 ë¶„í• " - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "열린 탭 보기" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "ë©”ì¸ ë©”ë‰´" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "명령 팔레트" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "í„°ë¯¸ë„ ì¸ìŠ¤íŽ™í„°" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "Ghostty ì •ë³´" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "종료" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "ëª…ë ¹ì„ ì‹¤í–‰í•˜ì„¸ìš”â€¦" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"ì‘ìš© í”„ë¡œê·¸ëž¨ì´ í´ë¦½ë³´ë“œì— 쓰기를 시ë„하고 있습니다. 현재 í´ë¦½ë³´ë“œ ë‚´ìš©ì€ ì•„" -"래와 같습니다." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"ì‘ìš© í”„ë¡œê·¸ëž¨ì´ í´ë¦½ë³´ë“œì—서 ì½ê¸°ë¥¼ 시ë„하고 있습니다. 현재 í´ë¦½ë³´ë“œ ë‚´ìš©ì€ " -"아래와 같습니다." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "경고: 잠재ì ìœ¼ë¡œ 안전하지 ì•Šì€ ë¶™ì—¬ë„£ê¸°" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"ì´ í…스트를 터미ë„ì— ë¶™ì—¬ë„£ìœ¼ë©´ ì¼ë¶€ ëª…ë ¹ì´ ì‹¤í–‰ë  ìˆ˜ 있어 위험할 수 있습니" -"다." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Ghostty를 종료하시겠습니까?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "íƒ­ì„ ë‹«ìœ¼ì‹œê² ìŠµë‹ˆê¹Œ?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "ì°½ì„ ë‹«ìœ¼ì‹œê² ìŠµë‹ˆê¹Œ?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "ë¶„í• ì„ ë‹«ìœ¼ì‹œê² ìŠµë‹ˆê¹Œ?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "모든 í„°ë¯¸ë„ ì„¸ì…˜ì´ ì¢…ë£Œë©ë‹ˆë‹¤." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "ì´ íƒ­ì˜ ëª¨ë“  í„°ë¯¸ë„ ì„¸ì…˜ì´ ì¢…ë£Œë©ë‹ˆë‹¤." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "ì´ ì°½ì˜ ëª¨ë“  í„°ë¯¸ë„ ì„¸ì…˜ì´ ì¢…ë£Œë©ë‹ˆë‹¤." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "ì´ ë¶„í• ì—서 현재 실행 ì¤‘ì¸ í”„ë¡œì„¸ìŠ¤ê°€ 종료ë©ë‹ˆë‹¤." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "명령 완료" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "명령 성공" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "명령 실패" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "명령 성공" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "명령 실패" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "í„°ë¯¸ë„ ì œëª© 변경" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "ì„¤ì •ê°’ì„ ë‹¤ì‹œ 불러왔습니다" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "í´ë¦½ë³´ë“œì— 복사ë¨" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "í´ë¦½ë³´ë“œ 지워ì§" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Ghostty 개발ìžë“¤" diff --git a/po/ko_KR.po b/po/ko_KR.po new file mode 100644 index 00000000000..f2559aa011d --- /dev/null +++ b/po/ko_KR.po @@ -0,0 +1,352 @@ +# Korean translations for com.mitchellh.ghostty package. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Ruben Engelbrecht , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-02-11 12:50+0900\n" +"Last-Translator: GyuYong Jung \n" +"Language-Team: Korean \n" +"Language: ko\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Ghosttyì—서 열기" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "í´ë¦½ë³´ë“œ 액세스 권한 부여" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "ê±°ë¶€" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "허용" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "ì´ ë¶„í• ì— ëŒ€í•œ ì„ íƒ ê¸°ì–µí•˜ê¸°" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "ì´ ì°½ì„ ë‹¤ì‹œ 보려면 ì„¤ì •ì„ ë‹¤ì‹œ 불러오세요" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "취소" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "닫기" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "설정 오류" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"ì„¤ì •ì— í•˜ë‚˜ ì´ìƒì˜ 문제가 발견ë˜ì—ˆìŠµë‹ˆë‹¤. 아래 오류를 확ì¸í•œ 후 ì„¤ì •ì„ ë‹¤ì‹œ " +"불러오거나 무시하세요." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "무시" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "설정 ê°’ 다시 불러오기" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "âš ï¸ Ghostty 디버그 빌드로 실행 중입니다! ì„±ëŠ¥ì´ ì €í•˜ë©ë‹ˆë‹¤." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: í„°ë¯¸ë„ ì¸ìŠ¤íŽ™í„°" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "찾기…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "ì´ì „ ê²°ê³¼" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "ë‹¤ìŒ ê²°ê³¼" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "ì´ëŸ°!" + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "ë Œë”ë§ì„ 위한 OpenGL 컨í…스트를 가져올 수 없습니다." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"ì´ í„°ë¯¸ë„ì€ ì½ê¸° ì „ìš© 모드입니다. 콘í…츠를 ë³´ê³  ì„ íƒí•˜ê³  스í¬ë¡¤í•  수는 있지" +"ë§Œ 실행 ì¤‘ì¸ ì• í”Œë¦¬ì¼€ì´ì…˜ìœ¼ë¡œ ìž…ë ¥ ì´ë²¤íŠ¸ê°€ 전송ë˜ì§€ 않습니다." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "ì½ê¸° ì „ìš©" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "복사" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "붙여넣기" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "ë‹¤ìŒ ëª…ë ¹ 완료 시 알림" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "지우기" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "초기화" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "나누기" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "제목 변경…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "위로 ì°½ 나누기" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "아래로 ì°½ 나누기" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "왼쪽으로 ì°½ 나누기" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "오른쪽으로 ì°½ 나누기" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "탭" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "탭 제목 변경…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "새 탭" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "탭 닫기" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "ì°½" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "새 ì°½" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "ì°½ 닫기" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "설정" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "설정 열기" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "ì œëª©ëž€ì„ ë¹„ì›Œ ë‘ë©´ 기본값으로 ë³µì›ë©ë‹ˆë‹¤." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "확ì¸" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "새 ë¶„í• " + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "열린 탭 보기" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "ë©”ì¸ ë©”ë‰´" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "명령 팔레트" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "í„°ë¯¸ë„ ì¸ìŠ¤íŽ™í„°" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "Ghostty ì •ë³´" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "종료" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "ëª…ë ¹ì„ ì‹¤í–‰í•˜ì„¸ìš”â€¦" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"ì‘ìš© í”„ë¡œê·¸ëž¨ì´ í´ë¦½ë³´ë“œì— 쓰기를 시ë„하고 있습니다. 현재 í´ë¦½ë³´ë“œ ë‚´ìš©ì€ ì•„" +"래와 같습니다." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"ì‘ìš© í”„ë¡œê·¸ëž¨ì´ í´ë¦½ë³´ë“œì—서 ì½ê¸°ë¥¼ 시ë„하고 있습니다. 현재 í´ë¦½ë³´ë“œ ë‚´ìš©ì€ " +"아래와 같습니다." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "경고: 잠재ì ìœ¼ë¡œ 안전하지 ì•Šì€ ë¶™ì—¬ë„£ê¸°" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"ì´ í…스트를 터미ë„ì— ë¶™ì—¬ë„£ìœ¼ë©´ ì¼ë¶€ ëª…ë ¹ì´ ì‹¤í–‰ë  ìˆ˜ 있어 위험할 수 있습니" +"다." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Ghostty를 종료하시겠습니까?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "íƒ­ì„ ë‹«ìœ¼ì‹œê² ìŠµë‹ˆê¹Œ?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "ì°½ì„ ë‹«ìœ¼ì‹œê² ìŠµë‹ˆê¹Œ?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "ë¶„í• ì„ ë‹«ìœ¼ì‹œê² ìŠµë‹ˆê¹Œ?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "모든 í„°ë¯¸ë„ ì„¸ì…˜ì´ ì¢…ë£Œë©ë‹ˆë‹¤." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "ì´ íƒ­ì˜ ëª¨ë“  í„°ë¯¸ë„ ì„¸ì…˜ì´ ì¢…ë£Œë©ë‹ˆë‹¤." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "ì´ ì°½ì˜ ëª¨ë“  í„°ë¯¸ë„ ì„¸ì…˜ì´ ì¢…ë£Œë©ë‹ˆë‹¤." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "ì´ ë¶„í• ì—서 현재 실행 ì¤‘ì¸ í”„ë¡œì„¸ìŠ¤ê°€ 종료ë©ë‹ˆë‹¤." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "명령 완료" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "명령 성공" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "명령 실패" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "명령 성공" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "명령 실패" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "í„°ë¯¸ë„ ì œëª© 변경" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "탭 제목 변경" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "ì„¤ì •ê°’ì„ ë‹¤ì‹œ 불러왔습니다" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "í´ë¦½ë³´ë“œì— 복사ë¨" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "í´ë¦½ë³´ë“œ 지워ì§" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Ghostty 개발ìžë“¤" diff --git a/po/lt.po b/po/lt.po new file mode 100644 index 00000000000..ba4995ddc43 --- /dev/null +++ b/po/lt.po @@ -0,0 +1,353 @@ +# 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: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-02-20 12:13+0100\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" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Atidaryti su Ghostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Leisti prieigÄ… prie iÅ¡karpinÄ—s" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Drausti" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Leisti" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Prisiminti pasirinkimÄ… Å¡iam padalijimui" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +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.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "AtÅ¡aukti" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Uždaryti" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "KonfigÅ«racijos klaidos" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +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.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Ignoruoti" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "IÅ¡ naujo įkelti konfigÅ«racijÄ…" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +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/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: terminalo inspektorius" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Rasti…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Ankstesnis atitikmuo" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "Kitas atitikmuo" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "Oi, ne." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "Nepavyko gauti OpenGL konteksto vaizdavimui." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Å is terminalas yra tik skaitymui. Vis tiek galite peržiÅ«rÄ—ti, pasirinkti ir " +"slinkti per turinį, taÄiau jokie įvesties įvykiai nebus siunÄiami " +"veikianÄiai programai." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Tik skaitymui" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Kopijuoti" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Ä®klijuoti" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "PraneÅ¡ti apie sekanÄios komandos užbaigimÄ…" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "IÅ¡valyti" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Atstatyti" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Padalinti" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Keisti pavadinimą…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Padalinti aukÅ¡tyn" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Padalinti žemyn" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Padalinti kairÄ—n" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Padalinti deÅ¡inÄ—n" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "KortelÄ—" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Keisti kortelÄ—s pavadinimą…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Nauja kortelÄ—" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Uždaryti kortelÄ™" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Langas" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Naujas langas" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Uždaryti langÄ…" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "KonfigÅ«racija" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Atidaryti konfigÅ«racijÄ…" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "Palikite tuÅ¡ÄiÄ…, kad atkurtumÄ—te numatytÄ…jį pavadinimÄ…." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "Gerai" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Naujas padalijimas" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "PeržiÅ«rÄ—ti atidarytas korteles" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Pagrindinis meniu" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Komandų paletÄ—" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "Terminalo inspektorius" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "Apie Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "IÅ¡eiti" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Vykdyti komandą…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +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/class/clipboard_confirmation_dialog.zig:202 +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/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Ä®spÄ—jimas: galimai nesaugus įklijavimas" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +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/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "IÅ¡eiti iÅ¡ Ghostty?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "Uždaryti kortelÄ™?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "Uždaryti langÄ…?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "Uždaryti padalijimÄ…?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Visos terminalo sesijos bus nutrauktos." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Visos terminalo sesijos Å¡ioje kortelÄ—je bus nutrauktos." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Visos terminalo sesijos Å¡iame lange bus nutrauktos." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "Å iuo metu vykdomas procesas Å¡iame padalijime bus nutrauktas." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Komanda užbaigta" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Komanda sÄ—kminga" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Komanda nepavyko" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Komanda sÄ—kminga" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Komanda nepavyko" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Keisti terminalo pavadinimÄ…" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Keisti kortelÄ—s pavadinimÄ…" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "KonfigÅ«racija įkelta iÅ¡ naujo" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Nukopijuota į iÅ¡karpinÄ™" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "IÅ¡karpinÄ— iÅ¡valyta" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Ghostty kÅ«rÄ—jai" diff --git a/po/lt_LT.UTF-8.po b/po/lt_LT.UTF-8.po deleted file mode 100644 index ed0ec86e6ac..00000000000 --- a/po/lt_LT.UTF-8.po +++ /dev/null @@ -1,353 +0,0 @@ -# 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: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-10 08:14+0100\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" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Leisti prieigÄ… prie iÅ¡karpinÄ—s" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "Drausti" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Leisti" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "Prisiminti pasirinkimÄ… Å¡iam padalijimui" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -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.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "AtÅ¡aukti" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Uždaryti" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "KonfigÅ«racijos klaidos" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -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.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Ignoruoti" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "IÅ¡ naujo įkelti konfigÅ«racijÄ…" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -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/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: terminalo inspektorius" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "Rasti…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "Ankstesnis atitikmuo" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "Kitas atitikmuo" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "Oi, ne." - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "Nepavyko gauti OpenGL konteksto vaizdavimui." - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"Å is terminalas yra tik skaitymui. Vis tiek galite peržiÅ«rÄ—ti, pasirinkti ir " -"slinkti per turinį, taÄiau jokie įvesties įvykiai nebus siunÄiami " -"veikianÄiai programai." - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "Tik skaitymui" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Kopijuoti" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "Ä®klijuoti" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "PraneÅ¡ti apie sekanÄios komandos užbaigimÄ…" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "IÅ¡valyti" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "Atstatyti" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "Padalinti" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "Keisti pavadinimą…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Padalinti aukÅ¡tyn" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "Padalinti žemyn" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Padalinti kairÄ—n" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "Padalinti deÅ¡inÄ—n" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "KortelÄ—" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Nauja kortelÄ—" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Uždaryti kortelÄ™" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Langas" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Naujas langas" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Uždaryti langÄ…" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "KonfigÅ«racija" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Atidaryti konfigÅ«racijÄ…" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "Palikite tuÅ¡ÄiÄ…, kad atkurtumÄ—te numatytÄ…jį pavadinimÄ…." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "Gerai" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Naujas padalijimas" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "PeržiÅ«rÄ—ti atidarytas korteles" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "Pagrindinis meniu" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Komandų paletÄ—" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "Terminalo inspektorius" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "Apie Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "IÅ¡eiti" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "Vykdyti komandą…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -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/class/clipboard_confirmation_dialog.zig:202 -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/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Ä®spÄ—jimas: galimai nesaugus įklijavimas" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -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/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "IÅ¡eiti iÅ¡ Ghostty?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "Uždaryti kortelÄ™?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "Uždaryti langÄ…?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "Uždaryti padalijimÄ…?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Visos terminalo sesijos bus nutrauktos." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Visos terminalo sesijos Å¡ioje kortelÄ—je bus nutrauktos." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Visos terminalo sesijos Å¡iame lange bus nutrauktos." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "Å iuo metu vykdomas procesas Å¡iame padalijime bus nutrauktas." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "Komanda užbaigta" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "Komanda sÄ—kminga" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "Komanda nepavyko" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Komanda sÄ—kminga" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Komanda nepavyko" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "Keisti terminalo pavadinimÄ…" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "KonfigÅ«racija įkelta iÅ¡ naujo" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "Nukopijuota į iÅ¡karpinÄ™" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "IÅ¡karpinÄ— iÅ¡valyta" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Ghostty kÅ«rÄ—jai" diff --git a/po/lv.po b/po/lv.po new file mode 100644 index 00000000000..d8d6313fcbe --- /dev/null +++ b/po/lv.po @@ -0,0 +1,352 @@ +# Latvian translations for com.mitchellh.ghostty package. +# Copyright (C) 2026 "Mitchell Hashimoto, Ghostty contributors" +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Ä’riks Remess , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-18 11:34+0200\n" +"PO-Revision-Date: 2026-02-09 03:24+0200\n" +"Last-Translator: Ä’riks Remess \n" +"Language-Team: Latvian\n" +"Language: LV\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n!=0 ? 1 : 2);\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "AtvÄ“rt ar Ghostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Atļaut piekļuvi starpliktuvei" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "NoraidÄ«t" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Atļaut" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "AtcerÄ“ties izvÄ“li Å¡im sadalÄ«jumam" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "PÄrlÄdÄ“jiet konfigurÄciju, lai Å¡o uzvedni rÄdÄ«tu atkal" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Atcelt" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "AizvÄ“rt" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "KonfigurÄcijas kļūdas" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Tika atrasta viena vai vairÄkas konfigurÄcijas kļūdas. LÅ«dzu, pÄrskatiet " +"zemÄk redzamÄs kļūdas un pÄrlÄdÄ“jiet konfigurÄciju vai ignorÄ“jiet tÄs." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "IgnorÄ“t" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "PÄrlÄdÄ“t konfigurÄciju" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "âš ï¸ JÅ«s izmantojat Ghostty atkļūdoÅ¡anas bÅ«vÄ“jumu! VeiktspÄ“ja bÅ«s zemÄka." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: TerminÄļa inspektors" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "MeklÄ“t…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "IepriekšējÄ atbilstÄ«ba" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "NÄkamÄ atbilstÄ«ba" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "Ak, nÄ“." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "NeizdevÄs iegÅ«t OpenGL kontekstu attÄ“loÅ¡anai." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Å is terminÄlis ir tikai lasīšanas režīmÄ. JÅ«s joprojÄm varat skatÄ«t, atlasÄ«t " +"un ritinÄt saturu, taÄu ievade netiks sÅ«tÄ«ta palaistajai lietotnei." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Tikai lasīšanai" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "KopÄ“t" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "IelÄ«mÄ“t" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "Paziņot, kad nÄkamÄ komanda bÅ«s izpildÄ«ta" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "NotÄ«rÄ«t" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "AtiestatÄ«t" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "SadalÄ«t" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "MainÄ«t virsrakstu…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "SadalÄ«t uz augÅ¡u" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "SadalÄ«t uz leju" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "SadalÄ«t pa kreisi" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "SadalÄ«t pa labi" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Cilne" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "MainÄ«t cilnes virsrakstu…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Jauna cilne" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "AizvÄ“rt cilni" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Logs" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Jauns logs" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "AizvÄ“rt logu" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "KonfigurÄcija" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "AtvÄ“rt konfigurÄciju" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "AtstÄj tukÅ¡u, lai atjaunotu noklusÄ“to virsrakstu." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "Labi" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Jauns sadalÄ«jums" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "SkatÄ«t atvÄ“rtÄs cilnes" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "GalvenÄ izvÄ“lne" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Komandu palete" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "TerminÄļa inspektors" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "Par Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Iziet" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "IzpildÄ«t komandu…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Lietotne mēģina rakstÄ«t starpliktuvÄ“. ZemÄk ir redzams paÅ¡reizÄ“jais " +"starpliktuves saturs." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Lietotne mēģina lasÄ«t no starpliktuves. ZemÄk ir redzams paÅ¡reizÄ“jais " +"starpliktuves saturs." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "BrÄ«dinÄjums: potenciÄli nedroÅ¡a ielÄ«mēšana" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Å Ä« teksta ielÄ«mēšana terminÄlÄ« var bÅ«t bÄ«stama, jo izskatÄs, ka var tikt " +"izpildÄ«tas komandas." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Iziet no Ghostty?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "AizvÄ“rt cilni?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "AizvÄ“rt logu?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "AizvÄ“rt sadalÄ«jumu?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Visas terminÄļa sesijas tiks pÄrtrauktas." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Visas terminÄļa sesijas Å¡ajÄ cilnÄ“ tiks pÄrtrauktas." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Visas terminÄļa sesijas Å¡ajÄ logÄ tiks pÄrtrauktas." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "PaÅ¡laik palaistais process Å¡ajÄ sadalÄ«jumÄ tiks pÄrtraukts." + +#: src/apprt/gtk/class/surface.zig:1104 +msgid "Command Finished" +msgstr "Komanda izpildÄ«ta" + +#: src/apprt/gtk/class/surface.zig:1105 +msgid "Command Succeeded" +msgstr "Komanda izdevÄs" + +#: src/apprt/gtk/class/surface.zig:1106 +msgid "Command Failed" +msgstr "Komanda neizdevÄs" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Komanda izdevÄs" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Komanda neizdevÄs" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "MainÄ«t terminÄļa virsrakstu" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "MainÄ«t cilnes virsrakstu" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "KonfigurÄcija pÄrlÄdÄ“ta" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "NokopÄ“ts starpliktuvÄ“" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "Starpliktuve notÄ«rÄ«ta" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Ghostty izstrÄdÄtÄji" diff --git a/po/lv_LV.UTF-8.po b/po/lv_LV.UTF-8.po deleted file mode 100644 index 96d340175ae..00000000000 --- a/po/lv_LV.UTF-8.po +++ /dev/null @@ -1,352 +0,0 @@ -# Latvian translations for com.mitchellh.ghostty package. -# Copyright (C) 2026 "Mitchell Hashimoto, Ghostty contributors" -# This file is distributed under the same license as the com.mitchellh.ghostty package. -# Ä’riks Remess , 2026. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-09 03:24+0200\n" -"Last-Translator: Ä’riks Remess \n" -"Language-Team: Latvian\n" -"Language: LV\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n!=0 ? 1 : 2);\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Atļaut piekļuvi starpliktuvei" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "NoraidÄ«t" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Atļaut" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "AtcerÄ“ties izvÄ“li Å¡im sadalÄ«jumam" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "PÄrlÄdÄ“jiet konfigurÄciju, lai Å¡o uzvedni rÄdÄ«tu atkal" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "Atcelt" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "AizvÄ“rt" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "KonfigurÄcijas kļūdas" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"Tika atrasta viena vai vairÄkas konfigurÄcijas kļūdas. LÅ«dzu, pÄrskatiet " -"zemÄk redzamÄs kļūdas un pÄrlÄdÄ“jiet konfigurÄciju vai ignorÄ“jiet tÄs." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "IgnorÄ“t" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "PÄrlÄdÄ“t konfigurÄciju" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "âš ï¸ JÅ«s izmantojat Ghostty atkļūdoÅ¡anas bÅ«vÄ“jumu! VeiktspÄ“ja bÅ«s zemÄka." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: TerminÄļa inspektors" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "MeklÄ“t…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "IepriekšējÄ atbilstÄ«ba" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "NÄkamÄ atbilstÄ«ba" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "Ak, nÄ“." - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "NeizdevÄs iegÅ«t OpenGL kontekstu attÄ“loÅ¡anai." - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"Å is terminÄlis ir tikai lasīšanas režīmÄ. JÅ«s joprojÄm varat skatÄ«t, atlasÄ«t " -"un ritinÄt saturu, taÄu ievade netiks sÅ«tÄ«ta palaistajai lietotnei." - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "Tikai lasīšanai" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "KopÄ“t" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "IelÄ«mÄ“t" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "Paziņot, kad nÄkamÄ komanda bÅ«s izpildÄ«ta" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "NotÄ«rÄ«t" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "AtiestatÄ«t" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "SadalÄ«t" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "MainÄ«t virsrakstu…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "SadalÄ«t uz augÅ¡u" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "SadalÄ«t uz leju" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "SadalÄ«t pa kreisi" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "SadalÄ«t pa labi" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Cilne" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Jauna cilne" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "AizvÄ“rt cilni" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Logs" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Jauns logs" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "AizvÄ“rt logu" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "KonfigurÄcija" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "AtvÄ“rt konfigurÄciju" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "AtstÄj tukÅ¡u, lai atjaunotu noklusÄ“to virsrakstu." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "Labi" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Jauns sadalÄ«jums" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "SkatÄ«t atvÄ“rtÄs cilnes" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "GalvenÄ izvÄ“lne" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Komandu palete" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "TerminÄļa inspektors" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "Par Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "Iziet" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "IzpildÄ«t komandu…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Lietotne mēģina rakstÄ«t starpliktuvÄ“. ZemÄk ir redzams paÅ¡reizÄ“jais " -"starpliktuves saturs." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Lietotne mēģina lasÄ«t no starpliktuves. ZemÄk ir redzams paÅ¡reizÄ“jais " -"starpliktuves saturs." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "BrÄ«dinÄjums: potenciÄli nedroÅ¡a ielÄ«mēšana" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"Å Ä« teksta ielÄ«mēšana terminÄlÄ« var bÅ«t bÄ«stama, jo izskatÄs, ka var tikt " -"izpildÄ«tas komandas." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Iziet no Ghostty?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "AizvÄ“rt cilni?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "AizvÄ“rt logu?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "AizvÄ“rt sadalÄ«jumu?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Visas terminÄļa sesijas tiks pÄrtrauktas." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Visas terminÄļa sesijas Å¡ajÄ cilnÄ“ tiks pÄrtrauktas." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Visas terminÄļa sesijas Å¡ajÄ logÄ tiks pÄrtrauktas." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "PaÅ¡laik palaistais process Å¡ajÄ sadalÄ«jumÄ tiks pÄrtraukts." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "Komanda izpildÄ«ta" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "Komanda izdevÄs" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "Komanda neizdevÄs" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Komanda izdevÄs" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Komanda neizdevÄs" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "MainÄ«t terminÄļa virsrakstu" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "KonfigurÄcija pÄrlÄdÄ“ta" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "NokopÄ“ts starpliktuvÄ“" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "Starpliktuve notÄ«rÄ«ta" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Ghostty izstrÄdÄtÄji" diff --git a/po/mk.po b/po/mk.po new file mode 100644 index 00000000000..0307fff95f1 --- /dev/null +++ b/po/mk.po @@ -0,0 +1,355 @@ +# Macedonian 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. +# Andrej Daskalov , 2025. +# Marija Gjorgjieva Gjondeva , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-02-12 17:00+0100\n" +"Last-Translator: Andrej Daskalov \n" +"Language-Team: Macedonian\n" +"Language: mk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Отвори во Ghostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Ðвторизирај приÑтап до привремена меморија" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Одбиј" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Дозволи" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Запомни го изборот за оваа поделба" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "Одново вчитај конфигурација за да Ñе повторно прикаже пораката" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Откажи" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Затвори" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Грешки во конфигурацијата" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Пронајдени Ñе една или повеќе грешки во конфигурацијата. Прегледајте ги " +"грешките подолу и повторно вчитајте ја конфигурацијата или игнорирајте ги " +"овие грешки." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Игнорирај" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Одново вчитај конфигурација" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ Ð˜Ð·Ð²Ñ€ÑˆÑƒÐ²Ð°Ñ‚Ðµ дебаг верзија на Ghostty! ПерформанÑите ќе бидат намалени." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: ИнÑпектор на терминал" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Пронајди…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Претходно Ñовпаѓање" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "Следно Ñовпаѓање" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "УпÑ." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "Ðе може да Ñе добие OpenGL контекÑÑ‚ за рендерирање." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Овој терминал е во режим за читање. Сè уште можете да гледате, избирате и да " +"Ñе движите низ Ñодржината, но влезните наÑтани нема да бидат иÑпратени до " +"апликацијата." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Само читање" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Копирај" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Вметни" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "ИзвеÑти по завршување на Ñледната команда" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "ИÑчиÑти" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "РеÑетирај" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Подели" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Промени наÑлов…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Подели нагоре" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Подели надолу" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Подели налево" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Подели надеÑно" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Јазиче" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Промени наÑлов на јазиче…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Ðово јазиче" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Затвори јазиче" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Прозор" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Ðов прозор" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Затвори прозор" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "Конфигурација" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Отвори конфигурација" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "ОÑтавете празно за враќање на ÑтандарÑниот наÑлов." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "Во ред" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Ðова поделба" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Прегледај отворени јазичиња" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Главно мени" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Командна палета" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "ИнÑпектор на терминал" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "За Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Излез" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Изврши команда…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Ðпликација Ñе обидува да запише во привремената меморија. Содржината е " +"прикажана подолу." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Ðпликација Ñе обидува да чита од привремената меморија. Содржината е " +"прикажана подолу." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Предупредување: Потенцијално небезбедно вметнување" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Вметнувањето на овој текÑÑ‚ во терминалот може да биде опаÑно, бидејќи " +"изгледа како да ќе Ñе извршат одредени команди." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Излези од Ghostty?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "Затвори јазиче?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "Затвори прозор?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "Затвори поделба?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Сите ÑеÑии на терминал ќе бидат прекинати." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Сите ÑеÑии во ова јазиче ќе бидат прекинати." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Сите ÑеÑии во овој прозорец ќе бидат прекинати." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "ПроцеÑот кој моментално Ñе извршува во оваа поделба ќе биде прекинат." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Командата заврши" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Командата уÑпеа" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Командата не уÑпеа" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Командата уÑпеа" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Командата не уÑпеа" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Промени наÑлов на терминал" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Промени наÑлов на јазиче" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "Конфигурацијата е одново вчитана" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Копирано во привремена меморија" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "ИÑчиÑтена привремена меморија" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Развивачи на Ghostty" diff --git a/po/mk_MK.UTF-8.po b/po/mk_MK.UTF-8.po deleted file mode 100644 index 539283271df..00000000000 --- a/po/mk_MK.UTF-8.po +++ /dev/null @@ -1,355 +0,0 @@ -# Macedonian 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. -# Andrej Daskalov , 2025. -# Marija Gjorgjieva Gjondeva , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-12 17:00+0100\n" -"Last-Translator: Andrej Daskalov \n" -"Language-Team: Macedonian\n" -"Language: mk\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Ðвторизирај приÑтап до привремена меморија" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "Одбиј" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Дозволи" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "Запомни го изборот за оваа поделба" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "Одново вчитај конфигурација за да Ñе повторно прикаже пораката" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "Откажи" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Затвори" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "Грешки во конфигурацијата" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"Пронајдени Ñе една или повеќе грешки во конфигурацијата. Прегледајте ги " -"грешките подолу и повторно вчитајте ја конфигурацијата или игнорирајте ги " -"овие грешки." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Игнорирај" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "Одново вчитај конфигурација" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"âš ï¸ Ð˜Ð·Ð²Ñ€ÑˆÑƒÐ²Ð°Ñ‚Ðµ дебаг верзија на Ghostty! ПерформанÑите ќе бидат намалени." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: ИнÑпектор на терминал" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "Пронајди…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "Претходно Ñовпаѓање" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "Следно Ñовпаѓање" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "УпÑ." - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "Ðе може да Ñе добие OpenGL контекÑÑ‚ за рендерирање." - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"Овој терминал е во режим за читање. Сè уште можете да гледате, избирате и да " -"Ñе движите низ Ñодржината, но влезните наÑтани нема да бидат иÑпратени до " -"апликацијата." - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "Само читање" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Копирај" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "Вметни" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "ИзвеÑти по завршување на Ñледната команда" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "ИÑчиÑти" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "РеÑетирај" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "Подели" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "Промени наÑлов…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Подели нагоре" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "Подели надолу" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Подели налево" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "Подели надеÑно" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Јазиче" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Ðово јазиче" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Затвори јазиче" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Прозор" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Ðов прозор" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Затвори прозор" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "Конфигурација" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Отвори конфигурација" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "ОÑтавете празно за враќање на ÑтандарÑниот наÑлов." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "Во ред" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Ðова поделба" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "Прегледај отворени јазичиња" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "Главно мени" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Командна палета" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "ИнÑпектор на терминал" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "За Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "Излез" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "Изврши команда…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Ðпликација Ñе обидува да запише во привремената меморија. Содржината е " -"прикажана подолу." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Ðпликација Ñе обидува да чита од привремената меморија. Содржината е " -"прикажана подолу." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Предупредување: Потенцијално небезбедно вметнување" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"Вметнувањето на овој текÑÑ‚ во терминалот може да биде опаÑно, бидејќи " -"изгледа како да ќе Ñе извршат одредени команди." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Излези од Ghostty?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "Затвори јазиче?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "Затвори прозор?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "Затвори поделба?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Сите ÑеÑии на терминал ќе бидат прекинати." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Сите ÑеÑии во ова јазиче ќе бидат прекинати." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Сите ÑеÑии во овој прозорец ќе бидат прекинати." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "ПроцеÑот кој моментално Ñе извршува во оваа поделба ќе биде прекинат." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "Командата заврши" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "Командата уÑпеа" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "Командата не уÑпеа" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Командата уÑпеа" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Командата не уÑпеа" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "Промени наÑлов на терминал" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "Конфигурацијата е одново вчитана" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "Копирано во привремена меморија" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "ИÑчиÑтена привремена меморија" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Развивачи на Ghostty" diff --git a/po/nb.po b/po/nb.po new file mode 100644 index 00000000000..8a81c3e59f5 --- /dev/null +++ b/po/nb.po @@ -0,0 +1,356 @@ +# Norwegian Bokmal 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. +# Hanna Rose , 2025. +# Uzair Aftab , 2025. +# Christoffer Tønnessen , 2025. +# cryptocode , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-02-12 15:50+0000\n" +"Last-Translator: Hanna Rose \n" +"Language-Team: Norwegian Bokmal \n" +"Language: nb\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Ã…pne i Ghostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Gi tilgang til utklippstavlen" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "AvslÃ¥" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Tillat" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Husk valget for dette delte vinduet?" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "Last inn konfigurasjonen pÃ¥ nytt for Ã¥ vise denne meldingen igjen" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Avbryt" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Lukk" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Konfigurasjonsfeil" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Én eller flere konfigurasjonsfeil ble funnet. Vennligst gjennomgÃ¥ feilene " +"under, og enten last konfigurasjonen din pÃ¥ nytt eller ignorer disse feilene." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Ignorer" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Last konfigurasjon pÃ¥ nytt" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "âš ï¸ Du kjører et debug-bygg av Ghostty. Debug-bygg har redusert ytelse." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Terminalinspektør" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Finn…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Forrige treff" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "Neste treff" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "Ã…, nei." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "Kan ikke hente en OpenGL-kontekst for rendering." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Denne terminalen er i skrivebeskyttet modus. Du kan fortsatt se, markere og " +"bla gjennom innholdet, men ingen inndatahendelser vil bli sendt til den " +"kjørende applikasjonen." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Skrivebeskyttet" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Kopier" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Lim inn" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "Varsle nÃ¥r neste kommandoen fullføres" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "Fjern" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Nullstill" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Del vindu" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Endre tittel…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Del oppover" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Del nedover" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Del til venstre" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Del til høyre" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Fane" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Endre fanetittel…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Ny fane" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Lukk fane" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Vindu" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Nytt vindu" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Lukk vindu" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "Konfigurasjon" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Ã…pne konfigurasjon" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "Blank verdi gjenoppretter standardtittelen." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Del opp vindu" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Se Ã¥pne faner" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Hovedmeny" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Kommandopalett" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "Terminalinspektør" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "Om Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Avslutt" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Kjør en kommando…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"En applikasjon forsøker Ã¥ skrive til utklippstavlen. Gjeldende " +"utklippstavleinnhold er vist nedenfor." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"En applikasjon forsøker Ã¥ lese fra utklippstavlen. Gjeldende " +"utklippstavleinnhold er vist nedenfor." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Adarsel: Lim inn kan være utrygt" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Det ser ut som at kommandoer vil bli kjørt hvis du limer inn dette, vurder " +"om du mener det er trygt." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Avslutt Ghostty?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "Lukk fane?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "Lukk vindu?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "Lukk delt vindu?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Alle terminaløkter vil bli avsluttet." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Alle terminaløkter i denne fanen vil bli avsluttet." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Alle terminaløkter i dette vinduet vil bli avsluttet." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "Den kjørende prosessen for denne splitten vil bli avsluttet." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Kommandoen fullført" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Kommandoen lyktes" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Kommandoen mislyktes" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Kommando lyktes" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Kommando mislyktes" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Endre terminaltittel" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Endre fanetittel" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "Konfigurasjonen ble lastet pÃ¥ nytt" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Kopiert til utklippstavlen" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "Utklippstavle tømt" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Ghostty-utviklere" diff --git a/po/nb_NO.UTF-8.po b/po/nb_NO.UTF-8.po deleted file mode 100644 index 68d470ceb1a..00000000000 --- a/po/nb_NO.UTF-8.po +++ /dev/null @@ -1,356 +0,0 @@ -# Norwegian Bokmal 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. -# Hanna Rose , 2025. -# Uzair Aftab , 2025. -# Christoffer Tønnessen , 2025. -# cryptocode , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-12 15:50+0000\n" -"Last-Translator: Hanna Rose \n" -"Language-Team: Norwegian Bokmal \n" -"Language: nb\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Gi tilgang til utklippstavlen" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "AvslÃ¥" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Tillat" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "Husk valget for dette delte vinduet?" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "Last inn konfigurasjonen pÃ¥ nytt for Ã¥ vise denne meldingen igjen" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "Avbryt" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Lukk" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "Konfigurasjonsfeil" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"Én eller flere konfigurasjonsfeil ble funnet. Vennligst gjennomgÃ¥ feilene " -"under, og enten last konfigurasjonen din pÃ¥ nytt eller ignorer disse feilene." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Ignorer" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "Last konfigurasjon pÃ¥ nytt" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "âš ï¸ Du kjører et debug-bygg av Ghostty. Debug-bygg har redusert ytelse." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Terminalinspektør" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "Finn…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "Forrige treff" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "Neste treff" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "Ã…, nei." - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "Kan ikke hente en OpenGL-kontekst for rendering." - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"Denne terminalen er i skrivebeskyttet modus. Du kan fortsatt se, markere og " -"bla gjennom innholdet, men ingen inndatahendelser vil bli sendt til den " -"kjørende applikasjonen." - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "Skrivebeskyttet" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Kopier" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "Lim inn" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "Varsle nÃ¥r neste kommandoen fullføres" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "Fjern" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "Nullstill" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "Del vindu" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "Endre tittel…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Del oppover" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "Del nedover" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Del til venstre" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "Del til høyre" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Fane" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Ny fane" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Lukk fane" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Vindu" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Nytt vindu" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Lukk vindu" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "Konfigurasjon" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Ã…pne konfigurasjon" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "Blank verdi gjenoppretter standardtittelen." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "OK" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Del opp vindu" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "Se Ã¥pne faner" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "Hovedmeny" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Kommandopalett" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "Terminalinspektør" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "Om Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "Avslutt" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "Kjør en kommando…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"En applikasjon forsøker Ã¥ skrive til utklippstavlen. Gjeldende " -"utklippstavleinnhold er vist nedenfor." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"En applikasjon forsøker Ã¥ lese fra utklippstavlen. Gjeldende " -"utklippstavleinnhold er vist nedenfor." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Adarsel: Lim inn kan være utrygt" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"Det ser ut som at kommandoer vil bli kjørt hvis du limer inn dette, vurder " -"om du mener det er trygt." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Avslutt Ghostty?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "Lukk fane?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "Lukk vindu?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "Lukk delt vindu?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Alle terminaløkter vil bli avsluttet." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Alle terminaløkter i denne fanen vil bli avsluttet." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Alle terminaløkter i dette vinduet vil bli avsluttet." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "Den kjørende prosessen for denne splitten vil bli avsluttet." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "Kommandoen fullført" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "Kommandoen lyktes" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "Kommandoen mislyktes" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Kommando lyktes" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Kommando mislyktes" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "Endre terminaltittel" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "Konfigurasjonen ble lastet pÃ¥ nytt" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "Kopiert til utklippstavlen" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "Utklippstavle tømt" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Ghostty-utviklere" diff --git a/po/nl.po b/po/nl.po new file mode 100644 index 00000000000..1d3e1014c2c --- /dev/null +++ b/po/nl.po @@ -0,0 +1,356 @@ +# Dutch 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. +# Nico Geesink , 2025. +# Merijntje Tak , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-02-18 20:59+0100\n" +"Last-Translator: Nico Geesink \n" +"Language-Team: Dutch \n" +"Language: nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Open in Ghostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Verleen toegang tot klembord" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Weigeren" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Toestaan" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Onthoud keuze voor deze splitsing" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "Herlaad de configuratie om deze prompt opnieuw weer te geven" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Annuleren" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Afsluiten" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Configuratiefouten" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Er zijn één of meer configuratiefouten gevonden. Bekijk de onderstaande " +"fouten en herlaad je configuratie of negeer deze fouten." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Negeer" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Herlaad configuratie" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ Je draait een debugversie van Ghostty! Prestaties zullen minder zijn dan " +"normaal." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: terminalinspecteur" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Zoeken…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Vorige resultaat" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "Volgende resultaat" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "Oh, nee." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "OpenGL-context voor rendering aanmaken mislukt." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Deze terminal staat in alleen-lezen modus. Je kunt de inhoud nog steeds " +"bekijken en selecteren, maar er wordt geen invoer naar de applicatie " +"verzonden." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Alleen-lezen" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Kopiëren" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Plakken" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "Meld wanneer het volgende commando is afgerond" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "Leegmaken" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Herstellen" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Splitsen" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Wijzig titel…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Splits naar boven" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Splits naar beneden" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Splits naar links" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Splits naar rechts" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Tabblad" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Wijzig tabbladtitel…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Nieuw tabblad" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Sluit tabblad" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Venster" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Nieuw venster" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Sluit venster" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "Configuratie" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Open configuratie" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "Laat leeg om de standaardtitel te herstellen." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Nieuwe splitsing" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Open tabbladen bekijken" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Hoofdmenu" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Opdrachtpalet" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "Terminalinspecteur" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "Over Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Afsluiten" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Voer een commando uit…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Een applicatie probeert de inhoud van het klembord te wijzigen. De huidige " +"inhoud van het klembord wordt hieronder weergegeven." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Een applicatie probeert de inhoud van het klembord te lezen. De huidige " +"inhoud van het klembord wordt hieronder weergegeven." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Waarschuwing: mogelijk onveilige plakactie" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Het plakken van deze tekst in de terminal is mogelijk gevaarlijk, omdat het " +"lijkt op een commando dat uitgevoerd kan worden." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Wil je Ghostty afsluiten?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "Wil je dit tabblad afsluiten?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "Wil je dit venster afsluiten?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "Wil je deze splitsing afsluiten?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Alle terminalsessies zullen worden beëindigd." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Alle terminalsessies binnen dit tabblad zullen worden beëindigd." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Alle terminalsessies binnen dit venster zullen worden beëindigd." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "Alle processen in deze splitsing zullen worden beëindigd." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Commando afgerond" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Commando succesvol afgerond" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Commando onsuccesvol afgerond" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Opdracht geslaagd" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Opdracht mislukt" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Titel van de terminal wijzigen" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Wijzig tabbladtitel" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "De configuratie is herladen" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Gekopieerd naar klembord" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "Klembord geleegd" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Ghostty-ontwikkelaars" diff --git a/po/nl_NL.UTF-8.po b/po/nl_NL.UTF-8.po deleted file mode 100644 index 9caee41a4f8..00000000000 --- a/po/nl_NL.UTF-8.po +++ /dev/null @@ -1,356 +0,0 @@ -# Dutch 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. -# Nico Geesink , 2025. -# Merijntje Tak , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-09 20:39+0100\n" -"Last-Translator: Nico Geesink \n" -"Language-Team: Dutch \n" -"Language: nl\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Verleen toegang tot klembord" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "Weigeren" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Toestaan" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "Onthoud keuze voor deze splitsing" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "Herlaad de configuratie om deze prompt opnieuw weer te geven" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "Annuleren" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Afsluiten" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "Configuratiefouten" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"Er zijn één of meer configuratiefouten gevonden. Bekijk de onderstaande " -"fouten en herlaad je configuratie of negeer deze fouten." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Negeer" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "Herlaad configuratie" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"âš ï¸ Je draait een debugversie van Ghostty! Prestaties zullen minder zijn dan " -"normaal." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: terminalinspecteur" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "Zoeken…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "Vorige resultaat" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "Volgende resultaat" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "Oh, nee." - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "OpenGL-context voor rendering aanmaken mislukt." - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"Deze terminal staat in alleen-lezen modus. Je kunt de inhoud nog steeds " -"bekijken en selecteren, maar er wordt geen invoer naar de applicatie " -"verzonden." - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "Alleen-lezen" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Kopiëren" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "Plakken" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "Meld wanneer het volgende commando is afgerond" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "Leegmaken" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "Herstellen" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "Splitsen" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "Wijzig titel…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Splits naar boven" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "Splits naar beneden" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Splits naar links" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "Splits naar rechts" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Tabblad" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Nieuw tabblad" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Sluit tabblad" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Venster" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Nieuw venster" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Sluit venster" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "Configuratie" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Open configuratie" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "Laat leeg om de standaardtitel te herstellen." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "OK" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Nieuwe splitsing" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "Open tabbladen bekijken" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "Hoofdmenu" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Opdrachtpalet" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "Terminalinspecteur" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "Over Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "Afsluiten" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "Voer een commando uit…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Een applicatie probeert de inhoud van het klembord te wijzigen. De huidige " -"inhoud van het klembord wordt hieronder weergegeven." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Een applicatie probeert de inhoud van het klembord te lezen. De huidige " -"inhoud van het klembord wordt hieronder weergegeven." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Waarschuwing: mogelijk onveilige plakactie" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"Het plakken van deze tekst in de terminal is mogelijk gevaarlijk, omdat het " -"lijkt op een commando dat uitgevoerd kan worden." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Wil je Ghostty afsluiten?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "Wil je dit tabblad afsluiten?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "Wil je dit venster afsluiten?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "Wil je deze splitsing afsluiten?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Alle terminalsessies zullen worden beëindigd." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Alle terminalsessies binnen dit tabblad zullen worden beëindigd." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Alle terminalsessies binnen dit venster zullen worden beëindigd." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "Alle processen in deze splitsing zullen worden beëindigd." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "Commando afgerond" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "Commando succesvol afgerond" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "Commando onsuccesvol afgerond" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Opdracht geslaagd" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Opdracht mislukt" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "Titel van de terminal wijzigen" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "De configuratie is herladen" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "Gekopieerd naar klembord" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "Klembord geleegd" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Ghostty-ontwikkelaars" diff --git a/po/pl.po b/po/pl.po new file mode 100644 index 00000000000..483cb96627c --- /dev/null +++ b/po/pl.po @@ -0,0 +1,356 @@ +# Polish translations for com.mitchellh.ghostty package +# Polskie tÅ‚umaczenia dla pakietu com.mitchellh.ghostty. +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Bartosz Sokorski , 2025. +# trag1c , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-02-11 14:12+0100\n" +"Last-Translator: trag1c \n" +"Language-Team: Polish \n" +"Language: pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2);\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Otwórz w Ghostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Udziel dostÄ™pu do schowka" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Odmów" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Zezwól" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "ZapamiÄ™taj wybór dla tego podziaÅ‚u" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "PrzeÅ‚aduj konfiguracjÄ™, by ponownie wyÅ›wietlić ten komunikat" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Anuluj" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Zamknij" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Błędy konfiguracji" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Znaleziono jeden lub wiÄ™cej błędów konfiguracji. Sprawdź błędy wylistowane " +"poniżej i przeÅ‚aduj konfiguracjÄ™ lub zignoruj je." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Zignoruj" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "PrzeÅ‚aduj konfiguracjÄ™" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "âš ï¸ Używasz wersji Ghostty do debugowania! Wydajność bÄ™dzie obniżona." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Inspektor terminala Ghostty" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Znajdź…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Poprzednie dopasowanie" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "NastÄ™pne dopasowanie" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "O nie!" + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "Nie można uzyskać kontekstu OpenGL do renderowania." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Ten terminal znajduje siÄ™ w trybie tylko do odczytu. Wciąż możesz " +"przeglÄ…dać, zaznaczać i przewijać zawartość, ale wprowadzane dane nie bÄ™dÄ… " +"przesyÅ‚ane do wykonywanej aplikacji." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Tylko do odczytu" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Kopiuj" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Wklej" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "Powiadom o ukoÅ„czeniu nastÄ™pnej komendy" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "Wyczyść" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Zresetuj" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "PodziaÅ‚" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "ZmieÅ„ tytuł…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Podziel w górÄ™" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Podziel w dół" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Podziel w lewo" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Podziel w prawo" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Karta" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "ZmieÅ„ tytuÅ‚ karty…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Nowa karta" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Zamknij kartÄ™" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Okno" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Nowe okno" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Zamknij okno" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "Konfiguracja" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Otwórz konfiguracjÄ™" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "Pozostaw puste by przywrócić domyÅ›lny tytuÅ‚." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Nowy podziaÅ‚" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Zobacz otwarte karty" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Menu główne" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Paleta komend" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "Inspektor terminala" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "O Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Zamknij" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Wykonaj komendę…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Aplikacja próbuje zapisać do schowka. Obecna zawartość schowka pokazana " +"poniżej." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Aplikacja próbuje odczytać zawartość schowka. Zawartość schowka pokazana " +"poniżej." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Uwaga: potencjalnie niebezpieczne wklejenie ze schowka" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Wklejenie tego tekstu do terminala może być niebezpieczne, ponieważ może " +"spowodować wykonanie komend." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Zamknąć Ghostty?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "Zamknąć kartÄ™?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "Zamknąć okno?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "Zamknąć podziaÅ‚?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Wszystkie sesje terminala zostanÄ… zakoÅ„czone." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Wszystkie sesje terminala w obecnej karcie zostanÄ… zakoÅ„czone." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Wszystkie sesje terminala w obecnym oknie zostanÄ… zakoÅ„czone." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "Wszyskie trwajÄ…ce procesy w obecnym podziale zostanÄ… zakoÅ„czone." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Komenda zakoÅ„czona" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Komenda wykonana pomyÅ›lnie" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Komenda nie powiodÅ‚a siÄ™" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Komenda wykonana pomyÅ›lnie" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Komenda nie powiodÅ‚a siÄ™" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "ZmieÅ„ tytuÅ‚ terminala" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "ZmieÅ„ tytuÅ‚ karty" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "PrzeÅ‚adowano konfiguracjÄ™" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Skopiowano do schowka" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "Wyczyszczono schowek" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Twórcy Ghostty" diff --git a/po/pl_PL.UTF-8.po b/po/pl_PL.UTF-8.po deleted file mode 100644 index 974fbb25032..00000000000 --- a/po/pl_PL.UTF-8.po +++ /dev/null @@ -1,356 +0,0 @@ -# Polish translations for com.mitchellh.ghostty package -# Polskie tÅ‚umaczenia dla pakietu com.mitchellh.ghostty. -# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors -# This file is distributed under the same license as the com.mitchellh.ghostty package. -# Bartosz Sokorski , 2025. -# trag1c , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-11 14:12+0100\n" -"Last-Translator: trag1c \n" -"Language-Team: Polish \n" -"Language: pl\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " -"|| n%100>=20) ? 1 : 2);\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Udziel dostÄ™pu do schowka" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "Odmów" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Zezwól" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "ZapamiÄ™taj wybór dla tego podziaÅ‚u" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "PrzeÅ‚aduj konfiguracjÄ™, by ponownie wyÅ›wietlić ten komunikat" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "Anuluj" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Zamknij" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "Błędy konfiguracji" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"Znaleziono jeden lub wiÄ™cej błędów konfiguracji. Sprawdź błędy wylistowane " -"poniżej i przeÅ‚aduj konfiguracjÄ™ lub zignoruj je." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Zignoruj" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "PrzeÅ‚aduj konfiguracjÄ™" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "âš ï¸ Używasz wersji Ghostty do debugowania! Wydajność bÄ™dzie obniżona." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Inspektor terminala Ghostty" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "Znajdź…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "Poprzednie dopasowanie" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "NastÄ™pne dopasowanie" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "O nie!" - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "Nie można uzyskać kontekstu OpenGL do renderowania." - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"Ten terminal znajduje siÄ™ w trybie tylko do odczytu. Wciąż możesz " -"przeglÄ…dać, zaznaczać i przewijać zawartość, ale wprowadzane dane nie bÄ™dÄ… " -"przesyÅ‚ane do wykonywanej aplikacji." - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "Tylko do odczytu" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Kopiuj" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "Wklej" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "Powiadom o ukoÅ„czeniu nastÄ™pnej komendy" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "Wyczyść" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "Zresetuj" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "PodziaÅ‚" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "ZmieÅ„ tytuł…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Podziel w górÄ™" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "Podziel w dół" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Podziel w lewo" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "Podziel w prawo" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Karta" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Nowa karta" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Zamknij kartÄ™" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Okno" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Nowe okno" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Zamknij okno" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "Konfiguracja" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Otwórz konfiguracjÄ™" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "Pozostaw puste by przywrócić domyÅ›lny tytuÅ‚." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "OK" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Nowy podziaÅ‚" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "Zobacz otwarte karty" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "Menu główne" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Paleta komend" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "Inspektor terminala" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "O Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "Zamknij" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "Wykonaj komendę…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Aplikacja próbuje zapisać do schowka. Obecna zawartość schowka pokazana " -"poniżej." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Aplikacja próbuje odczytać zawartość schowka. Zawartość schowka pokazana " -"poniżej." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Uwaga: potencjalnie niebezpieczne wklejenie ze schowka" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"Wklejenie tego tekstu do terminala może być niebezpieczne, ponieważ może " -"spowodować wykonanie komend." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Zamknąć Ghostty?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "Zamknąć kartÄ™?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "Zamknąć okno?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "Zamknąć podziaÅ‚?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Wszystkie sesje terminala zostanÄ… zakoÅ„czone." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Wszystkie sesje terminala w obecnej karcie zostanÄ… zakoÅ„czone." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Wszystkie sesje terminala w obecnym oknie zostanÄ… zakoÅ„czone." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "Wszyskie trwajÄ…ce procesy w obecnym podziale zostanÄ… zakoÅ„czone." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "Komenda zakoÅ„czona" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "Komenda wykonana pomyÅ›lnie" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "Komenda nie powiodÅ‚a siÄ™" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Komenda wykonana pomyÅ›lnie" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Komenda nie powiodÅ‚a siÄ™" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "ZmieÅ„ tytuÅ‚ terminala" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "PrzeÅ‚adowano konfiguracjÄ™" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "Skopiowano do schowka" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "Wyczyszczono schowek" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Twórcy Ghostty" diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po deleted file mode 100644 index 78306434368..00000000000 --- a/po/pt_BR.UTF-8.po +++ /dev/null @@ -1,355 +0,0 @@ -# Portuguese translations for com.mitchellh.ghostty package -# Traduções em português brasileiro para o pacote com.mitchellh.ghostty. -# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors -# This file is distributed under the same license as the com.mitchellh.ghostty package. -# Gustavo Peres , 2025. -# Guilherme Tiscoski , 2025. -# Nilton Perim Neto , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2025-09-15 13:57-0300\n" -"Last-Translator: Nilton Perim Neto \n" -"Language-Team: Brazilian Portuguese \n" -"Language: pt_BR\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Autorizar acesso à área de transferência" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "Negar" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Permitir" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "Lembrar escolha para esta divisão" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "Recarregue a configuração para mostrar este aviso novamente" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "Cancelar" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Fechar" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "Erros de configuração" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"Um ou mais erros de configuração encontrados. Por favor revise os erros " -"abaixo, e ou recarregue sua configuração, ou ignore esses erros." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Ignorar" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "Recarregar configuração" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"âš ï¸ Você está rodando uma build de debug do Ghostty! O desempenho será afetado." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Inspetor do terminal" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Copiar" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "Colar" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "Limpar" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "Reiniciar" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "Dividir" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "Mudar título…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Dividir para cima" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "Dividir para baixo" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Dividir à esquerda" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "Dividir à direita" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Aba" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Nova aba" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Fechar aba" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Janela" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Nova janela" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Fechar janela" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "Configurar" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Abrir configuração" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "Deixe em branco para restaurar o título padrão." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "OK" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Nova divisão" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "Visualizar abas abertas" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "Menu Principal" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Paleta de comandos" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "Inspetor de terminal" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "Sobre o Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "Sair" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "Executar um comando…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Uma aplicação está tentando escrever na área de transferência. O conteúdo " -"atual da área de transferência está aparecendo abaixo." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Uma aplicação está tentando ler da área de transferência. O conteúdo atual " -"da área de transferência está sendo exibido abaixo." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Aviso: Conteúdo potencialmente inseguro" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"Colar esse texto em um terminal pode ser perigoso, pois parece que alguns " -"comandos podem ser executados." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Fechar Ghostty?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "Fechar aba?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "Fechar janela?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "Fechar divisão?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Todas as sessões de terminal serão finalizadas." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Todas as sessões de terminal nessa aba serão finalizadas." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Todas as sessões de terminal nessa janela serão finalizadas." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "O processo atual rodando nessa divisão será finalizado." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Comando executado com sucesso" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Comando falhou" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "Mudar título do Terminal" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "Configuração recarregada" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "Copiado para a área de transferência" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "Ãrea de transferência limpa" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Desenvolvedores do Ghostty" diff --git a/po/pt_BR.po b/po/pt_BR.po new file mode 100644 index 00000000000..024da72af68 --- /dev/null +++ b/po/pt_BR.po @@ -0,0 +1,358 @@ +# Portuguese translations for com.mitchellh.ghostty package +# Traduções em português brasileiro para o pacote com.mitchellh.ghostty. +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Gustavo Peres , 2025. +# Guilherme Tiscoski , 2025. +# Nilton Perim Neto , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-05 10:23+0800\n" +"PO-Revision-Date: 2026-02-18 10:50-0300\n" +"Last-Translator: Guilherme Tiscoski \n" +"Language-Team: Brazilian Portuguese \n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Abrir no Ghostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Autorizar acesso à área de transferência" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Negar" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Permitir" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Lembrar escolha para esta divisão" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "Recarregue a configuração para mostrar este aviso novamente" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Cancelar" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Fechar" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Erros de configuração" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Um ou mais erros de configuração encontrados. Por favor revise os erros " +"abaixo, e ou recarregue sua configuração, ou ignore esses erros." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Ignorar" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Recarregar configuração" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ Você está rodando uma build de debug do Ghostty! O desempenho será afetado." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspetor do terminal" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Buscar…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Resultado anterior" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "Próximo resultado" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "Ah, não." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "Não foi possível obter um contexto OpenGL para renderização." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Este terminal está em modo somente leitura. Você ainda pode visualizar, " +"selecionar e rolar o conteúdo, mas nenhum evento de entrada será enviado " +"para a aplicação em execução." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Somente leitura" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Copiar" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Colar" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "Notificar ao finalizar o próximo comando" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "Limpar" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Reiniciar" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Dividir" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Mudar título…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Dividir para cima" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Dividir para baixo" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Dividir à esquerda" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Dividir à direita" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Aba" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Mudar título da aba…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Nova aba" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Fechar aba" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Janela" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Nova janela" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Fechar janela" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "Configurar" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Abrir configuração" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "Deixe em branco para restaurar o título padrão." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Nova divisão" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Visualizar abas abertas" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Menu Principal" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Paleta de comandos" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "Inspetor de terminal" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "Sobre o Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Sair" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Executar um comando…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Uma aplicação está tentando escrever na área de transferência. O conteúdo " +"atual da área de transferência está aparecendo abaixo." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Uma aplicação está tentando ler da área de transferência. O conteúdo atual " +"da área de transferência está sendo exibido abaixo." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Aviso: Conteúdo potencialmente inseguro" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Colar esse texto em um terminal pode ser perigoso, pois parece que alguns " +"comandos podem ser executados." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Fechar Ghostty?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "Fechar aba?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "Fechar janela?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "Fechar divisão?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Todas as sessões de terminal serão finalizadas." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Todas as sessões de terminal nessa aba serão finalizadas." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Todas as sessões de terminal nessa janela serão finalizadas." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "O processo atual rodando nessa divisão será finalizado." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Comando finalizado" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Comando bem-sucedido" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Comando falhou" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Comando executado com sucesso" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Comando falhou" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Mudar título do Terminal" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Mudar título da aba" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "Configuração recarregada" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Copiado para a área de transferência" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "Ãrea de transferência limpa" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Desenvolvedores do Ghostty" diff --git a/po/ru.po b/po/ru.po new file mode 100644 index 00000000000..d64229eedb6 --- /dev/null +++ b/po/ru.po @@ -0,0 +1,357 @@ +# Russian translations for com.mitchellh.ghostty package +# РуÑÑкие переводы Ð´Ð»Ñ Ð¿Ð°ÐºÐµÑ‚Ð° com.mitchellh.ghostty. +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# blackzeshi , 2025. +# Ivan Bastrakov , 2025. +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2025-02-18 10:20+0100\n" +"Last-Translator: Ivan Bastrakov \n" +"Language-Team: Russian \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Открыть в Ghostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Разрешить доÑтуп к буферу обмена" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Отклонить" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Разрешить" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Запомнить выбор Ð´Ð»Ñ Ñтого Ñплита" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "Перезагрузите конфигурацию, чтобы Ñнова увидеть Ñто Ñообщение" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Отмена" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Закрыть" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Ошибки конфигурации" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Ð’ конфигурации обнаружены перечиÑленные ниже ошибки. При необходимоÑти " +"иÑправьте их, а затем перезагрузите конфигурацию." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Игнорировать" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Перезагрузить конфигурацию" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ Ð’Ñ‹ запуÑтили отладочную Ñборку Ghostty! Это может влиÑть на " +"производительноÑть." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: инÑпектор терминала" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Ðайти…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Предыдущий результат" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "Следующий результат" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "Ой!" + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "Ðе удалоÑÑŒ получить доÑтуп к контекÑту OpenGL Ð´Ð»Ñ Ð¾Ñ‚Ñ€Ð¸Ñовки." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Терминал работает в режиме только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ: его Ñодержимое можно " +"прокручивать и выделÑть, но запущенное приложение не будет получать ÑÐ¾Ð±Ñ‹Ñ‚Ð¸Ñ " +"ввода." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Режим только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Копировать" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Ð’Ñтавить" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "Сообщить о завершении Ñледующей команды" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "ОчиÑтить" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "СброÑ" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Сплит" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Переименовать…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Сплит вверх" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Сплит вниз" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Сплит влево" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Сплит вправо" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Вкладка" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Переименовать вкладку…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "ÐÐ¾Ð²Ð°Ñ Ð²ÐºÐ»Ð°Ð´ÐºÐ°" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Закрыть вкладку" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Окно" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Ðовое окно" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Закрыть окно" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "КонфигурациÑ" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Открыть конфигурационный файл" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "ОÑтавьте поле пуÑтым, чтобы вернуть название по умолчанию." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "ОК" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Ðовый Ñплит" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "ПроÑмотреть открытые вкладки" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Главное меню" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Палитра команд" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "ИнÑпектор терминала" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "О Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Выход" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Выполнить команду…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Приложение пытаетÑÑ Ð·Ð°Ð¿Ð¸Ñать данные в буфер обмена. Текущее Ñодержимое " +"буфера обмена показано ниже." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Приложение пытаетÑÑ Ð¿Ñ€Ð¾Ñ‡Ð¸Ñ‚Ð°Ñ‚ÑŒ данные из буфера обмена. Его Ñодержимое " +"показано ниже." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Внимание! Ð’ÑтавлÑемые данные могут нанеÑти вред вашей ÑиÑтеме" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Этот текÑÑ‚ может быть опаÑен: его вÑтавка в терминал приведёт к выполнению " +"команд." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Выйти из Ghostty?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "Закрыть вкладку?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "Закрыть окно?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "Закрыть Ñплит-режим?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Ð’Ñе ÑеÑÑии терминала будут оÑтановлены." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Ð’Ñе ÑеÑÑии терминала в Ñтой вкладке будут оÑтановлены." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Ð’Ñе ÑеÑÑии терминала в Ñтом окне будут оÑтановлены." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "ПроцеÑÑ, работающий в Ñтой Ñплит-облаÑти, будет оÑтановлен." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Команда завершилаÑÑŒ" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Команда выполнена уÑпешно" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Команда завершилаÑÑŒ Ñ Ð¾ÑˆÐ¸Ð±ÐºÐ¾Ð¹" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Команда выполнена уÑпешно" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Команда завершилаÑÑŒ Ñ Ð¾ÑˆÐ¸Ð±ÐºÐ¾Ð¹" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Переименовать терминал" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Переименовать вкладку" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "ÐšÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ð¿ÐµÑ€ÐµÐ·Ð°Ð³Ñ€ÑƒÐ¶ÐµÐ½Ð°" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Скопировано в буфер обмена" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "Буфер обмена очищен" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Разработчики Ghostty" diff --git a/po/ru_RU.UTF-8.po b/po/ru_RU.UTF-8.po deleted file mode 100644 index e4e314141ce..00000000000 --- a/po/ru_RU.UTF-8.po +++ /dev/null @@ -1,354 +0,0 @@ -# Russian translations for com.mitchellh.ghostty package -# РуÑÑкие переводы Ð´Ð»Ñ Ð¿Ð°ÐºÐµÑ‚Ð° com.mitchellh.ghostty. -# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors -# This file is distributed under the same license as the com.mitchellh.ghostty package. -# blackzeshi , 2025. -# Ivan Bastrakov , 2025. -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2025-09-03 01:50+0300\n" -"Last-Translator: Ivan Bastrakov \n" -"Language-Team: Russian \n" -"Language: ru\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " -"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Разрешить доÑтуп к буферу обмена" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "Отклонить" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Разрешить" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "Запомнить выбор Ð´Ð»Ñ Ñтого Ñплита" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "Перезагрузите конфигурацию, чтобы Ñнова увидеть Ñто Ñообщение" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "Отмена" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Закрыть" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "Ошибки конфигурации" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"ÐšÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ñодержит ошибки. Проверьте их ниже, а затем либо перезагрузите " -"конфигурацию, либо проигнорируйте ошибки." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Игнорировать" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "Обновить конфигурацию" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"âš ï¸ Ð’Ñ‹ запуÑтили отладочную Ñборку Ghostty! Это может влиÑть на " -"производительноÑть." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: инÑпектор терминала" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Копировать" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "Ð’Ñтавить" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "ОчиÑтить" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "СброÑ" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "Сплит" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "Изменить заголовок…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Сплит вверх" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "Сплит вниз" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Сплит влево" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "Сплит вправо" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Вкладка" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "ÐÐ¾Ð²Ð°Ñ Ð²ÐºÐ»Ð°Ð´ÐºÐ°" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Закрыть вкладку" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Окно" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Ðовое окно" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Закрыть окно" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "КонфигурациÑ" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Открыть конфигурационный файл" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "ОÑтавьте пуÑтым, чтобы воÑÑтановить иÑходный заголовок." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "ОК" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Ðовый Ñплит" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "ПроÑмотреть открытые вкладки" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "Главное меню" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Палитра команд" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "ИнÑпектор терминала" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "О Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "Выход" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "Выполнить команду…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Приложение пытаетÑÑ Ð·Ð°Ð¿Ð¸Ñать данные в буфер обмена. Текущее Ñодержимое " -"буфера обмена показано ниже." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Приложение пытаетÑÑ Ð¿Ñ€Ð¾Ñ‡Ð¸Ñ‚Ð°Ñ‚ÑŒ данные из буфера обмена. Эти данные отображены " -"ниже." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Внимание! Ð’ÑтавлÑемые данные могут нанеÑти вред вашей ÑиÑтеме" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"Ð’Ñтавка Ñтого текÑта в терминал может быть опаÑной. Это выглÑдит как " -"команды, которые могут быть иÑполнены." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Закрыть Ghostty?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "Закрыть вкладку?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "Закрыть окно?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "Закрыть Ñплит-режим?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Ð’Ñе ÑеÑÑии терминала будут оÑтановлены." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Ð’Ñе ÑеÑÑии терминала в Ñтой вкладке будут оÑтановлены." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Ð’Ñе ÑеÑÑии терминала в Ñтом окне будут оÑтановлены." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "ПроцеÑÑ, работающий в Ñтой Ñплит-облаÑти, будет оÑтановлен." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Команда выполнена уÑпешно" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Команда завершилаÑÑŒ Ñ Ð¾ÑˆÐ¸Ð±ÐºÐ¾Ð¹" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "Изменить заголовок терминала" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "ÐšÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ð±Ñ‹Ð»Ð° обновлена" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "Скопировано в буфер обмена" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "Буфер обмена очищен" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Разработчики Ghostty" diff --git a/po/tr.po b/po/tr.po new file mode 100644 index 00000000000..37af61b7d34 --- /dev/null +++ b/po/tr.po @@ -0,0 +1,356 @@ +# Turkish 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. +# Emir SARI , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-02-09 22:18+0300\n" +"Last-Translator: Emir SARI \n" +"Language-Team: Turkish\n" +"Language: tr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Ghostty’de Aç" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Pano EriÅŸimine İzin Ver" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Reddet" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "İzin Ver" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Bu bölme için tercihi anımsa" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "Bu istemi tekrar göstermek için yapılandırmayı yeniden yükle" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "İptal" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Kapat" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Yapılandırma Hataları" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Bir veya daha fazla yapılandırma hatası bulundu. Lütfen aÅŸağıdaki hataları " +"gözden geçirin ve ardından ya yapılandırmanızı yeniden yükleyin ya da bu " +"hataları yok sayın." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Yok Say" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Yapılandırmayı Yeniden Yükle" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ Ghostty’nin hata ayıklama amaçlı yapılmış bir sürümünü kullanıyorsunuz! " +"BaÅŸarım normale göre daha düşük olacaktır." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Uçbirim Denetçisi" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Bul…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Önceki EÅŸleÅŸme" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "Sonraki EÅŸleÅŸme" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "Hayır, olamaz." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "Görüntü oluÅŸturma iÅŸlemi için OpenGL baÄŸlamı elde edilemiyor." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Bu uçbirim salt okunur kipte. İçeriÄŸi görüntüleyebilir, seçebilir ve " +"kaydırabilirsiniz; ancak çalışan uygulamaya hiçbir giriÅŸ olayı " +"gönderilmeyecektir." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Salt Okunur" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Kopyala" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Yapıştır" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "Sonraki Komut BittiÄŸinde Bildir" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "Temizle" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Sıfırla" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Böl" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "BaÅŸlığı DeÄŸiÅŸtir…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Yukarı DoÄŸru Böl" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "AÅŸağı DoÄŸru Böl" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Sola DoÄŸru Böl" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "SaÄŸa DoÄŸru Böl" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Sekme" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Sekme BaÅŸlığını DeÄŸiÅŸtir…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Yeni Sekme" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Sekmeyi Kapat" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Pencere" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Yeni Pencere" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Pencereyi Kapat" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "Yapılandırma" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Yapılandırmayı Aç" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "Öntanımlı baÅŸlığı geri yüklemek için boÅŸ bırakın." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "Tamam" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Yeni Bölme" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Açık Sekmeleri Görüntüle" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Ana Menü" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Komut Paleti" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "Uçbirim Denetçisi" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "Ghostty Hakkında" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Çık" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Bir komut çalıştır…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Bir uygulama panoya yazmaya çalışıyor. Geçerli pano içeriÄŸi aÅŸağıda " +"gösterilmektedir." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Bir uygulama panodan okumaya çalışıyor. Geçerli pano içeriÄŸi aÅŸağıda " +"gösterilmektedir." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Uyarı: Tehlikeli Olabilecek Yapıştırma" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Bu metni uçbirime yapıştırmak tehlikeli olabilir; çünkü bir komut " +"yürütülebilecekmiÅŸ gibi duruyor." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Ghostty’den Çık?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "Sekmeyi Kapat?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "Pencereyi Kapat?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "Bölmeyi Kapat?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Tüm uçbirim oturumları sonlandırılacaktır." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Bu sekmedeki tüm uçbirim oturumları sonlandırılacaktır." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Bu penceredeki tüm uçbirim oturumları sonlandırılacaktır." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "Bu bölmedeki ÅŸu anda çalışan süreç sonlandırılacaktır." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Komut Bitti" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Komut BaÅŸarılı" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Komut BaÅŸarısız" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Komut baÅŸarılı oldu" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Komut baÅŸarısız oldu" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Uçbirim BaÅŸlığını DeÄŸiÅŸtir" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Sekme BaÅŸlığını DeÄŸiÅŸtir" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "Yapılandırma yeniden yüklendi" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Panoya kopyalandı" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "Pano temizlendi" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Ghostty GeliÅŸtiricileri" diff --git a/po/tr_TR.UTF-8.po b/po/tr_TR.UTF-8.po deleted file mode 100644 index 322d15d5aa3..00000000000 --- a/po/tr_TR.UTF-8.po +++ /dev/null @@ -1,356 +0,0 @@ -# Turkish 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. -# Emir SARI , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-09 22:18+0300\n" -"Last-Translator: Emir SARI \n" -"Language-Team: Turkish\n" -"Language: tr\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Pano EriÅŸimine İzin Ver" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "Reddet" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "İzin Ver" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "Bu bölme için tercihi anımsa" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "Bu istemi tekrar göstermek için yapılandırmayı yeniden yükle" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "İptal" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Kapat" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "Yapılandırma Hataları" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"Bir veya daha fazla yapılandırma hatası bulundu. Lütfen aÅŸağıdaki hataları " -"gözden geçirin ve ardından ya yapılandırmanızı yeniden yükleyin ya da bu " -"hataları yok sayın." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Yok Say" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "Yapılandırmayı Yeniden Yükle" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"âš ï¸ Ghostty’nin hata ayıklama amaçlı yapılmış bir sürümünü kullanıyorsunuz! " -"BaÅŸarım normale göre daha düşük olacaktır." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Uçbirim Denetçisi" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "Bul…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "Önceki EÅŸleÅŸme" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "Sonraki EÅŸleÅŸme" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "Hayır, olamaz." - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "Görüntü oluÅŸturma iÅŸlemi için OpenGL baÄŸlamı elde edilemiyor." - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"Bu uçbirim salt okunur kipte. İçeriÄŸi görüntüleyebilir, seçebilir ve " -"kaydırabilirsiniz; ancak çalışan uygulamaya hiçbir giriÅŸ olayı " -"gönderilmeyecektir." - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "Salt Okunur" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Kopyala" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "Yapıştır" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "Sonraki Komut BittiÄŸinde Bildir" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "Temizle" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "Sıfırla" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "Böl" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "BaÅŸlığı DeÄŸiÅŸtir…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Yukarı DoÄŸru Böl" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "AÅŸağı DoÄŸru Böl" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Sola DoÄŸru Böl" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "SaÄŸa DoÄŸru Böl" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Sekme" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Yeni Sekme" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Sekmeyi Kapat" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Pencere" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Yeni Pencere" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Pencereyi Kapat" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "Yapılandırma" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Yapılandırmayı Aç" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "Öntanımlı baÅŸlığı geri yüklemek için boÅŸ bırakın." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "Tamam" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Yeni Bölme" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "Açık Sekmeleri Görüntüle" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "Ana Menü" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Komut Paleti" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "Uçbirim Denetçisi" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "Ghostty Hakkında" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "Çık" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "Bir komut çalıştır…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Bir uygulama panoya yazmaya çalışıyor. Geçerli pano içeriÄŸi aÅŸağıda " -"gösterilmektedir." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Bir uygulama panodan okumaya çalışıyor. Geçerli pano içeriÄŸi aÅŸağıda " -"gösterilmektedir." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Uyarı: Tehlikeli Olabilecek Yapıştırma" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"Bu metni uçbirime yapıştırmak tehlikeli olabilir; çünkü bir komut " -"yürütülebilecekmiÅŸ gibi duruyor." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Ghostty’den Çık?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "Sekmeyi Kapat?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "Pencereyi Kapat?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "Bölmeyi Kapat?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Tüm uçbirim oturumları sonlandırılacaktır." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Bu sekmedeki tüm uçbirim oturumları sonlandırılacaktır." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Bu penceredeki tüm uçbirim oturumları sonlandırılacaktır." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "Bu bölmedeki ÅŸu anda çalışan süreç sonlandırılacaktır." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "Komut Bitti" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "Komut BaÅŸarılı" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "Komut BaÅŸarısız" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Komut baÅŸarılı oldu" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Komut baÅŸarısız oldu" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "Uçbirim BaÅŸlığını DeÄŸiÅŸtir" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "Yapılandırma yeniden yüklendi" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "Panoya kopyalandı" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "Pano temizlendi" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Ghostty GeliÅŸtiricileri" diff --git a/po/uk.po b/po/uk.po new file mode 100644 index 00000000000..e2766b0ab6d --- /dev/null +++ b/po/uk.po @@ -0,0 +1,355 @@ +# Ukrainian 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. +# Danylo Zalizchuk , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-02-18 13:14+0100\n" +"Last-Translator: Volodymyr Chernetskyi " +"<19735328+chernetskyi@users.noreply.github.com>\n" +"Language-Team: Ukrainian \n" +"Language: uk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Відкрити в Ghostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Ðадати доÑтуп до буфера обміну" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Заборонити" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Дозволити" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "ЗапамʼÑтати Ð´Ð»Ñ Ñ†Ñ–Ñ”Ñ— панелі" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "Перезавантажте налаштуваннÑ, щоб показати це Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð·Ð½Ð¾Ð²Ñƒ" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "СкаÑувати" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Закрити" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Помилки налаштуваннÑ" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"ВиÑвлено одну або декілька помилок налаштуваннÑ. Будь лаÑка, переглÑньте " +"помилки нижче Ñ– або перезавантажте налаштуваннÑ, або проігноруйте ці помилки." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Ігнорувати" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Перезавантажити налаштуваннÑ" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ Ð’Ð¸ викориÑтовуєте відладочну збірку Ghostty! ПродуктивніÑть буде погіршено." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: ІнÑпектор терміналу" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Знайти…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Попередній збіг" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "ÐаÑтупний збіг" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "Халепа." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "Ðе вдалоÑÑ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ñ‚Ð¸ контекÑÑ‚ OpenGL Ð´Ð»Ñ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Цей термінал працює в режимі читаннÑ. Можна переглÑдати, виділÑти Ñ– гортати " +"вміÑÑ‚, але ввід не буде передано до запущеної програми." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Тільки Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Скопіювати" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Ð’Ñтавити" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "СповіÑтити про Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð½Ð°Ñтупної команди" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "ОчиÑтити" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Скинути" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Панель" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Змінити заголовок…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Ðова панель зверху" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Ðова панель знизу" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Ðова панель ліворуч" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Ðова панель праворуч" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Вкладка" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Змінити заголовок вкладки…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Ðова вкладка" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Закрити вкладку" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Вікно" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Ðове вікно" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Закрити вікно" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "ÐалаштуваннÑ" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Відкрити налаштуваннÑ" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "Залиште порожнім, щоб відновити заголовок за замовчуваннÑм." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "ОК" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Ðова панель" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "ПереглÑнути відкриті вкладки" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Головне меню" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Палітра команд" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "ІнÑпектор терміналу" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "Про Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Завершити" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Виконати команду…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Програма намагаєтьÑÑ Ð·Ð°Ð¿Ð¸Ñати дані до буфера обміну. Ðижче наведено вміÑÑ‚ " +"буфера обміну." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Програма намагаєтьÑÑ Ð¿Ñ€Ð¾Ñ‡Ð¸Ñ‚Ð°Ñ‚Ð¸ дані з буфера обміну. Ðижче наведено вміÑÑ‚ " +"буфера обміну." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Увага: потенційно небезпечна вÑтавка" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Ð’Ñтавка цього текÑту в термінал може бути небезпечною, бо Ñхоже, що деÑкі " +"команди можуть бути виконані." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Завершити Ghostty?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "Закрити вкладку?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "Закрити вікно?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "Закрити панель?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Ð’ÑÑ– ÑеÑÑ–Ñ— терміналу будуть завершені." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Ð’ÑÑ– ÑеÑÑ–Ñ— терміналу в цій вкладці будуть завершені." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Ð’ÑÑ– ÑеÑÑ–Ñ— терміналу в цьому вікні будуть завершені." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "ПроцеÑ, що виконуєтьÑÑ Ð² цій панелі, буде завершено." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Команда завершилаÑÑŒ" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Команда завершилаÑÑŒ уÑпішно" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Команда завершилаÑÑŒ з помилкою" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Команда завершилаÑÑŒ уÑпішно" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Команда завершилаÑÑŒ з помилкою" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Змінити заголовок терміналу" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Змінити заголовок вкладки" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿ÐµÑ€ÐµÐ·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð¾" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Скопійовано до буферa обміну" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "Буфер обміну очищено" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Розробники Ghostty" diff --git a/po/uk_UA.UTF-8.po b/po/uk_UA.UTF-8.po deleted file mode 100644 index 5ff919f8f22..00000000000 --- a/po/uk_UA.UTF-8.po +++ /dev/null @@ -1,355 +0,0 @@ -# Ukrainian 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. -# Danylo Zalizchuk , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-09 21:03+0100\n" -"Last-Translator: Volodymyr Chernetskyi " -"<19735328+chernetskyi@users.noreply.github.com>\n" -"Language-Team: Ukrainian \n" -"Language: uk\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " -"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "Ðадати доÑтуп до буфера обміну" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "Заборонити" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "Дозволити" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "ЗапамʼÑтати Ð´Ð»Ñ Ñ†Ñ–Ñ”Ñ— панелі" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "Перезавантажте налаштуваннÑ, щоб показати це Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð·Ð½Ð¾Ð²Ñƒ" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "СкаÑувати" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "Закрити" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "Помилки налаштуваннÑ" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"ВиÑвлено одну або декілька помилок налаштуваннÑ. Будь лаÑка, переглÑньте " -"помилки нижче Ñ– або перезавантажте налаштуваннÑ, або проігноруйте ці помилки." - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "Ігнорувати" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "Перезавантажити налаштуваннÑ" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"âš ï¸ Ð’Ð¸ викориÑтовуєте відладочну збірку Ghostty! ПродуктивніÑть буде погіршено." - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: ІнÑпектор терміналу" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "Знайти…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "Попередній збіг" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "ÐаÑтупний збіг" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "Халепа." - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "Ðе вдалоÑÑ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ñ‚Ð¸ контекÑÑ‚ OpenGL Ð´Ð»Ñ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ." - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"Цей термінал працює в режимі читаннÑ. Можна переглÑдати, виділÑти Ñ– гортати " -"вміÑÑ‚, але ввід не буде передано до запущеної програми." - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "Тільки Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "Скопіювати" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "Ð’Ñтавити" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "СповіÑтити про Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð½Ð°Ñтупної команди" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "ОчиÑтити" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "Скинути" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "Панель" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "Змінити заголовок…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "Ðова панель зверху" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "Ðова панель знизу" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "Ðова панель ліворуч" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "Ðова панель праворуч" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "Вкладка" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "Ðова вкладка" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "Закрити вкладку" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "Вікно" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "Ðове вікно" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "Закрити вікно" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "ÐалаштуваннÑ" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "Відкрити налаштуваннÑ" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "Залиште порожнім, щоб відновити заголовок за замовчуваннÑм." - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "ОК" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "Ðова панель" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "ПереглÑнути відкриті вкладки" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "Головне меню" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "Палітра команд" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "ІнÑпектор терміналу" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "Про Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "Завершити" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "Виконати команду…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Програма намагаєтьÑÑ Ð·Ð°Ð¿Ð¸Ñати дані до буфера обміну. Ðижче наведено вміÑÑ‚ " -"буфера обміну." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "" -"Програма намагаєтьÑÑ Ð¿Ñ€Ð¾Ñ‡Ð¸Ñ‚Ð°Ñ‚Ð¸ дані з буфера обміну. Ðижче наведено вміÑÑ‚ " -"буфера обміну." - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "Увага: потенційно небезпечна вÑтавка" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "" -"Ð’Ñтавка цього текÑту в термінал може бути небезпечною, бо Ñхоже, що деÑкі " -"команди можуть бути виконані." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "Завершити Ghostty?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "Закрити вкладку?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "Закрити вікно?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "Закрити панель?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "Ð’ÑÑ– ÑеÑÑ–Ñ— терміналу будуть завершені." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "Ð’ÑÑ– ÑеÑÑ–Ñ— терміналу в цій вкладці будуть завершені." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "Ð’ÑÑ– ÑеÑÑ–Ñ— терміналу в цьому вікні будуть завершені." - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "ПроцеÑ, що виконуєтьÑÑ Ð² цій панелі, буде завершено." - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "Команда завершилаÑÑŒ" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "Команда завершилаÑÑŒ уÑпішно" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "Команда завершилаÑÑŒ з помилкою" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "Команда завершилаÑÑŒ уÑпішно" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "Команда завершилаÑÑŒ з помилкою" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "Змінити заголовок терміналу" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿ÐµÑ€ÐµÐ·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð¾" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "Скопійовано до буферa обміну" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "Буфер обміну очищено" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Розробники Ghostty" diff --git a/po/vi.po b/po/vi.po new file mode 100644 index 00000000000..04bc5d8ed67 --- /dev/null +++ b/po/vi.po @@ -0,0 +1,353 @@ +# Vietnamese translations for com.mitchellh.ghostty package. +# Copyright (C) 2026 "Mitchell Hashimoto, Ghostty contributors" +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Anh Thang , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-03-04 09:32+0700\n" +"Last-Translator: Anh Thang \n" +"Language-Team: Vietnamese \n" +"Language: vi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "Mở Ghostty" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "Cho phép Truy cập Bảng tạm" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "Từ chối" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "Cho phép" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "Ghi nhá»› lá»±a chá»n cho chia màn hình này" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "Tải lại cấu hình để hiển thị lại thông báo này" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "Há»§y" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "Äóng" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "Lá»—i cấu hình" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Phát hiện má»™t hoặc nhiá»u lá»—i cấu hình. Vui lòng xem xét các lá»—i bên dưới, " +"sau đó tải lại cấu hình hoặc bá» qua các lá»—i này." + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "Bá» qua" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "Tải lại cấu hình" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"âš ï¸ Bạn Ä‘ang chạy bản build thá»­ nghiệm (debug) cá»§a Ghostty! Hiệu năng sẽ bị giảm." + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Bá»™ kiểm tra Terminal" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "Tìm kiếm…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "Kết quả trước" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "Kết quả tiếp theo" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "Ôi há»ng rồi." + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "Không thể lấy ngữ cảnh OpenGL để kết xuất đồ há»a." + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"Terminal này Ä‘ang ở chế độ chỉ Ä‘á»c. Bạn vẫn có thể xem, chá»n và cuá»™n " +"ná»™i dung, nhưng các sá»± kiện nhập liệu sẽ không được gá»­i đến ứng dụng." + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "Chỉ Ä‘á»c" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "Sao chép" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "Dán" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "Thông báo khi lệnh tiếp theo kết thúc" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "Xóa" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "Äặt lại" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "Chia màn hình" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "Äổi tiêu Ä‘á»â€¦" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "Chia lên trên" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "Chia xuống dưới" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "Chia sang trái" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "Chia sang phải" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "Tab" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "Äổi tiêu đỠTab…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "Tab má»›i" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "Äóng Tab" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "Cá»­a sổ" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "Cá»­a sổ má»›i" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "Äóng cá»­a sổ" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "Cấu hình" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "Mở tệp cấu hình" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "Äể trống để khôi phục tiêu đỠmặc định." + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "Äồng ý" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "Chia màn hình má»›i" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "Xem các Tab Ä‘ang mở" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "Trình đơn chính" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "Bảng lệnh" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "Bá»™ kiểm tra Terminal" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "Vá» Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "Thoát" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "Chạy má»™t lệnh…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Má»™t ứng dụng Ä‘ang cố gắng ghi vào bảng tạm. Ná»™i dung hiện tại cá»§a " +"bảng tạm được hiển thị bên dưới." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Má»™t ứng dụng Ä‘ang cố gắng Ä‘á»c từ bảng tạm. Ná»™i dung hiện tại cá»§a " +"bảng tạm được hiển thị bên dưới." + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Cảnh báo: Thao tác Dán có thể không an toàn" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Dán văn bản này vào terminal có thể nguy hiểm vì có vẻ như má»™t số " +"lệnh sẽ bị thá»±c thi." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "Thoát Ghostty?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "Äóng Tab?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "Äóng cá»­a sổ?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "Äóng phần chia màn hình?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "Tất cả các phiên làm việc terminal sẽ bị chấm dứt." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Tất cả các phiên làm việc terminal trong tab này sẽ bị chấm dứt." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "Tất cả các phiên làm việc terminal trong cá»­a sổ này sẽ bị chấm dứt." + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "Tiến trình Ä‘ang chạy trong phần chia này sẽ bị chấm dứt." + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "Lệnh đã kết thúc" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "Lệnh thành công" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "Lệnh thất bại" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "Lệnh thành công" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "Lệnh thất bại" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "Äổi tiêu đỠTerminal" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "Äổi tiêu đỠTab" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "Äã tải lại cấu hình" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "Äã sao chép vào bảng tạm" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "Äã xóa sạch bảng tạm" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Các nhà phát triển Ghostty" diff --git a/po/zh_CN.UTF-8.po b/po/zh_CN.UTF-8.po deleted file mode 100644 index 92b79ee2163..00000000000 --- a/po/zh_CN.UTF-8.po +++ /dev/null @@ -1,345 +0,0 @@ -# Chinese translations for com.mitchellh.ghostty package -# com.mitchellh.ghostty 软件包的简体中文翻译. -# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors -# This file is distributed under the same license as the com.mitchellh.ghostty package. -# Leah , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-12 01:56+0800\n" -"Last-Translator: Leah \n" -"Language-Team: Chinese (simplified) \n" -"Language: zh_CN\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "剪贴æ¿è®¿é—®æŽˆæƒ" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "æ‹’ç»" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "å…许" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "为本分å±è®°ä½å½“å‰é€‰æ‹©" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "本æç¤ºå°†åœ¨é‡è½½é…ç½®åŽå†æ¬¡å‡ºçް" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "å–æ¶ˆ" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "关闭" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "é…置错误" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "" -"加载é…置时å‘现了以下错误。请仔细阅读错误信æ¯ï¼Œå¹¶é€‰æ‹©å¿½ç•¥æˆ–釿–°åŠ è½½é…置文件。" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "忽略" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "釿–°åŠ è½½é…ç½®" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "âš ï¸ Ghostty 正在以调试模å¼è¿è¡Œï¼æ€§èƒ½å°†å¤§æ‰“折扣。" - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty 终端调试器" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "查找…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "上一个匹é…项" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "下一个匹é…项" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "糟糕。" - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "未能获å–å¯ç”¨äºŽæ¸²æŸ“çš„ OpenGL 环境。" - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"本终端当å‰å¤„于åªè¯»æ¨¡å¼ã€‚ä½ ä»å¯æµè§ˆã€é€‰æ‹©ã€å¹¶æ»šåŠ¨å…¶ä¸­å†…å®¹ï¼Œä½†ä»»ä½•ç”¨æˆ·è¾“å…¥éƒ½ä¸" -"会传给è¿è¡Œä¸­çš„程åºã€‚" - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "åªè¯»" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "å¤åˆ¶" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "粘贴" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "下æ¡å‘½ä»¤å®Œæˆæ—¶å‘出æé†’" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "清除å±å¹•" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "é‡ç½®ç»ˆç«¯" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "分å±" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "更改标题…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "å‘上分å±" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "å‘下分å±" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "å‘左分å±" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "å‘å³åˆ†å±" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "标签页" - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "新建标签页" - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "关闭标签页" - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "窗å£" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "新建窗å£" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "关闭窗å£" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "é…ç½®" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "打开é…置文件" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "留空以é‡ç½®è‡³é»˜è®¤æ ‡é¢˜ã€‚" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "确认" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "新建分å±" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "æµè§ˆæ ‡ç­¾é¡µ" - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "主èœå•" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "命令颿¿" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "终端调试器" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "关于 Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "退出" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "é€‰æ‹©è¦æ‰§è¡Œçš„命令…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "一个应用正在试图å‘剪贴æ¿å†™å…¥å†…容。剪贴æ¿ç›®å‰çš„内容如下:" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "一个应用正在试图从剪贴æ¿è¯»å–内容。剪贴æ¿ç›®å‰çš„内容如下:" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "警告:粘贴内容å¯èƒ½ä¸å®‰å…¨" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "将以下内容粘贴至终端内将å¯èƒ½æ‰§è¡Œæœ‰å®³å‘½ä»¤ã€‚" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "退出 Ghostty å—?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "关闭标签页å—?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "关闭窗å£å—?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "关闭分å±å—?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "终端内所有è¿è¡Œä¸­çš„进程将被终止。" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "标签页内所有è¿è¡Œä¸­çš„进程将被终止。" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "窗å£å†…所有è¿è¡Œä¸­çš„进程将被终止。" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "分å±å†…正在è¿è¡Œä¸­çš„进程将被终止。" - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "命令已完æˆ" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "命令执行æˆåŠŸ" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "命令执行失败" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "命令执行æˆåŠŸ" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "命令执行失败" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "更改终端标题" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "已釿–°åŠ è½½é…ç½®" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "å·²å¤åˆ¶è‡³å‰ªè´´æ¿" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "已清空剪贴æ¿" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Ghostty å¼€å‘团队" diff --git a/po/zh_CN.po b/po/zh_CN.po new file mode 100644 index 00000000000..8e7e241fc7b --- /dev/null +++ b/po/zh_CN.po @@ -0,0 +1,345 @@ +# Chinese translations for com.mitchellh.ghostty package +# com.mitchellh.ghostty 软件包的简体中文翻译. +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Leah , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-02-12 01:56+0800\n" +"Last-Translator: Leah \n" +"Language-Team: Chinese (simplified) \n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "在 Ghostty 中打开" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "剪贴æ¿è®¿é—®æŽˆæƒ" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "æ‹’ç»" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "å…许" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "为本分å±è®°ä½å½“å‰é€‰æ‹©" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "本æç¤ºå°†åœ¨é‡è½½é…ç½®åŽå†æ¬¡å‡ºçް" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "å–æ¶ˆ" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "关闭" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "é…置错误" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"加载é…置时å‘现了以下错误。请仔细阅读错误信æ¯ï¼Œå¹¶é€‰æ‹©å¿½ç•¥æˆ–釿–°åŠ è½½é…置文件。" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "忽略" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "釿–°åŠ è½½é…ç½®" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "âš ï¸ Ghostty 正在以调试模å¼è¿è¡Œï¼æ€§èƒ½å°†å¤§æ‰“折扣。" + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty 终端调试器" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "查找…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "上一个匹é…项" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "下一个匹é…项" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "糟糕。" + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "未能获å–å¯ç”¨äºŽæ¸²æŸ“çš„ OpenGL 环境。" + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"本终端当å‰å¤„于åªè¯»æ¨¡å¼ã€‚ä½ ä»å¯æµè§ˆã€é€‰æ‹©ã€å¹¶æ»šåŠ¨å…¶ä¸­å†…å®¹ï¼Œä½†ä»»ä½•ç”¨æˆ·è¾“å…¥éƒ½ä¸" +"会传给è¿è¡Œä¸­çš„程åºã€‚" + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "åªè¯»" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "å¤åˆ¶" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "粘贴" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "下æ¡å‘½ä»¤å®Œæˆæ—¶å‘出æé†’" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "清除å±å¹•" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "é‡ç½®ç»ˆç«¯" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "分å±" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "更改标题…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "å‘上分å±" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "å‘下分å±" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "å‘左分å±" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "å‘å³åˆ†å±" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "标签页" + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "更改标签页标题…" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "新建标签页" + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "关闭标签页" + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "窗å£" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "新建窗å£" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "关闭窗å£" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "é…ç½®" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "打开é…置文件" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "留空以é‡ç½®è‡³é»˜è®¤æ ‡é¢˜ã€‚" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "确认" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "新建分å±" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "æµè§ˆæ ‡ç­¾é¡µ" + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "主èœå•" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "命令颿¿" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "终端调试器" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "关于 Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "退出" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "é€‰æ‹©è¦æ‰§è¡Œçš„命令…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "一个应用正在试图å‘剪贴æ¿å†™å…¥å†…容。剪贴æ¿ç›®å‰çš„内容如下:" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "一个应用正在试图从剪贴æ¿è¯»å–内容。剪贴æ¿ç›®å‰çš„内容如下:" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "警告:粘贴内容å¯èƒ½ä¸å®‰å…¨" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "将以下内容粘贴至终端内将å¯èƒ½æ‰§è¡Œæœ‰å®³å‘½ä»¤ã€‚" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "退出 Ghostty å—?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "关闭标签页å—?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "关闭窗å£å—?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "关闭分å±å—?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "终端内所有è¿è¡Œä¸­çš„进程将被终止。" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "标签页内所有è¿è¡Œä¸­çš„进程将被终止。" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "窗å£å†…所有è¿è¡Œä¸­çš„进程将被终止。" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "分å±å†…正在è¿è¡Œä¸­çš„进程将被终止。" + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "命令已完æˆ" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "命令执行æˆåŠŸ" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "命令执行失败" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "命令执行æˆåŠŸ" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "命令执行失败" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "更改终端标题" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "更改标签页标题" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "已釿–°åŠ è½½é…ç½®" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "å·²å¤åˆ¶è‡³å‰ªè´´æ¿" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "已清空剪贴æ¿" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Ghostty å¼€å‘团队" diff --git a/po/zh_TW.UTF-8.po b/po/zh_TW.UTF-8.po deleted file mode 100644 index 25dacd566ff..00000000000 --- a/po/zh_TW.UTF-8.po +++ /dev/null @@ -1,343 +0,0 @@ -# Traditional Chinese (Taiwan) translation for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto -# This file is distributed under the same license as the com.mitchellh.ghostty package. -# Peter Dave Hello , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: com.mitchellh.ghostty\n" -"Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-10 15:32+0800\n" -"Last-Translator: Yi-Jyun Pan \n" -"Language-Team: Chinese (traditional)\n" -"Language: zh_TW\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: dist/linux/ghostty_nautilus.py:53 -msgid "Open in Ghostty" -msgstr "" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 -msgid "Authorize Clipboard Access" -msgstr "授權存å–剪貼簿" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 -msgid "Deny" -msgstr "拒絕" - -#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 -msgid "Allow" -msgstr "å…許" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 -msgid "Remember choice for this split" -msgstr "è¨˜ä½æ­¤çª—æ ¼çš„é¸æ“‡" - -#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 -msgid "Reload configuration to show this prompt again" -msgstr "釿–°è¼‰å…¥è¨­å®šä»¥å†æ¬¡é¡¯ç¤ºæ­¤æç¤º" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 -#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 -msgid "Cancel" -msgstr "å–æ¶ˆ" - -#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 -#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 -#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 -msgid "Close" -msgstr "關閉" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 -msgid "Configuration Errors" -msgstr "設定錯誤" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 -msgid "" -"One or more configuration errors were found. Please review the errors below, " -"and either reload your configuration or ignore these errors." -msgstr "ç™¼ç¾æœ‰è¨­å®šéŒ¯èª¤ã€‚è«‹æª¢è¦–ä»¥ä¸‹éŒ¯èª¤ï¼Œä¸¦é‡æ–°è¼‰å…¥è¨­å®šæˆ–忽略這些錯誤。" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 -msgid "Ignore" -msgstr "忽略" - -#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 -#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 -msgid "Reload Configuration" -msgstr "釿–°è¼‰å…¥è¨­å®š" - -#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 -#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 -msgid "" -"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "âš ï¸ æ‚¨æ­£åœ¨åŸ·è¡Œ Ghostty 的除錯版本ï¼ç¨‹å¼é‹ä½œæ•ˆèƒ½å°‡æœƒå—到影響。" - -#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty:終端機檢查工具" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 -msgid "Find…" -msgstr "尋找…" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 -msgid "Previous Match" -msgstr "上一筆符åˆ" - -#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 -msgid "Next Match" -msgstr "下一筆符åˆ" - -#: src/apprt/gtk/ui/1.2/surface.blp:6 -msgid "Oh, no." -msgstr "噢ä¸ã€‚" - -#: src/apprt/gtk/ui/1.2/surface.blp:7 -msgid "Unable to acquire an OpenGL context for rendering." -msgstr "無法å–得用於算繪的 OpenGL 上下文。" - -#: src/apprt/gtk/ui/1.2/surface.blp:97 -msgid "" -"This terminal is in read-only mode. You can still view, select, and scroll " -"through the content, but no input events will be sent to the running " -"application." -msgstr "" -"本終端機目å‰è™•於唯讀模å¼ã€‚您ä»å¯æŸ¥çœ‹ã€é¸å–åŠæ²å‹•å…§å®¹ï¼Œä½†ä¸æœƒå‚³é€ä»»ä½•輸入事件" -"至執行中的應用程å¼ã€‚" - -#: src/apprt/gtk/ui/1.2/surface.blp:107 -msgid "Read-only" -msgstr "唯讀" - -#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 -msgid "Copy" -msgstr "複製" - -#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 -msgid "Paste" -msgstr "貼上" - -#: src/apprt/gtk/ui/1.2/surface.blp:270 -msgid "Notify on Next Command Finish" -msgstr "ä¸‹å€‹å‘½ä»¤å®Œæˆæ™‚通知" - -#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 -msgid "Clear" -msgstr "清除" - -#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 -msgid "Reset" -msgstr "é‡è¨­" - -#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 -msgid "Split" -msgstr "分割" - -#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 -msgid "Change Title…" -msgstr "變更標題…" - -#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 -#: src/apprt/gtk/ui/1.5/window.blp:250 -msgid "Split Up" -msgstr "å‘上分割" - -#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 -#: src/apprt/gtk/ui/1.5/window.blp:255 -msgid "Split Down" -msgstr "å‘下分割" - -#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 -#: src/apprt/gtk/ui/1.5/window.blp:260 -msgid "Split Left" -msgstr "å‘左分割" - -#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 -#: src/apprt/gtk/ui/1.5/window.blp:265 -msgid "Split Right" -msgstr "å‘å³åˆ†å‰²" - -#: src/apprt/gtk/ui/1.2/surface.blp:322 -msgid "Tab" -msgstr "分é " - -#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 -#: src/apprt/gtk/ui/1.5/window.blp:320 -msgid "Change Tab Title…" -msgstr "è®Šæ›´åˆ†é æ¨™é¡Œâ€¦" - -#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 -#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 -msgid "New Tab" -msgstr "開新分é " - -#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 -msgid "Close Tab" -msgstr "關閉分é " - -#: src/apprt/gtk/ui/1.2/surface.blp:342 -msgid "Window" -msgstr "視窗" - -#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 -msgid "New Window" -msgstr "開新視窗" - -#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 -msgid "Close Window" -msgstr "關閉視窗" - -#: src/apprt/gtk/ui/1.2/surface.blp:358 -msgid "Config" -msgstr "設定" - -#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 -msgid "Open Configuration" -msgstr "開啟設定" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 -msgid "Leave blank to restore the default title." -msgstr "留空å³å¯é‚„原為é è¨­æ¨™é¡Œã€‚" - -#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 -msgid "OK" -msgstr "確定" - -#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 -msgid "New Split" -msgstr "新增窗格" - -#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 -msgid "View Open Tabs" -msgstr "檢視已開啟的分é " - -#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 -msgid "Main Menu" -msgstr "主é¸å–®" - -#: src/apprt/gtk/ui/1.5/window.blp:285 -msgid "Command Palette" -msgstr "命令颿¿" - -#: src/apprt/gtk/ui/1.5/window.blp:290 -msgid "Terminal Inspector" -msgstr "終端機檢查工具" - -#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 -msgid "About Ghostty" -msgstr "關於 Ghostty" - -#: src/apprt/gtk/ui/1.5/window.blp:312 -msgid "Quit" -msgstr "çµæŸ" - -#: src/apprt/gtk/ui/1.5/command-palette.blp:17 -msgid "Execute a command…" -msgstr "執行命令…" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 -msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." -msgstr "æœ‰æ‡‰ç”¨ç¨‹å¼æ­£å˜—試寫入剪貼簿,目å‰çš„剪貼簿內容顯示如下。" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 -msgid "" -"An application is attempting to read from the clipboard. The current " -"clipboard contents are shown below." -msgstr "æœ‰æ‡‰ç”¨ç¨‹å¼æ­£å˜—試讀å–剪貼簿,目å‰çš„剪貼簿內容顯示如下。" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 -msgid "Warning: Potentially Unsafe Paste" -msgstr "警告:å¯èƒ½æœ‰æ½›åœ¨å®‰å…¨é¢¨éšªçš„貼上æ“作" - -#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 -msgid "" -"Pasting this text into the terminal may be dangerous as it looks like some " -"commands may be executed." -msgstr "å°‡é€™æ®µæ–‡å­—è²¼åˆ°çµ‚ç«¯æ©Ÿå…·æœ‰æ½›åœ¨é¢¨éšªï¼Œå› ç‚ºå®ƒçœ‹èµ·ä¾†åƒæ˜¯å¯èƒ½æœƒè¢«åŸ·è¡Œçš„命令。" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 -msgid "Quit Ghostty?" -msgstr "è¦çµæŸ Ghostty 嗎?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 -msgid "Close Tab?" -msgstr "是å¦è¦é—œé–‰åˆ†é ï¼Ÿ" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 -msgid "Close Window?" -msgstr "是å¦è¦é—œé–‰è¦–窗?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 -msgid "Close Split?" -msgstr "是å¦è¦é—œé–‰çª—格?" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 -msgid "All terminal sessions will be terminated." -msgstr "所有終端機工作階段都將被終止。" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 -msgid "All terminal sessions in this tab will be terminated." -msgstr "此分é ä¸­çš„æ‰€æœ‰çµ‚端機工作階段都將被終止。" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 -msgid "All terminal sessions in this window will be terminated." -msgstr "此視窗中的所有終端機工作階段都將被終止。" - -#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 -msgid "The currently running process in this split will be terminated." -msgstr "此窗格中目å‰åŸ·è¡Œçš„處ç†ç¨‹åºå°‡è¢«çµ‚止。" - -#: src/apprt/gtk/class/surface.zig:1108 -msgid "Command Finished" -msgstr "命令執行完æˆ" - -#: src/apprt/gtk/class/surface.zig:1109 -msgid "Command Succeeded" -msgstr "命令執行æˆåŠŸ" - -#: src/apprt/gtk/class/surface.zig:1110 -msgid "Command Failed" -msgstr "命令執行失敗" - -#: src/apprt/gtk/class/surface_child_exited.zig:109 -msgid "Command succeeded" -msgstr "命令執行æˆåŠŸ" - -#: src/apprt/gtk/class/surface_child_exited.zig:113 -msgid "Command failed" -msgstr "命令執行失敗" - -#: src/apprt/gtk/class/title_dialog.zig:225 -msgid "Change Terminal Title" -msgstr "變更終端機標題" - -#: src/apprt/gtk/class/title_dialog.zig:226 -msgid "Change Tab Title" -msgstr "è®Šæ›´åˆ†é æ¨™é¡Œ" - -#: src/apprt/gtk/class/window.zig:1007 -msgid "Reloaded the configuration" -msgstr "已釿–°è¼‰å…¥è¨­å®š" - -#: src/apprt/gtk/class/window.zig:1566 -msgid "Copied to clipboard" -msgstr "已複製到剪貼簿" - -#: src/apprt/gtk/class/window.zig:1568 -msgid "Cleared clipboard" -msgstr "已清除剪貼簿" - -#: src/apprt/gtk/class/window.zig:1708 -msgid "Ghostty Developers" -msgstr "Ghostty 開發者" diff --git a/po/zh_TW.po b/po/zh_TW.po new file mode 100644 index 00000000000..cacdc8acbb7 --- /dev/null +++ b/po/zh_TW.po @@ -0,0 +1,343 @@ +# Traditional Chinese (Taiwan) translation for com.mitchellh.ghostty package. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Peter Dave Hello , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2026-02-17 23:16+0100\n" +"PO-Revision-Date: 2026-02-18 13:58+0800\n" +"Last-Translator: Yi-Jyun Pan \n" +"Language-Team: Chinese (traditional)\n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: dist/linux/ghostty_nautilus.py:53 +msgid "Open in Ghostty" +msgstr "在 Ghostty 中開啟" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197 +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201 +msgid "Authorize Clipboard Access" +msgstr "授權存å–剪貼簿" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17 +msgid "Deny" +msgstr "拒絕" + +#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18 +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18 +msgid "Allow" +msgstr "å…許" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92 +msgid "Remember choice for this split" +msgstr "è¨˜ä½æ­¤çª—æ ¼çš„é¸æ“‡" + +#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93 +msgid "Reload configuration to show this prompt again" +msgstr "釿–°è¼‰å…¥è¨­å®šä»¥å†æ¬¡é¡¯ç¤ºæ­¤æç¤º" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7 +#: src/apprt/gtk/ui/1.5/title-dialog.blp:8 +msgid "Cancel" +msgstr "å–æ¶ˆ" + +#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8 +#: src/apprt/gtk/ui/1.2/search-overlay.blp:85 +#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17 +msgid "Close" +msgstr "關閉" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "Configuration Errors" +msgstr "設定錯誤" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "ç™¼ç¾æœ‰è¨­å®šéŒ¯èª¤ã€‚è«‹æª¢è¦–ä»¥ä¸‹éŒ¯èª¤ï¼Œä¸¦é‡æ–°è¼‰å…¥è¨­å®šæˆ–忽略這些錯誤。" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Ignore" +msgstr "忽略" + +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 +#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 +msgid "Reload Configuration" +msgstr "釿–°è¼‰å…¥è¨­å®š" + +#: src/apprt/gtk/ui/1.2/debug-warning.blp:7 +#: src/apprt/gtk/ui/1.3/debug-warning.blp:6 +msgid "" +"âš ï¸ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "âš ï¸ æ‚¨æ­£åœ¨åŸ·è¡Œ Ghostty 的除錯版本ï¼ç¨‹å¼é‹ä½œæ•ˆèƒ½å°‡æœƒå—到影響。" + +#: src/apprt/gtk/ui/1.5/inspector-window.blp:5 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty:終端機檢查工具" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:29 +msgid "Find…" +msgstr "尋找…" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:64 +msgid "Previous Match" +msgstr "上一筆符åˆ" + +#: src/apprt/gtk/ui/1.2/search-overlay.blp:74 +msgid "Next Match" +msgstr "下一筆符åˆ" + +#: src/apprt/gtk/ui/1.2/surface.blp:6 +msgid "Oh, no." +msgstr "噢ä¸ã€‚" + +#: src/apprt/gtk/ui/1.2/surface.blp:7 +msgid "Unable to acquire an OpenGL context for rendering." +msgstr "無法å–得用於算繪的 OpenGL 上下文。" + +#: src/apprt/gtk/ui/1.2/surface.blp:97 +msgid "" +"This terminal is in read-only mode. You can still view, select, and scroll " +"through the content, but no input events will be sent to the running " +"application." +msgstr "" +"本終端機目å‰è™•於唯讀模å¼ã€‚您ä»å¯æŸ¥çœ‹ã€é¸å–åŠæ²å‹•å…§å®¹ï¼Œä½†ä¸æœƒå‚³é€ä»»ä½•輸入事件" +"至執行中的應用程å¼ã€‚" + +#: src/apprt/gtk/ui/1.2/surface.blp:107 +msgid "Read-only" +msgstr "唯讀" + +#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 +msgid "Copy" +msgstr "複製" + +#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205 +msgid "Paste" +msgstr "貼上" + +#: src/apprt/gtk/ui/1.2/surface.blp:270 +msgid "Notify on Next Command Finish" +msgstr "ä¸‹å€‹å‘½ä»¤å®Œæˆæ™‚通知" + +#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 +msgid "Clear" +msgstr "清除" + +#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278 +msgid "Reset" +msgstr "é‡è¨­" + +#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242 +msgid "Split" +msgstr "分割" + +#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 +msgid "Change Title…" +msgstr "變更標題…" + +#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 +#: src/apprt/gtk/ui/1.5/window.blp:250 +msgid "Split Up" +msgstr "å‘上分割" + +#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182 +#: src/apprt/gtk/ui/1.5/window.blp:255 +msgid "Split Down" +msgstr "å‘下分割" + +#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187 +#: src/apprt/gtk/ui/1.5/window.blp:260 +msgid "Split Left" +msgstr "å‘左分割" + +#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192 +#: src/apprt/gtk/ui/1.5/window.blp:265 +msgid "Split Right" +msgstr "å‘å³åˆ†å‰²" + +#: src/apprt/gtk/ui/1.2/surface.blp:322 +msgid "Tab" +msgstr "分é " + +#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 +#: src/apprt/gtk/ui/1.5/window.blp:320 +msgid "Change Tab Title…" +msgstr "è®Šæ›´åˆ†é æ¨™é¡Œâ€¦" + +#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 +#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 +msgid "New Tab" +msgstr "開新分é " + +#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234 +msgid "Close Tab" +msgstr "關閉分é " + +#: src/apprt/gtk/ui/1.2/surface.blp:342 +msgid "Window" +msgstr "視窗" + +#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212 +msgid "New Window" +msgstr "開新視窗" + +#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217 +msgid "Close Window" +msgstr "關閉視窗" + +#: src/apprt/gtk/ui/1.2/surface.blp:358 +msgid "Config" +msgstr "設定" + +#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295 +msgid "Open Configuration" +msgstr "開啟設定" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:5 +msgid "Leave blank to restore the default title." +msgstr "留空å³å¯é‚„原為é è¨­æ¨™é¡Œã€‚" + +#: src/apprt/gtk/ui/1.5/title-dialog.blp:9 +msgid "OK" +msgstr "確定" + +#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108 +msgid "New Split" +msgstr "新增窗格" + +#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126 +msgid "View Open Tabs" +msgstr "檢視已開啟的分é " + +#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140 +msgid "Main Menu" +msgstr "主é¸å–®" + +#: src/apprt/gtk/ui/1.5/window.blp:285 +msgid "Command Palette" +msgstr "命令颿¿" + +#: src/apprt/gtk/ui/1.5/window.blp:290 +msgid "Terminal Inspector" +msgstr "終端機檢查工具" + +#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727 +msgid "About Ghostty" +msgstr "關於 Ghostty" + +#: src/apprt/gtk/ui/1.5/window.blp:312 +msgid "Quit" +msgstr "çµæŸ" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:17 +msgid "Execute a command…" +msgstr "執行命令…" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "æœ‰æ‡‰ç”¨ç¨‹å¼æ­£å˜—試寫入剪貼簿,目å‰çš„剪貼簿內容顯示如下。" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "æœ‰æ‡‰ç”¨ç¨‹å¼æ­£å˜—試讀å–剪貼簿,目å‰çš„剪貼簿內容顯示如下。" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 +msgid "Warning: Potentially Unsafe Paste" +msgstr "警告:å¯èƒ½æœ‰æ½›åœ¨å®‰å…¨é¢¨éšªçš„貼上æ“作" + +#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "å°‡é€™æ®µæ–‡å­—è²¼åˆ°çµ‚ç«¯æ©Ÿå…·æœ‰æ½›åœ¨é¢¨éšªï¼Œå› ç‚ºå®ƒçœ‹èµ·ä¾†åƒæ˜¯å¯èƒ½æœƒè¢«åŸ·è¡Œçš„命令。" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:184 +msgid "Quit Ghostty?" +msgstr "è¦çµæŸ Ghostty 嗎?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:185 +msgid "Close Tab?" +msgstr "是å¦è¦é—œé–‰åˆ†é ï¼Ÿ" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:186 +msgid "Close Window?" +msgstr "是å¦è¦é—œé–‰è¦–窗?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:187 +msgid "Close Split?" +msgstr "是å¦è¦é—œé–‰çª—格?" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:193 +msgid "All terminal sessions will be terminated." +msgstr "所有終端機工作階段都將被終止。" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:194 +msgid "All terminal sessions in this tab will be terminated." +msgstr "此分é ä¸­çš„æ‰€æœ‰çµ‚端機工作階段都將被終止。" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:195 +msgid "All terminal sessions in this window will be terminated." +msgstr "此視窗中的所有終端機工作階段都將被終止。" + +#: src/apprt/gtk/class/close_confirmation_dialog.zig:196 +msgid "The currently running process in this split will be terminated." +msgstr "此窗格中目å‰åŸ·è¡Œçš„處ç†ç¨‹åºå°‡è¢«çµ‚止。" + +#: src/apprt/gtk/class/surface.zig:1108 +msgid "Command Finished" +msgstr "命令執行完æˆ" + +#: src/apprt/gtk/class/surface.zig:1109 +msgid "Command Succeeded" +msgstr "命令執行æˆåŠŸ" + +#: src/apprt/gtk/class/surface.zig:1110 +msgid "Command Failed" +msgstr "命令執行失敗" + +#: src/apprt/gtk/class/surface_child_exited.zig:109 +msgid "Command succeeded" +msgstr "命令執行æˆåŠŸ" + +#: src/apprt/gtk/class/surface_child_exited.zig:113 +msgid "Command failed" +msgstr "命令執行失敗" + +#: src/apprt/gtk/class/title_dialog.zig:225 +msgid "Change Terminal Title" +msgstr "變更終端機標題" + +#: src/apprt/gtk/class/title_dialog.zig:226 +msgid "Change Tab Title" +msgstr "è®Šæ›´åˆ†é æ¨™é¡Œ" + +#: src/apprt/gtk/class/window.zig:1007 +msgid "Reloaded the configuration" +msgstr "已釿–°è¼‰å…¥è¨­å®š" + +#: src/apprt/gtk/class/window.zig:1566 +msgid "Copied to clipboard" +msgstr "已複製到剪貼簿" + +#: src/apprt/gtk/class/window.zig:1568 +msgid "Cleared clipboard" +msgstr "已清除剪貼簿" + +#: src/apprt/gtk/class/window.zig:1708 +msgid "Ghostty Developers" +msgstr "Ghostty 開發者" diff --git a/snap/local/launcher b/snap/local/launcher index 71b92f5bb80..6057881b3cb 100755 --- a/snap/local/launcher +++ b/snap/local/launcher @@ -41,7 +41,7 @@ fi export LD_LIBRARY_PATH=${SNAP}/usr/lib/${ARCH}:${SNAP}/usr/lib/${ARCH}/vdpau:${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:} export LIBGL_DRIVERS_PATH=${LIBGL_DRIVERS_PATH:+$LIBGL_DRIVERS_PATH:}${SNAP}/usr/lib/${ARCH}/dri/ export LIBVA_DRIVERS_PATH=${LIBVA_DRIVERS_PATH:+$LIBVA_DRIVERS_PATH:}${SNAP}/usr/lib/${ARCH}/dri/ -export __EGL_VENDOR_LIBRARY_DIRS=${__EGL_VENDOR_LIBRARY_DIRS:+$__EGL_VENDOR_LIBRARY_DIRS:}${SNAP}/usr/share/glvnd/egl_vendor.d +export __EGL_VENDOR_LIBRARY_DIRS=${__EGL_VENDOR_LIBRARY_DIRS:+$__EGL_VENDOR_LIBRARY_DIRS:}/etc/glvnd/egl_vendor.d:/usr/share/glvnd/egl_vendor.d:${SNAP}/usr/share/glvnd/egl_vendor.d export __EGL_EXTERNAL_PLATFORM_CONFIG_DIRS=${__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS:+$__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS:}${SNAP}/usr/share/egl/egl_external_platform.d export DRIRC_CONFIGDIR=${SNAP}/usr/share/drirc.d export VK_LAYER_PATH=${VK_LAYER_PATH:+$VK_LAYER_PATH:}${SNAP}/usr/share/vulkan/implicit_layer.d/:${SNAP}/usr/share/vulkan/explicit_layer.d/ diff --git a/src/Command.zig b/src/Command.zig index 3a40143b948..2b381912b66 100644 --- a/src/Command.zig +++ b/src/Command.zig @@ -725,6 +725,11 @@ test "Command: redirect stdout to file" { .path = "C:\\Windows\\System32\\whoami.exe", .args = &.{"C:\\Windows\\System32\\whoami.exe"}, .stdout = stdout, + .os_pre_exec = null, + .rt_pre_exec = null, + .rt_post_fork = null, + .rt_pre_exec_info = undefined, + .rt_post_fork_info = undefined, } else .{ .path = "/bin/sh", .args = &.{ "/bin/sh", "-c", "echo hello" }, diff --git a/src/Surface.zig b/src/Surface.zig index 588d529689d..55eacf28328 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -46,8 +46,8 @@ const Renderer = rendererpkg.Renderer; /// being resized to a size that is too small to be useful. These defaults /// are chosen to match the default size of Mac's Terminal.app, but is /// otherwise somewhat arbitrary. -const min_window_width_cells: u32 = 10; -const min_window_height_cells: u32 = 4; +pub const min_window_width_cells: u32 = 10; +pub const min_window_height_cells: u32 = 4; /// The maximum number of key tables that can be active at any /// given time. `activate_key_table` calls after this are ignored. @@ -312,6 +312,7 @@ const DerivedConfig = struct { mouse_reporting: bool, mouse_scroll_multiplier: configpkg.MouseScrollMultiplier, mouse_shift_capture: configpkg.MouseShiftCapture, + fullscreen: configpkg.Fullscreen, macos_non_native_fullscreen: configpkg.NonNativeFullscreen, macos_option_as_alt: ?input.OptionAsAlt, selection_clear_on_copy: bool, @@ -323,7 +324,7 @@ const DerivedConfig = struct { window_padding_bottom: u32, window_padding_left: u32, window_padding_right: u32, - window_padding_balance: bool, + window_padding_balance: configpkg.Config.WindowPaddingBalance, window_height: u32, window_width: u32, title: ?[:0]const u8, @@ -389,6 +390,7 @@ const DerivedConfig = struct { .mouse_reporting = config.@"mouse-reporting", .mouse_scroll_multiplier = config.@"mouse-scroll-multiplier", .mouse_shift_capture = config.@"mouse-shift-capture", + .fullscreen = config.fullscreen, .macos_non_native_fullscreen = config.@"macos-non-native-fullscreen", .macos_option_as_alt = config.@"macos-option-as-alt", .selection_clear_on_copy = config.@"selection-clear-on-copy", @@ -534,8 +536,8 @@ pub fn init( x_dpi, y_dpi, ); - if (derived_config.window_padding_balance) { - size.balancePadding(explicit); + if (derived_config.window_padding_balance != .false) { + size.balancePadding(explicit, derived_config.window_padding_balance); } else { size.padding = explicit; } @@ -605,10 +607,14 @@ pub fn init( }; // The command we're going to execute - const command: ?configpkg.Command = if (app.first) - config.@"initial-command" orelse config.command - else - config.command; + const command: ?configpkg.Command = command: { + if (app.first) { + if (config.@"initial-command") |command| { + break :command command; + } + } + break :command config.command; + }; // Start our IO implementation // This separate block ({}) is important because our errdefers must @@ -633,7 +639,7 @@ pub fn init( .shell_integration = config.@"shell-integration", .shell_integration_features = config.@"shell-integration-features", .cursor_blink = config.@"cursor-style-blink", - .working_directory = config.@"working-directory", + .working_directory = if (config.@"working-directory") |wd| wd.value() else null, .resources_dir = global_state.resources_dir.host(), .term = config.term, .rt_pre_exec_info = .init(config), @@ -1174,7 +1180,7 @@ fn selectionScrollTick(self: *Surface) !void { } // Scroll the viewport as required - try t.scrollViewport(.{ .delta = delta }); + t.scrollViewport(.{ .delta = delta }); // Next, trigger our drag behavior const pin = t.screens.active.pages.pin(.{ @@ -2010,6 +2016,46 @@ pub fn hasSelection(self: *const Surface) bool { return self.io.terminal.screens.active.selection != null; } +/// Start a selection anchored at the active cursor position. +pub fn selectCursorCell(self: *Surface) !bool { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + const screen: *terminal.Screen = self.io.terminal.screens.active; + const pin = pin: { + // pointFromPin(.viewport, ...) can return points beyond the visible rows + // when the viewport includes additional written history; only anchor to + // the live cursor if it is truly visible in the viewport. + if (screen.pages.pointFromPin(.viewport, screen.cursor.page_pin.*)) |pt| { + if (pt.viewport.y < @as(u32, screen.pages.rows)) { + break :pin screen.cursor.page_pin.*; + } + } + + // If we've scrolled away from the live cursor, start at the viewport origin + // so copy mode can select from the currently visible history. + break :pin screen.pages.pin(.{ .viewport = .{} }) orelse return false; + }; + // Entering keyboard copy mode should not clobber clipboard contents. + try screen.select(terminal.Selection.init(pin, pin, false)); + screen.dirty.selection = true; + try self.queueRender(); + return true; +} + +/// Clear the active selection, if any. +pub fn clearSelection(self: *Surface) !bool { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + const screen: *terminal.Screen = self.io.terminal.screens.active; + if (screen.selection == null) return false; + try self.setSelection(null); + screen.dirty.selection = true; + try self.queueRender(); + return true; +} + /// Returns the selected text. This is allocated. pub fn selectionString(self: *Surface, alloc: Allocator) !?[:0]const u8 { self.renderer_state.mutex.lock(); @@ -2432,6 +2478,12 @@ pub fn sizeCallback(self: *Surface, size: apprt.SurfaceSize) !void { } fn resize(self: *Surface, size: rendererpkg.ScreenSize) !void { + // Resizes can arrive during UI/layout transitions where the surface is momentarily + // too small to fit even a single terminal cell. Rendering at a 0xN or Nx0 grid + // produces a transient "blank" frame. Keep the last valid size until we have + // a usable grid again. + const prev_size = self.size; + // Save our screen size self.size.screen = size; self.balancePaddingIfNeeded(); @@ -2441,6 +2493,10 @@ fn resize(self: *Surface, size: rendererpkg.ScreenSize) !void { // We have to update the IO thread no matter what because we send // pixel-level sizing to the subprocess. const grid_size = self.size.grid(); + if (grid_size.columns == 0 or grid_size.rows == 0) { + self.size = prev_size; + return; + } if (grid_size.columns < 5 and (self.size.padding.left > 0 or self.size.padding.right > 0)) { log.warn("WARNING: very small terminal grid detected with padding " ++ "set. Is your padding reasonable?", .{}); @@ -2456,11 +2512,11 @@ fn resize(self: *Surface, size: rendererpkg.ScreenSize) !void { /// Recalculate the balanced padding if needed. fn balancePaddingIfNeeded(self: *Surface) void { - if (!self.config.window_padding_balance) return; + if (self.config.window_padding_balance == .false) return; const content_scale = try self.rt_surface.getContentScale(); const x_dpi = content_scale.x * font.face.default_dpi; const y_dpi = content_scale.y * font.face.default_dpi; - self.size.balancePadding(self.config.scaledPadding(x_dpi, y_dpi)); + self.size.balancePadding(self.config.scaledPadding(x_dpi, y_dpi), self.config.window_padding_balance); } /// Called to set the preedit state for character input. Preedit is used @@ -2779,7 +2835,7 @@ pub fn keyCallback( try self.setSelection(null); } - if (self.config.scroll_to_bottom.keystroke) try self.io.terminal.scrollViewport(.bottom); + if (self.config.scroll_to_bottom.keystroke) self.io.terminal.scrollViewport(.bottom); try self.queueRender(); } @@ -2971,6 +3027,9 @@ fn maybeHandleBinding( // If our action was "ignore" then we return the special input // effect of "ignored". for (actions) |action| if (action == .ignore) { + // If we're in a sequence, clear it. + self.endKeySequence(.drop, .retain); + return .ignored; }; } @@ -3509,7 +3568,7 @@ pub fn scrollCallback( if (self.isMouseReporting()) { for (0..@abs(y.delta)) |_| { const pos = try self.rt_surface.getCursorPos(); - try self.mouseReport(switch (y.direction()) { + self.mouseReport(switch (y.direction()) { .up_right => .four, .down_left => .five, }, .press, self.mouse.mods, pos); @@ -3517,7 +3576,7 @@ pub fn scrollCallback( for (0..@abs(x.delta)) |_| { const pos = try self.rt_surface.getCursorPos(); - try self.mouseReport(switch (x.direction()) { + self.mouseReport(switch (x.direction()) { .up_right => .six, .down_left => .seven, }, .press, self.mouse.mods, pos); @@ -3532,7 +3591,7 @@ pub fn scrollCallback( // Modify our viewport, this requires a lock since it affects // rendering. We have to switch signs here because our delta // is negative down but our viewport is positive down. - try self.io.terminal.scrollViewport(.{ .delta = y.delta * -1 }); + self.io.terminal.scrollViewport(.{ .delta = y.delta * -1 }); } } @@ -3567,7 +3626,7 @@ pub fn contentScaleCallback(self: *Surface, content_scale: apprt.ContentScale) ! // Update our padding which is dependent on DPI. We only do this for // unbalanced padding since balanced padding is not dependent on DPI. - if (!self.config.window_padding_balance) { + if (self.config.window_padding_balance == .false) { self.size.padding = self.config.scaledPadding(x_dpi, y_dpi); } @@ -3576,9 +3635,6 @@ pub fn contentScaleCallback(self: *Surface, content_scale: apprt.ContentScale) ! try self.resize(self.size.screen); } -/// 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 { @@ -3589,228 +3645,65 @@ fn isMouseReporting(self: *const Surface) bool { fn mouseReport( self: *Surface, button: ?input.MouseButton, - action: MouseReportAction, + action: input.MouseAction, mods: input.Mods, pos: apprt.CursorPos, -) !void { +) 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 => unreachable, // checked by assert above - - // X10 only reports clicks with mouse button 1, 2, 3. We verify - // the button later. - .x10 => if (action != .press or - button == null or - !(button.? == .left or - button.? == .right or - button.? == .middle)) return, - - // Doesn't report motion - .normal => if (action == .motion) return, - - // Button must be pressed - .button => if (button == null) return, - - // Everything - .any => {}, - } - - // Handle scenarios where the mouse position is outside the viewport. - // We always report release events no matter where they happen. - if (action != .release) { - const pos_out_viewport = pos_out_viewport: { - const max_x: f32 = @floatFromInt(self.size.screen.width); - const max_y: f32 = @floatFromInt(self.size.screen.height); - break :pos_out_viewport pos.x < 0 or pos.y < 0 or - pos.x > max_x or pos.y > max_y; - }; - if (pos_out_viewport) outside_viewport: { - // If we don't have a motion-tracking event mode, do nothing. - if (!self.io.terminal.flags.mouse_event.motion()) return; + // Build our encoding options. + const encoding_opts: input.mouse_encode.Options = opts: { + // Terminal and size state. + var opts: input.mouse_encode.Options = .fromTerminal( + &self.io.terminal, + self.size, + ); - // If any button is pressed, we still do the report. Otherwise, - // we do not do the report. + // Whether any button is pressed at all. + opts.any_button_pressed = pressed: { for (self.mouse.click_state) |state| { - if (state != .release) break :outside_viewport; + if (state != .release) break :pressed true; } - return; - } - } - - // This format reports X/Y - const viewport_point = self.posToViewport(pos.x, pos.y); - - // Record our new point. We only want to send a mouse event if the - // cell changed, unless we're tracking raw pixels. - if (action == .motion and self.io.terminal.flags.mouse_format != .sgr_pixels) { - if (self.mouse.event_point) |last_point| { - if (last_point.eql(viewport_point)) return; - } - } - self.mouse.event_point = viewport_point; - - // Get the code we'll actually write - const button_code: u8 = code: { - var acc: u8 = 0; - - // Determine our initial button value - if (button == null) { - // Null button means motion without a button pressed - acc = 3; - } else if (action == .release and - self.io.terminal.flags.mouse_format != .sgr and - self.io.terminal.flags.mouse_format != .sgr_pixels) - { - // Release is 3. It is NOT 3 in SGR mode because SGR can tell - // the application what button was released. - acc = 3; - } else { - acc = switch (button.?) { - .left => 0, - .middle => 1, - .right => 2, - .four => 64, - .five => 65, - .six => 66, - .seven => 67, - .eight => 128, - .nine => 129, - else => return, // unsupported - }; - } - - // X10 doesn't have modifiers - if (self.io.terminal.flags.mouse_event != .x10) { - if (mods.shift) acc += 4; - if (mods.alt) acc += 8; - if (mods.ctrl) acc += 16; - } + break :pressed false; + }; - // Motion adds another bit - if (action == .motion) acc += 32; + // Keep track of our last reported viewport cell for event + // deduplication. + opts.last_cell = &self.mouse.event_point; - break :code acc; + break :opts opts; }; - switch (self.io.terminal.flags.mouse_format) { - .x10 => { - if (viewport_point.x > 222 or viewport_point.y > 222) { - log.info("X10 mouse format can only encode X/Y up to 223", .{}); - return; - } - - // + 1 below is because our x/y is 0-indexed and the protocol wants 1 - var data: termio.Message.WriteReq.Small.Array = undefined; - assert(data.len >= 6); - data[0] = '\x1b'; - data[1] = '['; - data[2] = 'M'; - data[3] = 32 + button_code; - data[4] = 32 + @as(u8, @intCast(viewport_point.x)) + 1; - data[5] = 32 + @as(u8, @intCast(viewport_point.y)) + 1; - - // Ask our IO thread to write the data - self.queueIo(.{ .write_small = .{ - .data = data, - .len = 6, - } }, .locked); - }, - - .utf8 => { - // Maximum of 12 because at most we have 2 fully UTF-8 encoded chars - var data: termio.Message.WriteReq.Small.Array = undefined; - assert(data.len >= 12); - data[0] = '\x1b'; - data[1] = '['; - data[2] = 'M'; - - // The button code will always fit in a single u8 - data[3] = 32 + button_code; - - // UTF-8 encode the x/y - var i: usize = 4; - i += try std.unicode.utf8Encode(@intCast(32 + viewport_point.x + 1), data[i..]); - i += try std.unicode.utf8Encode(@intCast(32 + viewport_point.y + 1), data[i..]); - - // Ask our IO thread to write the data - self.queueIo(.{ .write_small = .{ - .data = data, - .len = @intCast(i), - } }, .locked); - }, - - .sgr => { - // Final character to send in the CSI - const final: u8 = if (action == .release) 'm' else 'M'; - - // Response always is at least 4 chars, so this leaves the - // remainder for numbers which are very large... - var data: termio.Message.WriteReq.Small.Array = undefined; - const resp = try std.fmt.bufPrint(&data, "\x1B[<{d};{d};{d}{c}", .{ - button_code, - viewport_point.x + 1, - viewport_point.y + 1, - final, - }); - - // Ask our IO thread to write the data - self.queueIo(.{ .write_small = .{ - .data = data, - .len = @intCast(resp.len), - } }, .locked); + var data: termio.Message.WriteReq.Small.Array = undefined; + var writer: std.Io.Writer = .fixed(&data); + input.mouse_encode.encode(&writer, .{ + .button = button, + .action = action, + .mods = mods, + .pos = .{ + .x = pos.x, + .y = pos.y, }, - - .urxvt => { - // Response always is at least 4 chars, so this leaves the - // remainder for numbers which are very large... - var data: termio.Message.WriteReq.Small.Array = undefined; - const resp = try std.fmt.bufPrint(&data, "\x1B[{d};{d};{d}M", .{ - 32 + button_code, - viewport_point.x + 1, - viewport_point.y + 1, - }); - - // Ask our IO thread to write the data - self.queueIo(.{ .write_small = .{ - .data = data, - .len = @intCast(resp.len), - } }, .locked); + }, encoding_opts) catch |err| switch (err) { + error.WriteFailed => { + // This should never happen since mouse events should never + // be able to overflow the size of our small array. But if it + // does, let's log it and return. No need to crash upstreams. + // In the future we may want to fall back to allocation. + log.warn("failed to encode mouse event err={}", .{err}); + return; }, + }; + const written = writer.buffered(); + if (written.len == 0) return; - .sgr_pixels => { - // Final character to send in the CSI - const final: u8 = if (action == .release) 'm' else 'M'; - - // The position has to be adjusted to the terminal space. - const coord: rendererpkg.Coordinate.Terminal = (rendererpkg.Coordinate{ - .surface = .{ - .x = pos.x, - .y = pos.y, - }, - }).convert(.terminal, self.size).terminal; - - // Response always is at least 4 chars, so this leaves the - // remainder for numbers which are very large... - var data: termio.Message.WriteReq.Small.Array = undefined; - const resp = try std.fmt.bufPrint(&data, "\x1B[<{d};{d};{d}{c}", .{ - button_code, - @as(i32, @intFromFloat(@round(coord.x))), - @as(i32, @intFromFloat(@round(coord.y))), - final, - }); - - // Ask our IO thread to write the data - self.queueIo(.{ .write_small = .{ - .data = data, - .len = @intCast(resp.len), - } }, .locked); - }, - } + self.queueIo(.{ .write_small = .{ + .data = data, + .len = @intCast(written.len), + } }, .locked); } /// Returns true if the shift modifier is allowed to be captured by modifier @@ -3994,12 +3887,12 @@ pub fn mouseButtonCallback( const pos = try self.rt_surface.getCursorPos(); - const report_action: MouseReportAction = switch (action) { + const report_action: input.MouseAction = switch (action) { .press => .press, .release => .release, }; - try self.mouseReport( + self.mouseReport( button, report_action, self.mouse.mods, @@ -4267,6 +4160,9 @@ fn maybePromptClick(self: *Surface) !bool { // do anything. if (screen.semantic_prompt.click == .none) return false; + // If cursor-click-to-move is disabled, we don't do any prompt clicking. + if (!self.config.cursor_click_to_move) return false; + // If our cursor isn't currently at a prompt then we don't handle // prompt clicks because we can't move if we're not in a prompt! if (!t.cursorIsAtPrompt()) return false; @@ -4728,7 +4624,7 @@ pub fn cursorPosCallback( break :button @enumFromInt(i); } else null; - try self.mouseReport(button, .motion, self.mouse.mods, pos); + self.mouseReport(button, .motion, self.mouse.mods, pos); // If we're doing mouse motion tracking, we do not support text // selection. @@ -5063,7 +4959,7 @@ pub fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Coordin /// /// Precondition: the render_state mutex must be held. fn scrollToBottom(self: *Surface) !void { - try self.io.terminal.scrollViewport(.{ .bottom = {} }); + self.io.terminal.scrollViewport(.{ .bottom = {} }); try self.queueRender(); } @@ -5470,6 +5366,26 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .tab, ), + .set_surface_title => |v| { + const title = try self.alloc.dupeZ(u8, v); + defer self.alloc.free(title); + return try self.rt_app.performAction( + .{ .surface = self }, + .set_title, + .{ .title = title }, + ); + }, + + .set_tab_title => |v| { + const title = try self.alloc.dupeZ(u8, v); + defer self.alloc.free(title); + return try self.rt_app.performAction( + .{ .surface = self }, + .set_tab_title, + .{ .title = title }, + ); + }, + .clear_screen => { // This is a duplicate of some of the logic in termio.clearScreen // but we need to do this here so we can know the answer before diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 1d9ef633c7d..f6865af83dc 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -7,6 +7,7 @@ const input = @import("../input.zig"); const renderer = @import("../renderer.zig"); const terminal = @import("../terminal/main.zig"); const CoreSurface = @import("../Surface.zig"); +const lib = @import("../lib/main.zig"); /// The target for an action. This is generally the thing that had focus /// while the action was made but the concept of "focus" is not guaranteed @@ -19,6 +20,10 @@ pub const Target = union(Key) { pub const Key = enum(c_int) { app, surface, + + test "ghostty.h Target.Key" { + try lib.checkGhosttyHEnum(Key, "GHOSTTY_TARGET_"); + } }; // Sync with: ghostty_target_u @@ -109,7 +114,7 @@ pub const Action = union(Key) { /// Toggle the quick terminal in or out. toggle_quick_terminal, - /// Toggle the command palette. This currently only works on macOS. + /// Toggle the command palette. toggle_command_palette, /// Toggle the visibility of all Ghostty terminal windows. @@ -196,6 +201,9 @@ pub const Action = union(Key) { /// Set the title of the target to the requested value. set_title: SetTitle, + /// Set the tab title override for the target's tab. + set_tab_title: SetTitle, + /// Set the title of the target to a prompted value. It is up to /// the apprt to prompt. The value specifies whether to prompt for the /// surface title or the tab title. @@ -370,6 +378,7 @@ pub const Action = union(Key) { render_inspector, desktop_notification, set_title, + set_tab_title, prompt_title, pwd, mouse_shape, @@ -401,6 +410,10 @@ pub const Action = union(Key) { search_selected, readonly, copy_title_to_clipboard, + + test "ghostty.h Action.Key" { + try lib.checkGhosttyHEnum(Key, "GHOSTTY_ACTION_"); + } }; /// Sync with: ghostty_action_u @@ -482,6 +495,10 @@ pub const SplitDirection = enum(c_int) { down, left, up, + + test "ghostty.h SplitDirection" { + try lib.checkGhosttyHEnum(SplitDirection, "GHOSTTY_SPLIT_DIRECTION_"); + } }; // This is made extern (c_int) to make interop easier with our embedded @@ -494,6 +511,10 @@ pub const GotoSplit = enum(c_int) { left, down, right, + + test "ghostty.h GotoSplit" { + try lib.checkGhosttyHEnum(GotoSplit, "GHOSTTY_GOTO_SPLIT_"); + } }; // This is made extern (c_int) to make interop easier with our embedded @@ -501,6 +522,10 @@ pub const GotoSplit = enum(c_int) { pub const GotoWindow = enum(c_int) { previous, next, + + test "ghostty.h GotoWindow" { + try lib.checkGhosttyHEnum(GotoWindow, "GHOSTTY_GOTO_WINDOW_"); + } }; /// The amount to resize the split by and the direction to resize it in. @@ -513,6 +538,10 @@ pub const ResizeSplit = extern struct { down, left, right, + + test "ghostty.h ResizeSplit.Direction" { + try lib.checkGhosttyHEnum(Direction, "GHOSTTY_RESIZE_SPLIT_"); + } }; }; @@ -528,6 +557,11 @@ pub const GotoTab = enum(c_int) { next = -2, last = -3, _, + + // TODO: check non-exhaustive enums + // test "ghostty.h GotoTab" { + // try lib.checkGhosttyHEnum(GotoTab, "GHOSTTY_GOTO_TAB_"); + // } }; /// The fullscreen mode to toggle to if we're moving to fullscreen. @@ -539,18 +573,30 @@ pub const Fullscreen = enum(c_int) { macos_non_native, macos_non_native_visible_menu, macos_non_native_padded_notch, + + test "ghostty.h Fullscreen" { + try lib.checkGhosttyHEnum(Fullscreen, "GHOSTTY_FULLSCREEN_"); + } }; pub const FloatWindow = enum(c_int) { on, off, toggle, + + test "ghostty.h FloatWindow" { + try lib.checkGhosttyHEnum(FloatWindow, "GHOSTTY_FLOAT_WINDOW_"); + } }; pub const SecureInput = enum(c_int) { on, off, toggle, + + test "ghostty.h SecureInput" { + try lib.checkGhosttyHEnum(SecureInput, "GHOSTTY_SECURE_INPUT_"); + } }; /// The inspector mode to toggle to if we're toggling the inspector. @@ -558,27 +604,47 @@ pub const Inspector = enum(c_int) { toggle, show, hide, + + test "ghostty.h Inspector" { + try lib.checkGhosttyHEnum(Inspector, "GHOSTTY_INSPECTOR_"); + } }; pub const QuitTimer = enum(c_int) { start, stop, + + test "ghostty.h QuitTimer" { + try lib.checkGhosttyHEnum(QuitTimer, "GHOSTTY_QUIT_TIMER_"); + } }; pub const Readonly = enum(c_int) { off, on, + + test "ghostty.h Readonly" { + try lib.checkGhosttyHEnum(Readonly, "GHOSTTY_READONLY_"); + } }; pub const MouseVisibility = enum(c_int) { visible, hidden, + + test "ghostty.h MouseVisibility" { + try lib.checkGhosttyHEnum(MouseVisibility, "GHOSTTY_MOUSE_"); + } }; /// Whether to prompt for the surface title or tab title. pub const PromptTitle = enum(c_int) { surface, tab, + + test "ghostty.h PromptTitle" { + try lib.checkGhosttyHEnum(PromptTitle, "GHOSTTY_PROMPT_TITLE_"); + } }; pub const MouseOverLink = struct { @@ -782,6 +848,11 @@ pub const ColorKind = enum(c_int) { // 0+ values indicate a palette index _, + + // TODO: check non-non-exhaustive enums + // test "ghostty.h ColorKind" { + // try lib.checkGhosttyHEnum(ColorKind, "GHOSTTY_COLOR_KIND_"); + // } }; pub const ReloadConfig = extern struct { @@ -832,6 +903,10 @@ pub const OpenUrl = struct { /// The URL is known to contain HTML content. html, + + test "ghostty.h OpenUrl.Kind" { + try lib.checkGhosttyHEnum(Kind, "GHOSTTY_ACTION_OPEN_URL_KIND_"); + } }; // Sync with: ghostty_action_open_url_s @@ -858,6 +933,10 @@ pub const CloseTabMode = enum(c_int) { other, /// Close all tabs to the right of the current tab. right, + + test "ghostty.h CloseTabMode" { + try lib.checkGhosttyHEnum(CloseTabMode, "GHOSTTY_ACTION_CLOSE_TAB_MODE_"); + } }; pub const CommandFinished = struct { @@ -922,3 +1001,7 @@ pub const SearchSelected = struct { }; } }; + +test { + _ = std.testing.refAllDeclsRecursive(@This()); +} diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index dcf8a635790..44743c43f10 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -50,10 +50,11 @@ pub const App = struct { /// Callback called to handle an action. action: *const fn (*App, apprt.Target.C, apprt.Action.C) callconv(.c) bool, - /// Read the clipboard value. The return value must be preserved - /// by the host until the next call. If there is no valid clipboard - /// value then this should return null. - read_clipboard: *const fn (SurfaceUD, c_int, *apprt.ClipboardRequest) callconv(.c) void, + /// Read the clipboard value. Returns true if the clipboard request + /// was started and complete_clipboard_request may be called with the + /// given state pointer. Returns false if the clipboard request couldn't + /// be started (such as when no text is available for a paste request). + read_clipboard: *const fn (SurfaceUD, c_int, *apprt.ClipboardRequest) callconv(.c) bool, /// This may be called after a read clipboard call to request /// confirmation that the clipboard value is safe to read. The embedder @@ -512,7 +513,15 @@ pub const Surface = struct { break :wd; } - config.@"working-directory" = wd; + var wd_val: configpkg.WorkingDirectory = .{ .path = wd }; + if (wd_val.finalize(config.arenaAlloc())) |_| { + config.@"working-directory" = wd_val; + } else |err| { + log.warn( + "error finalizing working directory config dir={s} err={}", + .{ wd_val.path, err }, + ); + } } } @@ -672,14 +681,16 @@ pub const Surface = struct { errdefer alloc.destroy(state_ptr); state_ptr.* = state; - self.app.opts.read_clipboard( + const started = self.app.opts.read_clipboard( self.userdata, @intCast(@intFromEnum(clipboard_type)), state_ptr, ); + if (!started) { + alloc.destroy(state_ptr); + return false; + } - // Embedded apprt can't synchronously check clipboard content types, - // so we always return true to indicate the request was started. return true; } @@ -781,6 +792,11 @@ pub const Surface = struct { } pub fn updateSize(self: *Surface, width: u32, height: u32) void { + // A 0-sized surface can't be rendered and is commonly produced transiently + // by UI/layout systems during split/resize operations. Treat it as a no-op + // so we keep the last valid size/content until a real size arrives. + if (width == 0 or height == 0) return; + // Runtimes sometimes generate superfluous resize events even // if the size did not actually change (SwiftUI). We check // that the size actually changed from what we last recorded @@ -848,7 +864,7 @@ pub const Surface = struct { mods: input.Mods, ) void { // Convert our unscaled x/y to scaled. - self.cursor_pos = self.cursorPosToPixels(.{ + const pos = self.cursorPosToPixels(.{ .x = @floatCast(x), .y = @floatCast(y), }) catch |err| { @@ -859,6 +875,19 @@ pub const Surface = struct { return; }; + // There are cases where the platform reports a mouse motion event + // without the cursor actually moving. For example, on macOS, updating + // the window title can trigger a phantom mouse-move event at the same + // coordinates. This can cause the mouse to incorrectly unhide when + // mouse-hide-while-typing is enabled (commonly seen with TUI apps + // like Zellij that frequently update the title). To prevent incorrect + // behavior, we only continue with callback logic if the cursor has + // actually moved. + if (@abs(self.cursor_pos.x - pos.x) < 1 and + @abs(self.cursor_pos.y - pos.y) < 1) return; + + self.cursor_pos = pos; + self.core_surface.cursorPosCallback(self.cursor_pos, mods) catch |err| { log.err("error in cursor pos callback err={}", .{err}); return; @@ -1579,6 +1608,22 @@ pub const CAPI = struct { return surface.core_surface.hasSelection(); } + /// Start a selection at the active cursor cell. + export fn ghostty_surface_select_cursor_cell(surface: *Surface) bool { + return surface.core_surface.selectCursorCell() catch |err| { + log.warn("error selecting cursor cell err={}", .{err}); + return false; + }; + } + + /// Clear the active selection. + export fn ghostty_surface_clear_selection(surface: *Surface) bool { + return surface.core_surface.clearSelection() catch |err| { + log.warn("error clearing selection err={}", .{err}); + return false; + }; + } + /// Same as ghostty_surface_read_text but reads from the user selection, /// if any. export fn ghostty_surface_read_selection( diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 918e77146ec..715973671c0 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2,6 +2,7 @@ const Self = @This(); const std = @import("std"); const apprt = @import("../../apprt.zig"); +const configpkg = @import("../../config.zig"); const CoreSurface = @import("../../Surface.zig"); const ApprtApp = @import("App.zig"); const Application = @import("class/application.zig").Application; diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index c24352c180e..039e853aa4f 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -22,6 +22,7 @@ const xev = @import("../../../global.zig").xev; const Binding = @import("../../../input.zig").Binding; const CoreConfig = configpkg.Config; const CoreSurface = @import("../../../Surface.zig"); +const lib = @import("../../../lib/main.zig"); const ext = @import("../ext.zig"); const key = @import("../key.zig"); @@ -709,6 +710,7 @@ pub const Application = extern struct { .app => null, .surface => |v| v, }, + .none, ), .open_config => return Action.openConfig(self), @@ -738,6 +740,7 @@ pub const Application = extern struct { .scrollbar => Action.scrollbar(target, value), .set_title => Action.setTitle(target, value), + .set_tab_title => return Action.setTabTitle(target, value), .show_child_exited => return Action.showChildExited(target, value), @@ -1669,17 +1672,30 @@ pub const Application = extern struct { ) callconv(.c) void { log.debug("received new window action", .{}); - parameter: { + var arena: std.heap.ArenaAllocator = .init(Application.default().allocator()); + defer arena.deinit(); + + const alloc = arena.allocator(); + + var working_directory: ?[:0]const u8 = null; + var title: ?[:0]const u8 = null; + var command: ?configpkg.Command = null; + var args: std.ArrayList([:0]const u8) = .empty; + + overrides: { // were we given a parameter? - const parameter = parameter_ orelse break :parameter; + const parameter = parameter_ orelse break :overrides; const as_variant_type = glib.VariantType.new("as"); defer as_variant_type.free(); // ensure that the supplied parameter is an array of strings if (glib.Variant.isOfType(parameter, as_variant_type) == 0) { - log.warn("parameter is of type {s}", .{parameter.getTypeString()}); - break :parameter; + log.warn("parameter is of type '{s}', not '{s}'", .{ + parameter.getTypeString(), + as_variant_type.peekString()[0..as_variant_type.getStringLength()], + }); + break :overrides; } const s_variant_type = glib.VariantType.new("s"); @@ -1688,7 +1704,10 @@ pub const Application = extern struct { var it: glib.VariantIter = undefined; _ = it.init(parameter); - while (it.nextValue()) |value| { + var e_seen: bool = false; + var i: usize = 0; + + while (it.nextValue()) |value| : (i += 1) { defer value.unref(); // just to be sure @@ -1698,13 +1717,64 @@ pub const Application = extern struct { const buf = value.getString(&len); const str = buf[0..len]; - log.debug("new-window command argument: {s}", .{str}); + log.debug("new-window argument: {d} {s}", .{ i, str }); + + if (e_seen) { + const cpy = alloc.dupeZ(u8, str) catch |err| { + log.warn("unable to duplicate argument {d} {s}: {t}", .{ i, str, err }); + break :overrides; + }; + args.append(alloc, cpy) catch |err| { + log.warn("unable to append argument {d} {s}: {t}", .{ i, str, err }); + break :overrides; + }; + continue; + } + + if (std.mem.eql(u8, str, "-e")) { + e_seen = true; + continue; + } + + if (lib.cutPrefix(u8, str, "--command=")) |v| { + var cmd: configpkg.Command = undefined; + cmd.parseCLI(alloc, v) catch |err| { + log.warn("unable to parse command: {t}", .{err}); + continue; + }; + command = cmd; + continue; + } + if (lib.cutPrefix(u8, str, "--working-directory=")) |v| { + working_directory = alloc.dupeZ(u8, std.mem.trim(u8, v, &std.ascii.whitespace)) catch |err| wd: { + log.warn("unable to duplicate working directory: {t}", .{err}); + break :wd null; + }; + continue; + } + if (lib.cutPrefix(u8, str, "--title=")) |v| { + title = alloc.dupeZ(u8, std.mem.trim(u8, v, &std.ascii.whitespace)) catch |err| t: { + log.warn("unable to duplicate title: {t}", .{err}); + break :t null; + }; + continue; + } } } - _ = self.core().mailbox.push(.{ - .new_window = .{}, - }, .{ .forever = {} }); + if (args.items.len > 0) { + command = .{ + .direct = args.items, + }; + } + + Action.newWindow(self, null, .{ + .command = command, + .working_directory = working_directory, + .title = title, + }) catch |err| { + log.warn("unable to create new window: {t}", .{err}); + }; } pub fn actionOpenConfig( @@ -2151,6 +2221,13 @@ const Action = struct { pub fn newWindow( self: *Application, parent: ?*CoreSurface, + overrides: struct { + command: ?configpkg.Command = null, + working_directory: ?[:0]const u8 = null, + title: ?[:0]const u8 = null, + + pub const none: @This() = .{}; + }, ) !void { // Note that we've requested a window at least once. This is used // to trigger quit on no windows. Note I'm not sure if this is REALLY @@ -2159,14 +2236,32 @@ const Action = struct { // was a delay in the event loop before we created a Window. self.private().requested_window = true; - const win = Window.new(self); - initAndShowWindow(self, win, parent); + const win = Window.new(self, .{ + .title = overrides.title, + }); + initAndShowWindow( + self, + win, + parent, + .{ + .command = overrides.command, + .working_directory = overrides.working_directory, + .title = overrides.title, + }, + ); } fn initAndShowWindow( self: *Application, win: *Window, parent: ?*CoreSurface, + overrides: struct { + command: ?configpkg.Command = null, + working_directory: ?[:0]const u8 = null, + title: ?[:0]const u8 = null, + + pub const none: @This() = .{}; + }, ) void { // Setup a binding so that whenever our config changes so does the // window. There's never a time when the window config should be out @@ -2180,7 +2275,23 @@ const Action = struct { ); // Create a new tab with window context (first tab in new window) - win.newTabForWindow(parent); + win.newTabForWindow(parent, .{ + .command = overrides.command, + .working_directory = overrides.working_directory, + .title = overrides.title, + }); + + // Estimate the initial window size before presenting so the window + // manager can position it correctly. + if (win.getActiveSurface()) |surface| { + surface.estimateInitialSize(); + if (surface.getDefaultSize()) |size| { + win.as(gtk.Window).setDefaultSize( + @intCast(size.width), + @intCast(size.height), + ); + } + } // Show the window gtk.Window.present(win.as(gtk.Window)); @@ -2435,6 +2546,30 @@ const Action = struct { } } + pub fn setTabTitle( + target: apprt.Target, + value: apprt.action.SetTitle, + ) bool { + switch (target) { + .app => { + log.warn("set_tab_title to app is unexpected", .{}); + return false; + }, + .surface => |core| { + const surface = core.rt_surface.surface; + const tab = ext.getAncestor( + Tab, + surface.as(gtk.Widget), + ) orelse { + log.warn("surface is not in a tab, ignoring set_tab_title", .{}); + return false; + }; + tab.setTitleOverride(if (value.title.len == 0) null else value.title); + return true; + }, + } + } + pub fn showChildExited( target: apprt.Target, value: apprt.surface.Message.ChildExited, @@ -2494,7 +2629,7 @@ const Action = struct { .@"quick-terminal" = true, }); assert(win.isQuickTerminal()); - initAndShowWindow(self, win, null); + initAndShowWindow(self, win, null, .none); return true; } diff --git a/src/apprt/gtk/class/command_palette.zig b/src/apprt/gtk/class/command_palette.zig index 0d91c43b273..9c79f2712d8 100644 --- a/src/apprt/gtk/class/command_palette.zig +++ b/src/apprt/gtk/class/command_palette.zig @@ -691,12 +691,12 @@ const Command = extern struct { defer surface.unref(); const alloc = priv.arena.allocator(); - const surface_title = surface.getTitle() orelse "Untitled"; + const effective_title = surface.getEffectiveTitle() orelse "Untitled"; j.title = std.fmt.allocPrintSentinel( alloc, "Focus: {s}", - .{surface_title}, + .{effective_title}, 0, ) catch null; @@ -717,8 +717,7 @@ const Command = extern struct { defer surface.unref(); const alloc = priv.arena.allocator(); - - const title = surface.getTitle() orelse "Untitled"; + const title = surface.getEffectiveTitle() orelse "Untitled"; const pwd = surface.getPwd(); if (pwd) |p| { diff --git a/src/apprt/gtk/class/imgui_widget.zig b/src/apprt/gtk/class/imgui_widget.zig index 01b3f3e5c52..0ef753a87d5 100644 --- a/src/apprt/gtk/class/imgui_widget.zig +++ b/src/apprt/gtk/class/imgui_widget.zig @@ -257,8 +257,18 @@ pub const ImguiWidget = extern struct { priv.tick_callback_id = 0; } + // Unrealize is not guaranteed to be called with a current GL context, + // so we make it current for ImGui cleanup. + priv.gl_area.makeCurrent(); + if (priv.gl_area.getError()) |err| { + log.warn("GLArea for Dear ImGui widget failed to realize: {s}", .{err.f_message orelse "(unknown)"}); + return; + } + self.setCurrentContext() catch return; - cimgui.ImGui_ImplOpenGL3_Shutdown(); + cimgui.ImGui_ImplOpenGL3_ShutdownWithLoaderCleanup(); + cimgui.c.ImGui_DestroyContext(priv.ig_context); + priv.ig_context = null; } /// Handle a request to resize the GLArea diff --git a/src/apprt/gtk/class/split_tree.zig b/src/apprt/gtk/class/split_tree.zig index 0ff7e60441d..311fbd8a6cf 100644 --- a/src/apprt/gtk/class/split_tree.zig +++ b/src/apprt/gtk/class/split_tree.zig @@ -7,6 +7,7 @@ const glib = @import("glib"); const gobject = @import("gobject"); const gtk = @import("gtk"); +const configpkg = @import("../../../config.zig"); const apprt = @import("../../../apprt.zig"); const ext = @import("../ext.zig"); const gresource = @import("../build/gresource.zig"); @@ -157,11 +158,6 @@ pub const SplitTree = extern struct { /// used to debounce updates. rebuild_source: ?c_uint = null, - /// Tracks whether we want a rebuild to happen at the next tick - /// that our surface tree has no surfaces with parents. See the - /// propTree function for a lot more details. - rebuild_pending: bool, - /// Used to store state about a pending surface close for the /// close dialog. pending_close: ?Surface.Tree.Node.Handle, @@ -208,11 +204,22 @@ pub const SplitTree = extern struct { self: *Self, direction: Surface.Tree.Split.Direction, parent_: ?*Surface, + overrides: struct { + command: ?configpkg.Command = null, + working_directory: ?[:0]const u8 = null, + title: ?[:0]const u8 = null, + + pub const none: @This() = .{}; + }, ) Allocator.Error!void { const alloc = Application.default().allocator(); // Create our new surface. - const surface: *Surface = .new(); + const surface: *Surface = .new(.{ + .command = overrides.command, + .working_directory = overrides.working_directory, + .title = overrides.title, + }); defer surface.unref(); _ = surface.refSink(); @@ -408,13 +415,6 @@ pub const SplitTree = extern struct { self, .{ .detail = "focused" }, ); - _ = gobject.Object.signals.notify.connect( - surface.as(gtk.Widget), - *Self, - propSurfaceParent, - self, - .{ .detail = "parent" }, - ); } } @@ -478,20 +478,6 @@ pub const SplitTree = extern struct { return surface; } - /// Returns whether any of the surfaces in the tree have a parent. - /// This is important because we can only rebuild the widget tree - /// when every surface has no parent. - fn getTreeHasParents(self: *Self) bool { - const tree: *const Surface.Tree = self.getTree() orelse &.empty; - var it = tree.iterator(); - while (it.next()) |entry| { - const surface = entry.view; - if (surface.as(gtk.Widget).getParent() != null) return true; - } - - return false; - } - pub fn getHasSurfaces(self: *Self) bool { const tree: *const Surface.Tree = self.private().tree orelse &.empty; return !tree.isEmpty(); @@ -638,6 +624,7 @@ pub const SplitTree = extern struct { self.newSplit( direction, self.getActiveSurface(), + .none, ) catch |err| { log.warn("new split failed error={}", .{err}); }; @@ -779,27 +766,6 @@ pub const SplitTree = extern struct { self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec); } - fn propSurfaceParent( - _: *gtk.Widget, - _: *gobject.ParamSpec, - self: *Self, - ) callconv(.c) void { - const priv = self.private(); - - // If we're not waiting to rebuild then ignore this. - if (!priv.rebuild_pending) return; - - // If any parents still exist in our tree then don't do anything. - if (self.getTreeHasParents()) return; - - // Schedule the rebuild. Note, I tried to do this immediately (not - // on an idle tick) and it didn't work and had obvious rendering - // glitches. Something to look into in the future. - assert(priv.rebuild_source == null); - priv.rebuild_pending = false; - priv.rebuild_source = glib.idleAdd(onRebuild, self); - } - fn propTree( self: *Self, _: *gobject.ParamSpec, @@ -807,6 +773,12 @@ pub const SplitTree = extern struct { ) callconv(.c) void { const priv = self.private(); + // No matter what we notify + self.as(gobject.Object).freezeNotify(); + defer self.as(gobject.Object).thawNotify(); + self.as(gobject.Object).notifyByPspec(properties.@"has-surfaces".impl.param_spec); + self.as(gobject.Object).notifyByPspec(properties.@"is-zoomed".impl.param_spec); + // If we were planning a rebuild, always remove that so we can // start from a clean slate. if (priv.rebuild_source) |v| { @@ -816,38 +788,22 @@ pub const SplitTree = extern struct { priv.rebuild_source = null; } - // We need to wait for all our previous surfaces to lose their - // parent before adding them to a new one. I'm not sure if its a GTK - // bug, but manually forcing an unparent of all prior surfaces AND - // adding them to a new parent in the same tick causes the GLArea - // to break (it seems). I didn't investigate too deeply. - // - // Note, we also can't just defer to an idle tick (via idleAdd) because - // sometimes it takes more than one tick for all our surfaces to - // lose their parent. - // - // To work around this issue, if we have any surfaces that have - // a parent, we set the build pending flag and wait for the tree - // to be fully parent-free before building. - priv.rebuild_pending = self.getTreeHasParents(); - - // Reset our prior bin. This will force all prior surfaces to - // unparent... eventually. - priv.tree_bin.setChild(null); - - // If none of the surfaces we plan on drawing require an unparent - // then we can setup our tree immediately. Otherwise, it'll happen - // via the `propSurfaceParent` callback. - if (!priv.rebuild_pending and priv.rebuild_source == null) { - priv.rebuild_source = glib.idleAdd( - onRebuild, - self, - ); + // If we transitioned to an empty tree, clear immediately instead of + // waiting for an idle callback. Delaying teardown can keep the last + // surface alive during shutdown if the main loop exits first. + if (priv.tree == null) { + priv.tree_bin.setChild(null); + return; } - // Dependent properties - self.as(gobject.Object).notifyByPspec(properties.@"has-surfaces".impl.param_spec); - self.as(gobject.Object).notifyByPspec(properties.@"is-zoomed".impl.param_spec); + // Build on an idle callback so rapid tree changes are debounced. + // We keep the existing tree attached until the rebuild runs, + // which avoids transient empty frames. + assert(priv.rebuild_source == null); + priv.rebuild_source = glib.idleAdd( + onRebuild, + self, + ); } fn onRebuild(ud: ?*anyopaque) callconv(.c) c_int { @@ -857,22 +813,21 @@ pub const SplitTree = extern struct { const priv = self.private(); priv.rebuild_source = null; - // Prior to rebuilding the tree, our surface tree must be - // comprised of fully orphaned surfaces. - assert(!self.getTreeHasParents()); - // Rebuild our tree const tree: *const Surface.Tree = self.private().tree orelse &.empty; - if (!tree.isEmpty()) { - priv.tree_bin.setChild(self.buildTree( + if (tree.isEmpty()) { + priv.tree_bin.setChild(null); + } else { + const built = self.buildTree( tree, tree.zoomed orelse .root, - )); + ); + defer built.deinit(); + priv.tree_bin.setChild(built.widget); } - // If we have a last focused surface, we need to refocus it, because - // during the frame between setting the bin to null and rebuilding, - // GTK will reset our focus state (as it should!) + // Replacing our tree widget hierarchy can reset focus state. + // If we have a last-focused surface, restore focus to it. if (priv.last_focused.get()) |v| { defer v.unref(); v.grabFocus(); @@ -889,26 +844,120 @@ pub const SplitTree = extern struct { /// Builds the widget tree associated with a surface split tree. /// - /// The final returned widget is expected to be a floating reference, - /// ready to be attached to a parent widget. + /// Returned widgets are expected to be attached to a parent by the caller. + /// + /// If `release_ref` is true then `widget` has an extra temporary + /// reference that must be released once it is parented in the rebuilt + /// tree. + const BuildTreeResult = struct { + widget: *gtk.Widget, + release_ref: bool, + + pub fn initNew(widget: *gtk.Widget) BuildTreeResult { + return .{ .widget = widget, .release_ref = false }; + } + + pub fn initReused(widget: *gtk.Widget) BuildTreeResult { + // We add a temporary ref to the widget to ensure it doesn't + // get destroyed while we're rebuilding the tree and detaching + // it from its old parent. The caller is expected to release + // this ref once the widget is attached to its new parent. + _ = widget.as(gobject.Object).ref(); + + // Detach after we ref it so that this doesn't mark the + // widget for destruction. + detachWidget(widget); + + return .{ .widget = widget, .release_ref = true }; + } + + pub fn deinit(self: BuildTreeResult) void { + // If we have to release a ref, do it. + if (self.release_ref) self.widget.as(gobject.Object).unref(); + } + }; + fn buildTree( self: *Self, tree: *const Surface.Tree, current: Surface.Tree.Node.Handle, - ) *gtk.Widget { + ) BuildTreeResult { return switch (tree.nodes[current.idx()]) { - .leaf => |v| gobject.ext.newInstance(SurfaceScrolledWindow, .{ - .surface = v, - }).as(gtk.Widget), - .split => |s| SplitTreeSplit.new( - current, - &s, - self.buildTree(tree, s.left), - self.buildTree(tree, s.right), - ).as(gtk.Widget), + .leaf => |v| leaf: { + const window = ext.getAncestor( + SurfaceScrolledWindow, + v.as(gtk.Widget), + ) orelse { + // The surface isn't in a window already so we don't + // have to worry about reuse. + break :leaf .initNew(gobject.ext.newInstance( + SurfaceScrolledWindow, + .{ .surface = v }, + ).as(gtk.Widget)); + }; + + // Keep this widget alive while we detach it from the + // old tree and adopt it into the new one. + break :leaf .initReused(window.as(gtk.Widget)); + }, + .split => |s| split: { + const left = self.buildTree(tree, s.left); + defer left.deinit(); + const right = self.buildTree(tree, s.right); + defer right.deinit(); + + break :split .initNew(SplitTreeSplit.new( + current, + &s, + left.widget, + right.widget, + ).as(gtk.Widget)); + }, }; } + /// Detach a split widget from its current parent. + /// + /// We intentionally use parent-specific child APIs when possible + /// (`GtkPaned.setStartChild/setEndChild`, `AdwBin.setChild`) instead of + /// calling `gtk.Widget.unparent` directly. Container implementations track + /// child pointers/properties internally, and those setters are the path + /// that keeps container state and notifications in sync. + fn detachWidget(widget: *gtk.Widget) void { + const parent = widget.getParent() orelse return; + + // Surface will be in a paned when it is split. + if (gobject.ext.cast(gtk.Paned, parent)) |paned| { + if (paned.getStartChild()) |child| { + if (child == widget) { + paned.setStartChild(null); + return; + } + } + + if (paned.getEndChild()) |child| { + if (child == widget) { + paned.setEndChild(null); + return; + } + } + } + + // Surface will be in a bin when it is not split. + if (gobject.ext.cast(adw.Bin, parent)) |bin| { + if (bin.getChild()) |child| { + if (child == widget) { + bin.setChild(null); + return; + } + } + } + + // Fallback for unexpected parents where we don't have a typed + // container API available. + widget.unparent(); + } + //--------------------------------------------------------------- // Class diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 7627470a587..4b31c43d5e1 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -10,6 +10,7 @@ const gtk = @import("gtk"); const apprt = @import("../../../apprt.zig"); const build_config = @import("../../../build_config.zig"); +const configpkg = @import("../../../config.zig"); const datastruct = @import("../../../datastruct/main.zig"); const font = @import("../../../font/main.zig"); const input = @import("../../../input.zig"); @@ -693,6 +694,10 @@ pub const Surface = extern struct { /// Whether primary paste (middle-click paste) is enabled. gtk_enable_primary_paste: bool = true, + /// True when a left mouse down was consumed purely for a focus change, + /// and the matching left mouse release should also be suppressed. + suppress_left_mouse_release: bool = false, + /// How much pending horizontal scroll do we have? pending_horizontal_scroll: f64 = 0.0, @@ -700,11 +705,33 @@ pub const Surface = extern struct { /// stops scrolling. pending_horizontal_scroll_reset: ?c_uint = null, + overrides: struct { + command: ?configpkg.Command = null, + working_directory: ?[:0]const u8 = null, + + pub const none: @This() = .{}; + } = .none, + pub var offset: c_int = 0; }; - pub fn new() *Self { - return gobject.ext.newInstance(Self, .{}); + pub fn new(overrides: struct { + command: ?configpkg.Command = null, + working_directory: ?[:0]const u8 = null, + title: ?[:0]const u8 = null, + + pub const none: @This() = .{}; + }) *Self { + const self = gobject.ext.newInstance(Self, .{ + .@"title-override" = overrides.title, + }); + const alloc = Application.default().allocator(); + const priv: *Private = self.private(); + priv.overrides = .{ + .command = if (overrides.command) |c| c.clone(alloc) catch null else null, + .working_directory = if (overrides.working_directory) |wd| alloc.dupeZ(u8, wd) catch null else null, + }; + return self; } pub fn core(self: *Self) ?*CoreSurface { @@ -796,10 +823,11 @@ pub const Surface = extern struct { /// should be applied to the surface fn closureShouldUnfocusedSplitBeShown( _: *Self, + search_active: c_int, focused: c_int, is_split: c_int, ) callconv(.c) c_int { - return @intFromBool(focused == 0 and is_split != 0); + return @intFromBool(search_active == 0 and focused == 0 and is_split != 0); } pub fn toggleFullscreen(self: *Self) void { @@ -985,6 +1013,14 @@ pub const Surface = extern struct { priv.progress_bar_timer = null; } + if (priv.config) |config| { + if (!config.get().@"progress-style") { + log.debug("progress_report action blocked by config", .{}); + priv.progress_bar_overlay.as(gtk.Widget).setVisible(@intFromBool(false)); + return; + } + } + const progress_bar = priv.progress_bar_overlay; switch (value.state) { // Remove the progress bar @@ -1849,6 +1885,7 @@ pub const Surface = extern struct { } fn finalize(self: *Self) callconv(.c) void { + const alloc = Application.default().allocator(); const priv = self.private(); if (priv.core_surface) |v| { // Remove ourselves from the list of known surfaces in the app. @@ -1862,7 +1899,6 @@ pub const Surface = extern struct { // Deinit the surface v.deinit(); - const alloc = Application.default().allocator(); alloc.destroy(v); priv.core_surface = null; @@ -1895,9 +1931,16 @@ pub const Surface = extern struct { glib.free(@ptrCast(@constCast(v))); priv.title_override = null; } + if (priv.overrides.command) |c| { + c.deinit(alloc); + priv.overrides.command = null; + } + if (priv.overrides.working_directory) |wd| { + alloc.free(wd); + priv.overrides.working_directory = null; + } // Clean up key sequence and key table state - const alloc = Application.default().allocator(); for (priv.key_sequence.items) |s| alloc.free(s); priv.key_sequence.deinit(alloc); for (priv.key_tables.items) |s| alloc.free(s); @@ -2005,6 +2048,55 @@ pub const Surface = extern struct { self.as(gobject.Object).notifyByPspec(properties.@"default-size".impl.param_spec); } + /// Estimate and set the initial window size from config and font metrics. + /// This can be called before the core surface exists to set up the window + /// size before presenting. This is an estimate because it does not take + /// into account any padding that may need to be added to the window. + pub fn estimateInitialSize(self: *Self) void { + const priv: *Private = self.private(); + const config_obj = priv.config orelse return; + const config = config_obj.get(); + + // Both dimensions must be configured + if (config.@"window-height" <= 0 or config.@"window-width" <= 0) return; + + const app = Application.default(); + const alloc = app.allocator(); + + // Get content scale and compute DPI + const content_scale = self.getContentScale(); + const x_dpi = content_scale.x * font.face.default_dpi; + const y_dpi = content_scale.y * font.face.default_dpi; + + const font_size: font.face.DesiredSize = .{ + .points = config.@"font-size", + .xdpi = @intFromFloat(x_dpi), + .ydpi = @intFromFloat(y_dpi), + }; + + // Get font grid for cell metrics + var derived_config = font.SharedGridSet.DerivedConfig.init(alloc, config) catch return; + defer derived_config.deinit(); + + const font_grid_key, const font_grid = app.core().font_grid_set.ref( + &derived_config, + font_size, + ) catch return; + defer app.core().font_grid_set.deref(font_grid_key); + + const cell = font_grid.cellSize(); + + const width = @max(CoreSurface.min_window_width_cells, config.@"window-width") * cell.width; + const height = @max(CoreSurface.min_window_height_cells, config.@"window-height") * cell.height; + const width_f32: f32 = @floatFromInt(width); + const height_f32: f32 = @floatFromInt(height); + + const final_width: u32 = @intFromFloat(@ceil(width_f32 / content_scale.x)); + const final_height: u32 = @intFromFloat(@ceil(height_f32 / content_scale.y)); + + self.setDefaultSize(.{ .width = final_width, .height = final_height }); + } + /// Get the key sequence list. Full transfer. fn getKeySequence(self: *Self) ?*ext.StringList { const priv = self.private(); @@ -2684,13 +2776,21 @@ pub const Surface = extern struct { // If we don't have focus, grab it. const gl_area_widget = priv.gl_area.as(gtk.Widget); - if (gl_area_widget.hasFocus() == 0) { + const had_focus = gl_area_widget.hasFocus() != 0; + if (!had_focus) { _ = gl_area_widget.grabFocus(); } // Report the event const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton()); + // If this click is only transitioning split focus, suppress it so + // it doesn't get forwarded to the terminal as a mouse event. + if (!had_focus and button == .left) { + priv.suppress_left_mouse_release = true; + return; + } + if (button == .middle and !priv.gtk_enable_primary_paste) { return; } @@ -2746,6 +2846,11 @@ pub const Surface = extern struct { const gtk_mods = event.getModifierState(); const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton()); + if (button == .left and priv.suppress_left_mouse_release) { + priv.suppress_left_mouse_release = false; + return; + } + if (button == .middle and !priv.gtk_enable_primary_paste) { return; } @@ -3247,7 +3352,7 @@ pub const Surface = extern struct { }; fn initSurface(self: *Self) InitError!void { - const priv = self.private(); + const priv: *Private = self.private(); assert(priv.core_surface == null); const gl_area = priv.gl_area; @@ -3280,9 +3385,24 @@ pub const Surface = extern struct { ); defer config.deinit(); + if (priv.overrides.command) |c| { + config.command = try c.clone(config._arena.?.allocator()); + } + if (priv.overrides.working_directory) |wd| { + const config_alloc = config.arenaAlloc(); + var wd_val: configpkg.WorkingDirectory = .{ .path = try config_alloc.dupe(u8, wd) }; + try wd_val.finalize(config_alloc); + config.@"working-directory" = wd_val; + } + // Properties that can impact surface init if (priv.font_size_request) |size| config.@"font-size" = size.points; - if (priv.pwd) |pwd| config.@"working-directory" = pwd; + if (priv.pwd) |pwd| { + const config_alloc = config.arenaAlloc(); + var wd_val: configpkg.WorkingDirectory = .{ .path = try config_alloc.dupe(u8, pwd) }; + try wd_val.finalize(config_alloc); + config.@"working-directory" = wd_val; + } // Initialize the surface surface.init( diff --git a/src/apprt/gtk/class/tab.zig b/src/apprt/gtk/class/tab.zig index 15e1266426c..0c60c8ccc29 100644 --- a/src/apprt/gtk/class/tab.zig +++ b/src/apprt/gtk/class/tab.zig @@ -5,6 +5,7 @@ const glib = @import("glib"); const gobject = @import("gobject"); const gtk = @import("gtk"); +const configpkg = @import("../../../config.zig"); const apprt = @import("../../../apprt.zig"); const CoreSurface = @import("../../../Surface.zig"); const ext = @import("../ext.zig"); @@ -186,22 +187,34 @@ pub const Tab = extern struct { } } - fn init(self: *Self, _: *Class) callconv(.c) void { - gtk.Widget.initTemplate(self.as(gtk.Widget)); + pub fn new(config: ?*Config, overrides: struct { + command: ?configpkg.Command = null, + working_directory: ?[:0]const u8 = null, + title: ?[:0]const u8 = null, - // Init our actions - self.initActionMap(); + pub const none: @This() = .{}; + }) *Self { + const tab = gobject.ext.newInstance(Tab, .{}); + + const priv: *Private = tab.private(); + + if (config) |c| priv.config = c.ref(); // If our configuration is null then we get the configuration // from the application. - const priv = self.private(); if (priv.config == null) { const app = Application.default(); priv.config = app.getConfig(); } + tab.as(gobject.Object).notifyByPspec(properties.config.impl.param_spec); + // Create our initial surface in the split tree. - priv.split_tree.newSplit(.right, null) catch |err| switch (err) { + priv.split_tree.newSplit(.right, null, .{ + .command = overrides.command, + .working_directory = overrides.working_directory, + .title = overrides.title, + }) catch |err| switch (err) { error.OutOfMemory => { // TODO: We should make our "no surfaces" state more aesthetically // pleasing and show something like an "Oops, something went wrong" @@ -209,6 +222,15 @@ pub const Tab = extern struct { @panic("oom"); }, }; + + return tab; + } + + fn init(self: *Self, _: *Class) callconv(.c) void { + gtk.Widget.initTemplate(self.as(gtk.Widget)); + + // Init our actions + self.initActionMap(); } fn initActionMap(self: *Self) void { @@ -330,6 +352,10 @@ pub const Tab = extern struct { glib.free(@ptrCast(@constCast(v))); priv.title = null; } + if (priv.title_override) |v| { + glib.free(@ptrCast(@constCast(v))); + priv.title_override = null; + } gobject.Object.virtual_methods.finalize.call( Class.parent, diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index f96bccd642e..c01cad618d8 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -266,10 +266,27 @@ pub const Window = extern struct { pub var offset: c_int = 0; }; - pub fn new(app: *Application) *Self { - return gobject.ext.newInstance(Self, .{ + pub fn new( + app: *Application, + overrides: struct { + title: ?[:0]const u8 = null, + + pub const none: @This() = .{}; + }, + ) *Self { + const win = gobject.ext.newInstance(Self, .{ .application = app, }); + + if (overrides.title) |title| { + // If the overrides have a title set, we set that immediately + // so that any applications inspecting the window states see an + // immediate title set when the window appears, rather than waiting + // possibly a few event loop ticks for it to sync from the surface. + win.as(gtk.Window).setTitle(title); + } + + return win; } fn init(self: *Self, _: *Class) callconv(.c) void { @@ -278,10 +295,14 @@ pub const Window = extern struct { // If our configuration is null then we get the configuration // from the application. const priv = self.private(); - if (priv.config == null) { + + const config = config: { + if (priv.config) |config| break :config config.get(); const app = Application.default(); - priv.config = app.getConfig(); - } + const config = app.getConfig(); + priv.config = config; + break :config config.get(); + }; // We initialize our windowing protocol to none because we can't // actually initialize this until we get realized. @@ -305,17 +326,16 @@ pub const Window = extern struct { self.initActionMap(); // Start states based on config. - if (priv.config) |config_obj| { - const config = config_obj.get(); - if (config.maximize) self.as(gtk.Window).maximize(); - if (config.fullscreen) self.as(gtk.Window).fullscreen(); - - // If we have an explicit title set, we set that immediately - // so that any applications inspecting the window states see - // an immediate title set when the window appears, rather than - // waiting possibly a few event loop ticks for it to sync from - // the surface. - if (config.title) |v| self.as(gtk.Window).setTitle(v); + if (config.maximize) self.as(gtk.Window).maximize(); + if (config.fullscreen != .false) self.as(gtk.Window).fullscreen(); + + // If we have an explicit title set, we set that immediately + // so that any applications inspecting the window states see + // an immediate title set when the window appears, rather than + // waiting possibly a few event loop ticks for it to sync from + // the surface. + if (config.title) |title| { + self.as(gtk.Window).setTitle(title); } // We always sync our appearance at the end because loading our @@ -339,6 +359,7 @@ pub const Window = extern struct { .init("close-tab", actionCloseTab, s_variant_type), .init("new-tab", actionNewTab, null), .init("new-window", actionNewWindow, null), + .init("prompt-surface-title", actionPromptSurfaceTitle, null), .init("prompt-tab-title", actionPromptTabTitle, null), .init("prompt-context-tab-title", actionPromptContextTabTitle, null), .init("ring-bell", actionRingBell, null), @@ -367,22 +388,61 @@ pub const Window = extern struct { /// at the position dictated by the `window-new-tab-position` config. /// The new tab will be selected. pub fn newTab(self: *Self, parent_: ?*CoreSurface) void { - _ = self.newTabPage(parent_, .tab); + _ = self.newTabPage(parent_, .tab, .none); } - pub fn newTabForWindow(self: *Self, parent_: ?*CoreSurface) void { - _ = self.newTabPage(parent_, .window); + pub fn newTabForWindow( + self: *Self, + parent_: ?*CoreSurface, + overrides: struct { + command: ?configpkg.Command = null, + working_directory: ?[:0]const u8 = null, + title: ?[:0]const u8 = null, + + pub const none: @This() = .{}; + }, + ) void { + _ = self.newTabPage( + parent_, + .window, + .{ + .command = overrides.command, + .working_directory = overrides.working_directory, + .title = overrides.title, + }, + ); } - fn newTabPage(self: *Self, parent_: ?*CoreSurface, context: apprt.surface.NewSurfaceContext) *adw.TabPage { - const priv = self.private(); + fn newTabPage( + self: *Self, + parent_: ?*CoreSurface, + context: apprt.surface.NewSurfaceContext, + overrides: struct { + command: ?configpkg.Command = null, + working_directory: ?[:0]const u8 = null, + title: ?[:0]const u8 = null, + + pub const none: @This() = .{}; + }, + ) *adw.TabPage { + const priv: *Private = self.private(); const tab_view = priv.tab_view; // Create our new tab object - const tab = gobject.ext.newInstance(Tab, .{ - .config = priv.config, - }); + const tab = Tab.new( + priv.config, + .{ + .command = overrides.command, + .working_directory = overrides.working_directory, + .title = overrides.title, + }, + ); + if (parent_) |p| { + // For a new window's first tab, inherit the parent's initial size hints. + if (context == .window) { + surfaceInit(p.rt_surface.gobj(), self); + } tab.setParentWithContext(p, context); } @@ -1248,7 +1308,7 @@ pub const Window = extern struct { _: *adw.TabOverview, self: *Self, ) callconv(.c) *adw.TabPage { - return self.newTabPage(if (self.getActiveSurface()) |v| v.core() else null, .tab); + return self.newTabPage(if (self.getActiveSurface()) |v| v.core() else null, .tab, .none); } fn tabOverviewOpen( @@ -1799,6 +1859,14 @@ pub const Window = extern struct { tab.promptTabTitle(); } + fn actionPromptSurfaceTitle( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *Window, + ) callconv(.c) void { + self.performBindingAction(.prompt_surface_title); + } + fn actionPromptTabTitle( _: *gio.SimpleAction, _: ?*glib.Variant, diff --git a/src/apprt/gtk/ipc/new_window.zig b/src/apprt/gtk/ipc/new_window.zig index 19c46e3aaa7..02fed3229ab 100644 --- a/src/apprt/gtk/ipc/new_window.zig +++ b/src/apprt/gtk/ipc/new_window.zig @@ -18,7 +18,7 @@ const DBus = @import("DBus.zig"); // `ghostty +new-window -e echo hello` would be equivalent to the following command (on a release build): // // ``` -// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["echo" "hello"]>]' [] +// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["-e" "echo" "hello"]>]' [] // ``` 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( @@ -32,10 +32,10 @@ pub fn newWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Ac defer dbus.deinit(alloc); if (value.arguments) |arguments| { - // If `-e` was specified on the command line, the first - // parameter is an array of strings that contain the arguments - // that came after `-e`, which will be interpreted as a command - // to run. + // If any arguments were specified on the command line, the first + // parameter is an array of strings that contain the arguments. They + // will be sent to the main Ghostty instance and interpreted as CLI + // arguments. const as_variant_type = glib.VariantType.new("as"); defer as_variant_type.free(); diff --git a/src/apprt/gtk/ui/1.2/surface.blp b/src/apprt/gtk/ui/1.2/surface.blp index d8483285ff0..794ea1801d9 100644 --- a/src/apprt/gtk/ui/1.2/surface.blp +++ b/src/apprt/gtk/ui/1.2/surface.blp @@ -203,7 +203,7 @@ Overlay terminal_page { // Apply unfocused-split-fill and unfocused-split-opacity to current surface // this is only applied when a tab has more than one surface Revealer { - reveal-child: bind $should_unfocused_split_be_shown(template.focused, template.is-split) as ; + reveal-child: bind $should_unfocused_split_be_shown(search_overlay.active, template.focused, template.is-split) as ; transition-duration: 0; // This is all necessary so that the Revealer itself doesn't override // any input events from the other overlays. Namely, if you don't have @@ -221,7 +221,7 @@ Overlay terminal_page { DropTarget drop_target { drop => $drop(); - actions: copy; + actions: copy | move; } } diff --git a/src/apprt/gtk/ui/1.5/window.blp b/src/apprt/gtk/ui/1.5/window.blp index a139f8cc5c6..b66a9309362 100644 --- a/src/apprt/gtk/ui/1.5/window.blp +++ b/src/apprt/gtk/ui/1.5/window.blp @@ -243,7 +243,7 @@ menu main_menu { item { label: _("Change Title…"); - action: "win.prompt-title"; + action: "win.prompt-surface-title"; } item { diff --git a/src/apprt/ipc.zig b/src/apprt/ipc.zig index a6e8412e049..b37647e0212 100644 --- a/src/apprt/ipc.zig +++ b/src/apprt/ipc.zig @@ -3,6 +3,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = @import("../quirks.zig").inlineAssert; +const lib = @import("../lib/main.zig"); pub const Errors = error{ /// The IPC failed. If a function returns this error, it's expected that @@ -22,6 +23,10 @@ pub const Target = union(Key) { pub const Key = enum(c_int) { class, detect, + + test "ghostty.h Target.Key" { + try lib.checkGhosttyHEnum(Key, "GHOSTTY_IPC_TARGET_"); + } }; // Sync with: ghostty_ipc_target_u @@ -106,8 +111,12 @@ pub const Action = union(enum) { }; /// Sync with: ghostty_ipc_action_tag_e - pub const Key = enum(c_uint) { + pub const Key = enum(c_int) { new_window, + + test "ghostty.h Action.Key" { + try lib.checkGhosttyHEnum(Key, "GHOSTTY_IPC_ACTION_"); + } }; /// Sync with: ghostty_ipc_action_u diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 5c25281c8d0..3cb0016fadf 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -188,7 +188,7 @@ pub fn newConfig( if (prev) |p| { if (shouldInheritWorkingDirectory(context, config)) { if (try p.pwd(alloc)) |pwd| { - copy.@"working-directory" = pwd; + copy.@"working-directory" = .{ .path = pwd }; } } } diff --git a/src/benchmark/CodepointWidth.zig b/src/benchmark/CodepointWidth.zig index effabb036e4..30d3f91e75f 100644 --- a/src/benchmark/CodepointWidth.zig +++ b/src/benchmark/CodepointWidth.zig @@ -6,6 +6,7 @@ const CodepointWidth = @This(); const std = @import("std"); +const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Benchmark = @import("Benchmark.zig"); @@ -104,6 +105,11 @@ fn stepNoop(ptr: *anyopaque) Benchmark.Error!void { extern "c" fn wcwidth(c: u32) c_int; fn stepWcwidth(ptr: *anyopaque) Benchmark.Error!void { + if (comptime builtin.os.tag == .windows) { + log.warn("wcwidth is not available on Windows", .{}); + return; + } + const self: *CodepointWidth = @ptrCast(@alignCast(ptr)); const f = self.data_f orelse return; diff --git a/src/benchmark/ScreenClone.zig b/src/benchmark/ScreenClone.zig index 380379bc3e1..108eaa0c6a2 100644 --- a/src/benchmark/ScreenClone.zig +++ b/src/benchmark/ScreenClone.zig @@ -94,9 +94,9 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void { // Force a style on every single row, which var s = self.terminal.vtStream(); defer s.deinit(); - s.nextSlice("\x1b[48;2;20;40;60m") catch unreachable; - for (0..self.terminal.rows - 1) |_| s.nextSlice("hello\r\n") catch unreachable; - s.nextSlice("hello") catch unreachable; + s.nextSlice("\x1b[48;2;20;40;60m"); + for (0..self.terminal.rows - 1) |_| s.nextSlice("hello\r\n"); + s.nextSlice("hello"); // Setup our terminal state const data_f: std.fs.File = (options.dataFile( @@ -120,10 +120,7 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void { return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached - stream.nextSlice(buf[0..n]) catch |err| { - log.warn("error processing data file chunk err={}", .{err}); - return error.BenchmarkFailed; - }; + stream.nextSlice(buf[0..n]); } } diff --git a/src/benchmark/TerminalStream.zig b/src/benchmark/TerminalStream.zig index 7cf28217f47..1cac656e278 100644 --- a/src/benchmark/TerminalStream.zig +++ b/src/benchmark/TerminalStream.zig @@ -125,10 +125,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void { return error.BenchmarkFailed; }; if (n == 0) break; // EOF reached - self.stream.nextSlice(buf[0..n]) catch |err| { - log.warn("error processing data file chunk err={}", .{err}); - return error.BenchmarkFailed; - }; + self.stream.nextSlice(buf[0..n]); } } @@ -142,9 +139,11 @@ const Handler = struct { self: *Handler, comptime action: Stream.Action.Tag, value: Stream.Action.Value(action), - ) !void { + ) void { switch (action) { - .print => try self.t.print(value.cp), + .print => self.t.print(value.cp) catch |err| { + log.warn("error processing benchmark print err={}", .{err}); + }, else => {}, } } diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index aae8ace191a..6d44c62b614 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -1,6 +1,7 @@ const GhosttyLibVt = @This(); const std = @import("std"); +const builtin = @import("builtin"); const assert = std.debug.assert; const RunStep = std.Build.Step.Run; const GhosttyZig = @import("GhosttyZig.zig"); @@ -61,6 +62,26 @@ pub fn initShared( .{ .include_extensions = &.{".h"} }, ); + if (lib.rootModuleTarget().abi.isAndroid()) { + // Support 16kb page sizes, required for Android 15+. + lib.link_z_max_page_size = 16384; // 16kb + + try @import("android_ndk").addPaths(b, lib); + } + + if (lib.rootModuleTarget().os.tag.isDarwin()) { + // Self-hosted x86_64 doesn't work for darwin. It may not work + // for other platforms too but definitely darwin. + lib.use_llvm = true; + + // This is required for codesign and dynamic linking to work. + lib.headerpad_max_install_names = true; + + // If we're not cross compiling then we try to find the Apple + // SDK using standard Apple tooling. + if (builtin.os.tag.isDarwin()) try @import("apple_sdk").addPaths(b, lib); + } + // Get our debug symbols const dsymutil: ?std.Build.LazyPath = dsymutil: { if (!target.result.os.tag.isDarwin()) { diff --git a/src/build/GhosttyXcodebuild.zig b/src/build/GhosttyXcodebuild.zig index 5ca4c5e9a64..81af994ca51 100644 --- a/src/build/GhosttyXcodebuild.zig +++ b/src/build/GhosttyXcodebuild.zig @@ -104,6 +104,8 @@ pub fn init( "test", "-scheme", "Ghostty", + "-skip-testing", + "GhosttyUITests", }); if (xc_arch) |arch| step.addArgs(&.{ "-arch", arch }); diff --git a/src/build/GitVersion.zig b/src/build/GitVersion.zig index 566fec2e9c9..8b368d2cd3f 100644 --- a/src/build/GitVersion.zig +++ b/src/build/GitVersion.zig @@ -39,7 +39,7 @@ pub fn detect(b: *std.Build) !Version { const short_hash = short_hash: { const output = b.runAllowFail( - &[_][]const u8{ "git", "-C", b.build_root.path orelse ".", "log", "--pretty=format:%h", "-n", "1" }, + &[_][]const u8{ "git", "-C", b.build_root.path orelse ".", "-c", "log.showSignature=false", "log", "--pretty=format:%h", "-n", "1" }, &code, .Ignore, ) catch |err| switch (err) { diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 0ca43e78d6f..9276c99145f 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -477,7 +477,9 @@ pub fn add( .freetype = true, .@"backend-metal" = target.result.os.tag.isDarwin(), .@"backend-osx" = target.result.os.tag == .macos, - .@"backend-opengl3" = target.result.os.tag != .macos, + // OpenGL3 backend should only be built on non-Apple targets. + // Apple platforms use Metal (and macOS may also use the OSX backend). + .@"backend-opengl3" = !target.result.os.tag.isDarwin(), })) |dep| { step.root_module.addImport("dcimgui", dep.module("dcimgui")); step.linkLibrary(dep.artifact("dcimgui")); diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig index 594a053669f..2bb0d4508b0 100644 --- a/src/build/uucode_config.zig +++ b/src/build/uucode_config.zig @@ -20,12 +20,17 @@ fn computeWidth( _ = backing; _ = tracking; - // This condition is to get the previous behavior of uucode's `wcwidth`, - // returning the width of a code point in a grapheme cluster but with the - // exception to treat emoji modifiers as width 2 so they can be displayed - // in isolation. PRs to follow will take advantage of the new uucode - // `wcwidth_standalone` vs `wcwidth_zero_in_grapheme` split. - if (data.wcwidth_zero_in_grapheme and !data.is_emoji_modifier) { + // This condition is needed as Ghostty currently has a singular concept for + // the `width` of a code point, while `uucode` splits the concept into + // `wcwidth_standalone` and `wcwidth_zero_in_grapheme`. The two cases where + // we want to use the `wcwidth_standalone` despite the code point occupying + // zero width in a grapheme (`wcwidth_zero_in_grapheme`) are emoji + // modifiers and prepend code points. For emoji modifiers we want to + // support displaying them in isolation as color patches, and if prepend + // characters were to be width 0 they would disappear from the output with + // Ghostty's current width 0 handling. Future work will take advantage of + // the new uucode `wcwidth_standalone` vs `wcwidth_zero_in_grapheme` split. + if (data.wcwidth_zero_in_grapheme and !data.is_emoji_modifier and data.grapheme_break_no_control != .prepend) { data.width = 0; } else { data.width = @min(2, data.wcwidth_standalone); @@ -37,6 +42,7 @@ const width = config.Extension{ "wcwidth_standalone", "wcwidth_zero_in_grapheme", "is_emoji_modifier", + "grapheme_break_no_control", }, .compute = &computeWidth, .fields = &.{ @@ -94,6 +100,7 @@ pub const tables = [_]config.Table{ }, .fields = &.{ width.field("width"), + wcwidth.field("wcwidth_zero_in_grapheme"), grapheme_break_no_control.field("grapheme_break_no_control"), is_symbol.field("is_symbol"), d.field("is_emoji_vs_base"), diff --git a/src/cli/list_actions.zig b/src/cli/list_actions.zig index 682eed251fb..aaef1195ee8 100644 --- a/src/cli/list_actions.zig +++ b/src/cli/list_actions.zig @@ -46,6 +46,7 @@ pub fn run(alloc: Allocator) !u8 { opts.docs, std.heap.page_allocator, ); + try stdout_writer.interface.flush(); return 0; } diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig index 42aff9d566a..63bf6f8f500 100644 --- a/src/cli/list_themes.zig +++ b/src/cli/list_themes.zig @@ -1,3 +1,4 @@ +const builtin = @import("builtin"); const std = @import("std"); const args = @import("args.zig"); const Action = @import("ghostty.zig").Action; @@ -9,6 +10,7 @@ const global_state = &@import("../global.zig").state; const vaxis = @import("vaxis"); const zf = @import("zf"); +const objc = if (builtin.target.os.tag.isDarwin()) @import("objc") else struct {}; // When the number of filtered themes is less than or equal to this threshold, // the window position will be reset to 0 to show all results from the top. @@ -17,6 +19,79 @@ const zf = @import("zf"); const SMALL_LIST_THRESHOLD = 10; const ColorScheme = enum { all, dark, light }; +const ThemeTargetMode = enum { both, light, dark }; +const cmux_block_start = "# cmux themes start"; +const cmux_block_end = "# cmux themes end"; + +const CmuxThemePicker = struct { + config_path: []u8, + bundle_id: []u8, + initial_light: ?[]u8, + initial_dark: ?[]u8, + target_mode: ThemeTargetMode, + ui_color_scheme: vaxis.Color.Scheme, + original_contents: ?[]u8, + + fn load(alloc: std.mem.Allocator) !?CmuxThemePicker { + const config_path = try trimmedEnvValue(alloc, "CMUX_THEME_PICKER_CONFIG"); + if (config_path == null) return null; + errdefer alloc.free(config_path.?); + + const bundle_id = (try trimmedEnvValue(alloc, "CMUX_THEME_PICKER_BUNDLE_ID")) orelse + try alloc.dupe(u8, "com.cmuxterm.app"); + errdefer alloc.free(bundle_id); + + const initial_light = try trimmedEnvValue(alloc, "CMUX_THEME_PICKER_INITIAL_LIGHT"); + errdefer if (initial_light) |value| alloc.free(value); + + const initial_dark = try trimmedEnvValue(alloc, "CMUX_THEME_PICKER_INITIAL_DARK"); + errdefer if (initial_dark) |value| alloc.free(value); + + const target_mode = target: { + const raw = try trimmedEnvValue(alloc, "CMUX_THEME_PICKER_TARGET") orelse break :target .both; + defer alloc.free(raw); + break :target std.meta.stringToEnum(ThemeTargetMode, raw) orelse .both; + }; + + const ui_color_scheme = color_scheme: { + const raw = try trimmedEnvValue(alloc, "CMUX_THEME_PICKER_COLOR_SCHEME") orelse break :color_scheme .light; + defer alloc.free(raw); + break :color_scheme std.meta.stringToEnum(vaxis.Color.Scheme, raw) orelse .light; + }; + + const original_contents = try readOptionalFile(alloc, config_path.?); + errdefer if (original_contents) |value| alloc.free(value); + + return .{ + .config_path = config_path.?, + .bundle_id = bundle_id, + .initial_light = initial_light, + .initial_dark = initial_dark, + .target_mode = target_mode, + .ui_color_scheme = ui_color_scheme, + .original_contents = original_contents, + }; + } + + fn deinit(self: *CmuxThemePicker, alloc: std.mem.Allocator) void { + alloc.free(self.config_path); + alloc.free(self.bundle_id); + if (self.initial_light) |value| alloc.free(value); + if (self.initial_dark) |value| alloc.free(value); + if (self.original_contents) |value| alloc.free(value); + } + + fn initialTheme(self: *const CmuxThemePicker) ?[]const u8 { + return switch (self.target_mode) { + .both => if (eqlOptionalTheme(self.initial_light, self.initial_dark)) + self.initial_light orelse self.initial_dark + else + self.initial_dark orelse self.initial_light, + .light => self.initial_light orelse self.initial_dark, + .dark => self.initial_dark orelse self.initial_light, + }; + } +}; pub const Options = struct { /// If true, print the full path to the theme. @@ -222,6 +297,223 @@ fn writeAutoThemeFile(alloc: std.mem.Allocator, theme_name: []const u8) !void { try w.interface.flush(); } +fn trimmedEnvValue(alloc: std.mem.Allocator, key: []const u8) !?[]u8 { + const raw = std.process.getEnvVarOwned(alloc, key) catch |err| switch (err) { + error.EnvironmentVariableNotFound => return null, + else => return err, + }; + + const trimmed = std.mem.trim(u8, raw, " \t\r\n"); + if (trimmed.len == 0) { + alloc.free(raw); + return null; + } + if (trimmed.ptr == raw.ptr and trimmed.len == raw.len) { + return raw; + } + + const duped = try alloc.dupe(u8, trimmed); + alloc.free(raw); + return duped; +} + +fn readOptionalFile(alloc: std.mem.Allocator, path: []const u8) !?[]u8 { + const file = std.fs.openFileAbsolute(path, .{}) catch |err| switch (err) { + error.FileNotFound => return null, + else => return err, + }; + defer file.close(); + + return try file.readToEndAlloc(alloc, 1024 * 1024); +} + +fn writeAbsoluteFile(path: []const u8, contents: []const u8) !void { + if (std.fs.path.dirname(path)) |dir| { + try std.fs.cwd().makePath(dir); + var dir_handle = try std.fs.openDirAbsolute(dir, .{}); + defer dir_handle.close(); + + var buf: [1024]u8 = undefined; + var atomic_file = try dir_handle.atomicFile(std.fs.path.basename(path), .{ + .mode = 0o600, + .write_buffer = &buf, + }); + defer atomic_file.deinit(); + + try atomic_file.file_writer.interface.writeAll(contents); + try atomic_file.finish(); + return; + } + + var file = try std.fs.createFileAbsolute(path, .{ + .truncate = true, + .mode = 0o600, + }); + defer file.close(); + try file.writeAll(contents); +} + +fn removeManagedThemeOverride( + alloc: std.mem.Allocator, + contents: []const u8, +) ![]u8 { + var result: std.ArrayList(u8) = .empty; + errdefer result.deinit(alloc); + + var cursor: usize = 0; + while (true) { + const start = std.mem.indexOfPos(u8, contents, cursor, cmux_block_start) orelse { + try result.appendSlice(alloc, contents[cursor..]); + break; + }; + const end_marker = std.mem.indexOfPos(u8, contents, start, cmux_block_end) orelse { + try result.appendSlice(alloc, contents[cursor..]); + break; + }; + + var remove_start = start; + if (remove_start > cursor and contents[remove_start - 1] == '\n') { + remove_start -= 1; + } + + var remove_end = end_marker + cmux_block_end.len; + if (remove_end < contents.len and contents[remove_end] == '\n') { + remove_end += 1; + } + + try result.appendSlice(alloc, contents[cursor..remove_start]); + cursor = remove_end; + } + + return try result.toOwnedSlice(alloc); +} + +fn encodeCmuxThemeValue( + alloc: std.mem.Allocator, + light: ?[]const u8, + dark: ?[]const u8, +) !?[]u8 { + if (light) |light_theme| { + if (dark) |dark_theme| { + return try std.fmt.allocPrint( + alloc, + "light:{s},dark:{s}", + .{ light_theme, dark_theme }, + ); + } + + return try std.fmt.allocPrint( + alloc, + "light:{s}", + .{light_theme}, + ); + } + + if (dark) |dark_theme| { + return try std.fmt.allocPrint( + alloc, + "dark:{s}", + .{dark_theme}, + ); + } + + return null; +} + +fn writeCmuxThemeOverride( + alloc: std.mem.Allocator, + cmux: *const CmuxThemePicker, + raw_theme_value: []const u8, +) !void { + const existing = (try readOptionalFile(alloc, cmux.config_path)) orelse + try alloc.dupe(u8, ""); + defer alloc.free(existing); + + const stripped = try removeManagedThemeOverride(alloc, existing); + defer alloc.free(stripped); + + const trimmed = std.mem.trim(u8, stripped, " \t\r\n"); + const block = try std.fmt.allocPrint( + alloc, + "{s}\ntheme = {s}\n{s}\n", + .{ cmux_block_start, raw_theme_value, cmux_block_end }, + ); + defer alloc.free(block); + + var next_contents: std.ArrayList(u8) = .empty; + defer next_contents.deinit(alloc); + if (trimmed.len > 0) { + try next_contents.appendSlice(alloc, trimmed); + try next_contents.appendSlice(alloc, "\n\n"); + } + try next_contents.appendSlice(alloc, block); + try writeAbsoluteFile(cmux.config_path, next_contents.items); +} + +fn restoreCmuxThemeOverride(cmux: *const CmuxThemePicker) !void { + if (cmux.original_contents) |contents| { + try writeAbsoluteFile(cmux.config_path, contents); + return; + } + + std.fs.deleteFileAbsolute(cmux.config_path) catch |err| switch (err) { + error.FileNotFound => {}, + else => return err, + }; +} + +fn postCmuxReloadNotification( + alloc: std.mem.Allocator, + bundle_id: []const u8, +) !void { + if (!builtin.target.os.tag.isDarwin()) return; + + const pool = objc.AutoreleasePool.init(); + defer pool.deinit(); + + const NSString = objc.getClass("NSString") orelse return error.ObjCFailed; + const center_class = objc.getClass("NSDistributedNotificationCenter") orelse + return error.ObjCFailed; + const center = center_class.msgSend(objc.Object, objc.sel("defaultCenter"), .{}); + + const name_c = try alloc.dupeZ(u8, "com.cmuxterm.themes.reload-config"); + defer alloc.free(name_c); + const object_c = try alloc.dupeZ(u8, bundle_id); + defer alloc.free(object_c); + + const name = NSString.msgSend( + objc.Object, + objc.sel("stringWithUTF8String:"), + .{name_c.ptr}, + ); + const object = NSString.msgSend( + objc.Object, + objc.sel("stringWithUTF8String:"), + .{object_c.ptr}, + ); + + center.msgSend( + void, + objc.sel("postNotificationName:object:userInfo:deliverImmediately:"), + .{ + name, + object, + @as(?*anyopaque, null), + true, + }, + ); +} + +fn eqlOptionalTheme(lhs: ?[]const u8, rhs: ?[]const u8) bool { + if (lhs) |left| { + if (rhs) |right| { + return std.ascii.eqlIgnoreCase(left, right); + } + return false; + } + return rhs == null; +} + const Event = union(enum) { key_press: vaxis.Key, mouse: vaxis.Mouse, @@ -232,6 +524,10 @@ const Event = union(enum) { const Preview = struct { allocator: std.mem.Allocator, should_quit: bool, + outcome: enum { + cancel, + apply, + }, tty: vaxis.Tty, vx: vaxis.Vaxis, mouse: ?vaxis.Mouse, @@ -249,6 +545,12 @@ const Preview = struct { color_scheme: vaxis.Color.Scheme, text_input: vaxis.widgets.TextInput, theme_filter: ColorScheme, + cmux: ?CmuxThemePicker, + cmux_target_mode: ThemeTargetMode, + cmux_preview_light: ?[]const u8, + cmux_preview_dark: ?[]const u8, + cmux_applied_light: ?[]const u8, + cmux_applied_dark: ?[]const u8, pub fn init( allocator: std.mem.Allocator, @@ -257,10 +559,12 @@ const Preview = struct { buf: []u8, ) !*Preview { const self = try allocator.create(Preview); + const cmux = try CmuxThemePicker.load(allocator); self.* = .{ .allocator = allocator, .should_quit = false, + .outcome = .cancel, .tty = try .init(buf), .vx = try vaxis.init(allocator, .{}), .mouse = null, @@ -270,9 +574,15 @@ const Preview = struct { .window = 0, .hex = false, .mode = .normal, - .color_scheme = .light, + .color_scheme = if (cmux) |value| value.ui_color_scheme else .light, .text_input = .init(allocator), .theme_filter = theme_filter, + .cmux = cmux, + .cmux_target_mode = if (cmux) |value| value.target_mode else .both, + .cmux_preview_light = if (cmux) |value| value.initial_light else null, + .cmux_preview_dark = if (cmux) |value| value.initial_dark else null, + .cmux_applied_light = if (cmux) |value| value.initial_light else null, + .cmux_applied_dark = if (cmux) |value| value.initial_dark else null, }; try self.updateFiltered(); @@ -282,6 +592,7 @@ const Preview = struct { pub fn deinit(self: *Preview) void { const allocator = self.allocator; + if (self.cmux) |*value| value.deinit(allocator); self.filtered.deinit(allocator); self.text_input.deinit(); self.vx.deinit(allocator, self.tty.writer()); @@ -290,6 +601,11 @@ const Preview = struct { } pub fn run(self: *Preview) !void { + errdefer self.restoreCmuxOriginal() catch {}; + defer if (self.outcome == .cancel) { + self.restoreCmuxOriginal() catch {}; + }; + var loop: vaxis.Loop(Event) = .{ .tty = &self.tty, .vaxis = &self.vx, @@ -300,10 +616,15 @@ const Preview = struct { 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.setTitle( + writer, + if (self.cmux != null) "cmux Theme Preview" else "👻 Ghostty Theme Preview 👻", + ); + if (self.cmux == null) { + try self.vx.queryTerminal(writer, 1 * std.time.ns_per_s); + } try self.vx.setMouseMode(writer, true); - if (self.vx.caps.color_scheme_updates) + if (self.cmux == null and self.vx.caps.color_scheme_updates) try self.vx.subscribeToColorSchemeUpdates(writer); while (!self.should_quit) { @@ -411,6 +732,56 @@ const Preview = struct { }; } + fn applyCmuxSelectionForCurrentTheme(self: *Preview) !void { + const cmux = self.cmux orelse return; + if (self.filtered.items.len == 0) return; + + const theme = self.themes[self.filtered.items[self.current]].theme; + switch (self.cmux_target_mode) { + .both => { + self.cmux_preview_light = theme; + self.cmux_preview_dark = theme; + }, + .light => self.cmux_preview_light = theme, + .dark => self.cmux_preview_dark = theme, + } + + try self.syncCmuxPreview(cmux); + } + + fn restoreCmuxOriginal(self: *Preview) !void { + const cmux = self.cmux orelse return; + self.cmux_preview_light = cmux.initial_light; + self.cmux_preview_dark = cmux.initial_dark; + try self.syncCmuxPreview(cmux); + } + + fn syncCmuxPreview(self: *Preview, cmux: CmuxThemePicker) !void { + if (eqlOptionalTheme(self.cmux_preview_light, self.cmux_applied_light) and + eqlOptionalTheme(self.cmux_preview_dark, self.cmux_applied_dark)) + { + return; + } + + if (eqlOptionalTheme(self.cmux_preview_light, cmux.initial_light) and + eqlOptionalTheme(self.cmux_preview_dark, cmux.initial_dark)) + { + try restoreCmuxThemeOverride(&cmux); + } else { + const raw_theme_value = (try encodeCmuxThemeValue( + self.allocator, + self.cmux_preview_light, + self.cmux_preview_dark, + )) orelse return; + defer self.allocator.free(raw_theme_value); + try writeCmuxThemeOverride(self.allocator, &cmux, raw_theme_value); + } + + try postCmuxReloadNotification(self.allocator, cmux.bundle_id); + self.cmux_applied_light = self.cmux_preview_light; + self.cmux_applied_dark = self.cmux_preview_dark; + } + fn up(self: *Preview, count: usize) void { if (self.filtered.items.len == 0) { self.current = 0; @@ -432,40 +803,71 @@ const Preview = struct { pub fn update(self: *Preview, event: Event, alloc: std.mem.Allocator) !void { switch (event) { .key_press => |key| { - if (key.matches('c', .{ .ctrl = true })) + if (key.matches('c', .{ .ctrl = true })) { + self.outcome = .cancel; self.should_quit = true; + } switch (self.mode) { .normal => { - if (key.matchesAny(&.{ 'q', vaxis.Key.escape }, .{})) + if (key.matchesAny(&.{ 'q', vaxis.Key.escape }, .{})) { + self.outcome = .cancel; self.should_quit = true; + } if (key.matchesAny(&.{ '?', vaxis.Key.f1 }, .{})) self.mode = .help; if (key.matches('h', .{ .ctrl = true })) self.mode = .help; if (key.matches('/', .{})) self.mode = .search; - if (key.matchesAny(&.{ vaxis.Key.enter, vaxis.Key.kp_enter }, .{})) - self.mode = .save; + if (key.matchesAny(&.{ vaxis.Key.enter, vaxis.Key.kp_enter }, .{})) { + if (self.cmux != null) { + self.outcome = .apply; + self.should_quit = true; + } else { + self.mode = .save; + } + } if (key.matchesAny(&.{ 'x', '/' }, .{ .ctrl = true })) { self.text_input.buf.clearRetainingCapacity(); try self.updateFiltered(); + try self.applyCmuxSelectionForCurrentTheme(); } - if (key.matchesAny(&.{ vaxis.Key.home, vaxis.Key.kp_home }, .{})) + if (key.matchesAny(&.{ vaxis.Key.home, vaxis.Key.kp_home }, .{})) { self.current = 0; - if (key.matchesAny(&.{ vaxis.Key.end, vaxis.Key.kp_end }, .{})) + try self.applyCmuxSelectionForCurrentTheme(); + } + if (key.matchesAny(&.{ vaxis.Key.end, vaxis.Key.kp_end }, .{})) { self.current = self.filtered.items.len - 1; - if (key.matchesAny(&.{ 'j', '+', vaxis.Key.down, vaxis.Key.kp_down, vaxis.Key.kp_add }, .{})) + try self.applyCmuxSelectionForCurrentTheme(); + } + if (key.matchesAny(&.{ 'j', '+', vaxis.Key.down, vaxis.Key.kp_down, vaxis.Key.kp_add }, .{})) { self.down(1); - if (key.matchesAny(&.{ vaxis.Key.page_down, vaxis.Key.kp_down }, .{})) + try self.applyCmuxSelectionForCurrentTheme(); + } + if (key.matchesAny(&.{ vaxis.Key.page_down, vaxis.Key.kp_down }, .{})) { self.down(20); - if (key.matchesAny(&.{ 'k', '-', vaxis.Key.up, vaxis.Key.kp_up, vaxis.Key.kp_subtract }, .{})) + try self.applyCmuxSelectionForCurrentTheme(); + } + if (key.matchesAny(&.{ 'k', '-', vaxis.Key.up, vaxis.Key.kp_up, vaxis.Key.kp_subtract }, .{})) { self.up(1); - if (key.matchesAny(&.{ vaxis.Key.page_up, vaxis.Key.kp_page_up }, .{})) + try self.applyCmuxSelectionForCurrentTheme(); + } + if (key.matchesAny(&.{ vaxis.Key.page_up, vaxis.Key.kp_page_up }, .{})) { self.up(20); + try self.applyCmuxSelectionForCurrentTheme(); + } if (key.matchesAny(&.{ 'h', 'x' }, .{})) self.hex = true; if (key.matches('d', .{})) self.hex = false; + if (self.cmux != null and key.matches('t', .{})) { + self.cmux_target_mode = switch (self.cmux_target_mode) { + .both => .light, + .light => .dark, + .dark => .both, + }; + try self.applyCmuxSelectionForCurrentTheme(); + } if (key.matches('c', .{})) try self.vx.copyToSystemClipboard( self.tty.writer(), @@ -485,11 +887,14 @@ const Preview = struct { .light => self.theme_filter = .all, } try self.updateFiltered(); + try self.applyCmuxSelectionForCurrentTheme(); } }, .help => { - if (key.matches('q', .{})) + if (key.matches('q', .{})) { + self.outcome = .cancel; self.should_quit = true; + } if (key.matchesAny(&.{ '?', vaxis.Key.escape, vaxis.Key.f1 }, .{})) self.mode = .normal; if (key.matches('h', .{ .ctrl = true })) @@ -503,14 +908,18 @@ const Preview = struct { if (key.matchesAny(&.{ 'x', '/' }, .{ .ctrl = true })) { self.text_input.clearRetainingCapacity(); try self.updateFiltered(); + try self.applyCmuxSelectionForCurrentTheme(); break :search; } try self.text_input.update(.{ .key_press = key }); try self.updateFiltered(); + try self.applyCmuxSelectionForCurrentTheme(); }, .save => { - if (key.matches('q', .{})) + if (key.matches('q', .{})) { + self.outcome = .cancel; self.should_quit = true; + } if (key.matchesAny(&.{ vaxis.Key.escape, vaxis.Key.enter, vaxis.Key.kp_enter }, .{})) self.mode = .normal; if (key.matches('w', .{})) { @@ -595,6 +1004,13 @@ const Preview = struct { }; } + pub fn ui_footer(self: *Preview) vaxis.Style { + return .{ + .fg = self.ui_fg(), + .bg = self.ui_hover_bg(), + }; + } + pub fn draw(self: *Preview, alloc: std.mem.Allocator) !void { const win = self.vx.window(); win.clear(); @@ -615,15 +1031,18 @@ const Preview = struct { if (self.mode == .normal) { if (mouse.button == .wheel_up) { self.up(1); + try self.applyCmuxSelectionForCurrentTheme(); } if (mouse.button == .wheel_down) { self.down(1); + try self.applyCmuxSelectionForCurrentTheme(); } if (theme_list.hasMouse(mouse)) |_| { if (mouse.button == .left and mouse.type == .release) { const selection = self.window + mouse.row; if (selection < self.filtered.items.len) { self.current = selection; + try self.applyCmuxSelectionForCurrentTheme(); } } highlight = mouse.row; @@ -720,6 +1139,37 @@ const Preview = struct { try self.drawPreview(alloc, win, theme_list.x_off + theme_list.width); + if (self.cmux != null) { + const footer = win.child(.{ + .x_off = 0, + .y_off = win.height - 1, + .width = win.width, + .height = 1, + }); + footer.fill(.{ .style = self.ui_footer() }); + + const text = try std.fmt.allocPrint( + alloc, + " cmux live preview target={s} light={s} dark={s} t cycle target Enter apply q cancel ", + .{ + @tagName(self.cmux_target_mode), + self.cmux_preview_light orelse "inherit", + self.cmux_preview_dark orelse "inherit", + }, + ); + const max_len = @min(text.len, footer.width); + _ = footer.printSegment( + .{ + .text = text[0..max_len], + .style = self.ui_footer(), + }, + .{ + .row_offset = 0, + .col_offset = 0, + }, + ); + } + switch (self.mode) { .normal => { win.hideCursor(); @@ -761,11 +1211,31 @@ const Preview = struct { .{ .keys = "End", .help = "Go to the end of the list." }, .{ .keys = "/", .help = "Start search." }, .{ .keys = "^X, ^/", .help = "Clear search." }, - .{ .keys = "âŽ", .help = "Save theme or close search window." }, - .{ .keys = "w", .help = "Write theme to auto config file." }, + .{ + .keys = "âŽ", + .help = if (self.cmux != null) + "Apply current preview and close." + else + "Save theme or close search window.", + }, + .{ + .keys = "w", + .help = if (self.cmux != null) + "Unused in cmux mode." + else + "Write theme to auto config file.", + }, + .{ + .keys = "t", + .help = if (self.cmux != null) + "Cycle cmux target (both, light, dark)." + else + "", + }, }; for (key_help, 0..) |help, captured_i| { + if (help.help.len == 0) continue; const i: u16 = @intCast(captured_i); _ = child.printSegment( .{ diff --git a/src/cli/new_window.zig b/src/cli/new_window.zig index f3f4740d12e..a89c4ffabf5 100644 --- a/src/cli/new_window.zig +++ b/src/cli/new_window.zig @@ -5,6 +5,8 @@ const Action = @import("../cli.zig").ghostty.Action; const apprt = @import("../apprt.zig"); const args = @import("args.zig"); const diagnostics = @import("diagnostics.zig"); +const lib = @import("../lib/main.zig"); +const homedir = @import("../os/homedir.zig"); pub const Options = struct { /// This is set by the CLI parser for deinit. @@ -13,35 +15,63 @@ pub const Options = struct { /// If set, open up a new window in a custom instance of Ghostty. class: ?[:0]const u8 = null, - /// If `-e` is found in the arguments, this will contain all of the - /// arguments to pass to Ghostty as the command. - _arguments: ?[][:0]const u8 = null, + /// Did the user specify a `--working-directory` argument on the command line? + _working_directory_seen: bool = false, + + /// All of the arguments after `+new-window`. They will be sent to Ghosttty + /// for processing. + _arguments: std.ArrayList([:0]const u8) = .empty, /// Enable arg parsing diagnostics so that we don't get an error if /// there is a "normal" config setting on the cli. _diagnostics: diagnostics.DiagnosticList = .{}, - /// Manual parse hook, used to deal with `-e` - pub fn parseManuallyHook(self: *Options, alloc: Allocator, arg: []const u8, iter: anytype) Allocator.Error!bool { - // If it's not `-e` continue with the standard argument parsning. - if (!std.mem.eql(u8, arg, "-e")) return true; + /// Manual parse hook, collect all of the arguments after `+new-window`. + pub fn parseManuallyHook(self: *Options, alloc: Allocator, arg: []const u8, iter: anytype) (error{InvalidValue} || homedir.ExpandError || std.fs.Dir.RealPathAllocError || Allocator.Error)!bool { + var e_seen: bool = std.mem.eql(u8, arg, "-e"); - var arguments: std.ArrayList([:0]const u8) = .empty; - errdefer { - for (arguments.items) |argument| alloc.free(argument); - arguments.deinit(alloc); - } + // Include the argument that triggered the manual parse hook. + if (try self.checkArg(alloc, arg)) |a| try self._arguments.append(alloc, a); - // Otherwise gather up the rest of the arguments to use as the command. + // Gather up the rest of the arguments to use as the command. while (iter.next()) |param| { - try arguments.append(alloc, try alloc.dupeZ(u8, param)); + if (e_seen) { + try self._arguments.append(alloc, try alloc.dupeZ(u8, param)); + continue; + } + if (std.mem.eql(u8, param, "-e")) { + e_seen = true; + try self._arguments.append(alloc, try alloc.dupeZ(u8, param)); + continue; + } + if (try self.checkArg(alloc, param)) |a| try self._arguments.append(alloc, a); } - self._arguments = try arguments.toOwnedSlice(alloc); - return false; } + fn checkArg(self: *Options, alloc: Allocator, arg: []const u8) (error{InvalidValue} || homedir.ExpandError || std.fs.Dir.RealPathAllocError || Allocator.Error)!?[:0]const u8 { + if (lib.cutPrefix(u8, arg, "--class=")) |rest| { + self.class = try alloc.dupeZ(u8, std.mem.trim(u8, rest, &std.ascii.whitespace)); + return null; + } + + if (lib.cutPrefix(u8, arg, "--working-directory=")) |rest| { + const stripped = std.mem.trim(u8, rest, &std.ascii.whitespace); + if (std.mem.eql(u8, stripped, "home")) return try alloc.dupeZ(u8, arg); + if (std.mem.eql(u8, stripped, "inherit")) return try alloc.dupeZ(u8, arg); + const cwd: std.fs.Dir = std.fs.cwd(); + var expandhome_buf: [std.fs.max_path_bytes]u8 = undefined; + const expanded = try homedir.expandHome(stripped, &expandhome_buf); + var realpath_buf: [std.fs.max_path_bytes]u8 = undefined; + const realpath = try cwd.realpath(expanded, &realpath_buf); + self._working_directory_seen = true; + return try std.fmt.allocPrintSentinel(alloc, "--working-directory={s}", .{realpath}, 0); + } + + return try alloc.dupeZ(u8, arg); + } + pub fn deinit(self: *Options) void { if (self._arena) |arena| arena.deinit(); self.* = undefined; @@ -63,11 +93,23 @@ pub const Options = struct { /// and contact a running Ghostty instance that was configured with the same /// `class` as was given on the command line. /// -/// If the `-e` flag is included on the command line, any arguments that follow -/// will be sent to the running Ghostty instance and used as the command to run -/// in the new window rather than the default. If `-e` is not specified, Ghostty -/// will use the default command (either specified with `command` in your config -/// or your default shell as configured on your system). +/// All of the arguments after the `+new-window` argument (except for the +/// `--class` flag) will be sent to the remote Ghostty instance and will be +/// parsed as command line flags. These flags will override certain settings +/// when creating the first surface in the new window. Currently, only +/// `--working-directory`, `--command`, and `--title` are supported. `-e` will +/// also work as an alias for `--command`, except that if `-e` is found on the +/// command line all following arguments will become part of the command and no +/// more arguments will be parsed for configuration settings. +/// +/// If `--working-directory` is found on the command line and is a relative +/// path (i.e. doesn't start with `/`) it will be resolved to an absolute path +/// relative to the current working directory that the `ghostty +new-window` +/// command is run from. `~/` prefixes will also be expanded to the user's home +/// directory. +/// +/// If `--working-directory` is _not_ found on the command line, the working +/// directory that `ghostty +new-window` is run from will be passed to Ghostty. /// /// GTK uses an application ID to identify instances of applications. If Ghostty /// is compiled with release optimizations, the default application ID will be @@ -92,8 +134,16 @@ pub const Options = struct { /// * `--class=`: If set, open up a new window in a custom instance of /// Ghostty. The class must be a valid GTK application ID. /// +/// * `--command`: The command to be executed in the first surface of the new window. +/// +/// * `--working-directory=`: The working directory to pass to Ghostty. +/// +/// * `--title`: A title that will override the title of the first surface in +/// the new window. The title override may be edited or removed later. +/// /// * `-e`: Any arguments after this will be interpreted as a command to -/// execute inside the new window instead of the default command. +/// execute inside the first surface of the new window instead of the +/// default command. /// /// Available since: 1.2.0 pub fn run(alloc: Allocator) !u8 { @@ -143,11 +193,13 @@ fn runArgs( if (exit) return 1; } - if (opts._arguments) |arguments| { - if (arguments.len == 0) { - try stderr.print("The -e flag was specified on the command line, but no other arguments were found.\n", .{}); - return 1; - } + if (!opts._working_directory_seen) { + const alloc = opts._arena.?.allocator(); + const cwd: std.fs.Dir = std.fs.cwd(); + var buf: [std.fs.max_path_bytes]u8 = undefined; + const wd = try cwd.realpath(".", &buf); + // This should be inserted at the beginning of the list, just in case `-e` was used. + try opts._arguments.insert(alloc, 0, try std.fmt.allocPrintSentinel(alloc, "--working-directory={s}", .{wd}, 0)); } var arena = ArenaAllocator.init(alloc_gpa); @@ -159,7 +211,7 @@ fn runArgs( if (opts.class) |class| .{ .class = class } else .detect, .new_window, .{ - .arguments = opts._arguments, + .arguments = if (opts._arguments.items.len == 0) null else opts._arguments.items, }, ) catch |err| switch (err) { error.IPCFailed => { diff --git a/src/cli/ssh-cache/DiskCache.zig b/src/cli/ssh-cache/DiskCache.zig index 6214d042963..6fa74b43d18 100644 --- a/src/cli/ssh-cache/DiskCache.zig +++ b/src/cli/ssh-cache/DiskCache.zig @@ -57,6 +57,16 @@ pub fn clear(self: DiskCache) !void { pub const AddResult = enum { added, updated }; +pub const AddError = std.fs.Dir.MakeError || + std.fs.Dir.StatFileError || + std.fs.File.OpenError || + std.fs.File.ChmodError || + std.io.Reader.LimitedAllocError || + FixupPermissionsError || + ReadEntriesError || + WriteCacheFileError || + Error; + /// 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). @@ -64,7 +74,7 @@ pub fn add( self: DiskCache, alloc: Allocator, hostname: []const u8, -) !AddResult { +) AddError!AddResult { if (!isValidCacheKey(hostname)) return error.HostnameIsInvalid; // Create cache directory if needed @@ -128,13 +138,19 @@ pub fn add( return result; } +pub const RemoveError = std.fs.File.OpenError || + FixupPermissionsError || + ReadEntriesError || + WriteCacheFileError || + Error; + /// 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, -) !void { +) RemoveError!void { if (!isValidCacheKey(hostname)) return error.HostnameIsInvalid; // Open our file @@ -168,13 +184,17 @@ pub fn remove( try self.writeCacheFile(entries, null); } +pub const ContainsError = std.fs.File.OpenError || + ReadEntriesError || + error{HostnameIsInvalid}; + /// Check if a hostname exists in the cache. /// Returns false if the cache file doesn't exist. pub fn contains( self: DiskCache, alloc: Allocator, hostname: []const u8, -) !bool { +) ContainsError!bool { if (!isValidCacheKey(hostname)) return error.HostnameIsInvalid; // Open our file @@ -194,7 +214,9 @@ pub fn contains( return entries.contains(hostname); } -fn fixupPermissions(file: std.fs.File) (std.fs.File.StatError || std.fs.File.ChmodError)!void { +pub const FixupPermissionsError = (std.fs.File.StatError || std.fs.File.ChmodError); + +fn fixupPermissions(file: std.fs.File) FixupPermissionsError!void { // Windows does not support chmod if (comptime builtin.os.tag == .windows) return; @@ -206,11 +228,18 @@ fn fixupPermissions(file: std.fs.File) (std.fs.File.StatError || std.fs.File.Chm } } +pub const WriteCacheFileError = std.fs.Dir.OpenError || + std.fs.AtomicFile.InitError || + std.fs.AtomicFile.FlushError || + std.fs.AtomicFile.FinishError || + Entry.FormatError || + error{InvalidCachePath}; + fn writeCacheFile( self: DiskCache, entries: std.StringHashMap(Entry), expire_days: ?u32, -) !void { +) WriteCacheFileError!void { const cache_dir = std.fs.path.dirname(self.path) orelse return error.InvalidCachePath; const cache_basename = std.fs.path.basename(self.path); @@ -270,10 +299,12 @@ pub fn deinitEntries( entries.deinit(); } +pub const ReadEntriesError = std.mem.Allocator.Error || std.io.Reader.LimitedAllocError; + fn readEntries( alloc: Allocator, file: std.fs.File, -) !std.StringHashMap(Entry) { +) ReadEntriesError!std.StringHashMap(Entry) { var reader = file.reader(&.{}); const content = try reader.interface.allocRemaining( alloc, diff --git a/src/cli/ssh-cache/Entry.zig b/src/cli/ssh-cache/Entry.zig index f3403dbd4fb..b586161f269 100644 --- a/src/cli/ssh-cache/Entry.zig +++ b/src/cli/ssh-cache/Entry.zig @@ -33,7 +33,9 @@ pub fn parse(line: []const u8) ?Entry { }; } -pub fn format(self: Entry, writer: *std.Io.Writer) !void { +pub const FormatError = std.Io.Writer.Error; + +pub fn format(self: Entry, writer: *std.Io.Writer) FormatError!void { try writer.print( "{s}|{d}|{s}\n", .{ self.hostname, self.timestamp, self.terminfo_version }, diff --git a/src/config.zig b/src/config.zig index 4abd319a6bf..314fb49eeec 100644 --- a/src/config.zig +++ b/src/config.zig @@ -31,6 +31,7 @@ pub const Keybinds = Config.Keybinds; pub const MouseShiftCapture = Config.MouseShiftCapture; pub const MouseScrollMultiplier = Config.MouseScrollMultiplier; pub const NonNativeFullscreen = Config.NonNativeFullscreen; +pub const Fullscreen = Config.Fullscreen; pub const RepeatableCodepointMap = Config.RepeatableCodepointMap; pub const RepeatableFontVariation = Config.RepeatableFontVariation; pub const RepeatableString = Config.RepeatableString; @@ -43,6 +44,7 @@ pub const WindowPaddingColor = Config.WindowPaddingColor; pub const BackgroundImagePosition = Config.BackgroundImagePosition; pub const BackgroundImageFit = Config.BackgroundImageFit; pub const LinkPreviews = Config.LinkPreviews; +pub const WorkingDirectory = Config.WorkingDirectory; // Alternate APIs pub const CApi = @import("config/CApi.zig"); diff --git a/src/config/CApi.zig b/src/config/CApi.zig index 4ea9ea63f7a..ca15ce4e89f 100644 --- a/src/config/CApi.zig +++ b/src/config/CApi.zig @@ -144,3 +144,101 @@ export fn ghostty_config_open_path() c.String { const Diagnostic = extern struct { message: [*:0]const u8 = "", }; + +test "ghostty_config_get: bool" { + const testing = std.testing; + const alloc = testing.allocator; + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + cfg.maximize = true; + + var out = false; + const key = "maximize"; + try testing.expect(ghostty_config_get(&cfg, &out, key, key.len)); + try testing.expect(out); +} + +test "ghostty_config_get: enum" { + const testing = std.testing; + const alloc = testing.allocator; + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + cfg.@"window-theme" = .dark; + + var out: [*:0]const u8 = undefined; + const key = "window-theme"; + try testing.expect(ghostty_config_get(&cfg, @ptrCast(&out), key, key.len)); + const str = std.mem.sliceTo(out, 0); + try testing.expectEqualStrings("dark", str); +} + +test "ghostty_config_get: optional null returns false" { + const testing = std.testing; + const alloc = testing.allocator; + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + cfg.@"unfocused-split-fill" = null; + + var out: Config.Color.C = undefined; + const key = "unfocused-split-fill"; + try testing.expect(!ghostty_config_get(&cfg, @ptrCast(&out), key, key.len)); +} + +test "ghostty_config_get: unknown key returns false" { + const testing = std.testing; + const alloc = testing.allocator; + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + + var out = false; + const key = "not-a-real-key"; + try testing.expect(!ghostty_config_get(&cfg, &out, key, key.len)); +} + +test "ghostty_config_get: optional string null returns true" { + const testing = std.testing; + const alloc = testing.allocator; + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + cfg.title = null; + + var out: ?[*:0]const u8 = undefined; + const key = "title"; + try testing.expect(ghostty_config_get(&cfg, @ptrCast(&out), key, key.len)); + try testing.expect(out == null); +} + +test "ghostty_config_get: float" { + const testing = std.testing; + const alloc = testing.allocator; + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + cfg.@"background-opacity" = 0.42; + + var out: f64 = 0; + const key = "background-opacity"; + try testing.expect(ghostty_config_get(&cfg, &out, key, key.len)); + try testing.expectApproxEqAbs(@as(f64, 0.42), out, 0.000001); +} + +test "ghostty_config_get: struct cval conversion" { + const testing = std.testing; + const alloc = testing.allocator; + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + cfg.background = .{ .r = 12, .g = 34, .b = 56 }; + + var out: Config.Color.C = undefined; + const key = "background"; + try testing.expect(ghostty_config_get(&cfg, @ptrCast(&out), key, key.len)); + try testing.expectEqual(@as(u8, 12), out.r); + try testing.expectEqual(@as(u8, 34), out.g); + try testing.expectEqual(@as(u8, 56), out.b); +} diff --git a/src/config/Config.zig b/src/config/Config.zig index bb86b6bd59a..856aa1df5ef 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -29,7 +29,7 @@ const file_load = @import("file_load.zig"); const formatterpkg = @import("formatter.zig"); const themepkg = @import("theme.zig"); const url = @import("url.zig"); -const Key = @import("key.zig").Key; +pub const Key = @import("key.zig").Key; const MetricModifier = fontpkg.Metrics.Modifier; const help_strings = @import("help_strings"); pub const Command = @import("command.zig").Command; @@ -39,6 +39,7 @@ pub const Path = @import("path.zig").Path; pub const RepeatablePath = @import("path.zig").RepeatablePath; const ClipboardCodepointMap = @import("ClipboardCodepointMap.zig"); const KeyRemapSet = @import("../input/key_mods.zig").RemapSet; +const string = @import("string.zig"); // We do this instead of importing all of terminal/main.zig to // limit the dependency graph. This is important because some things @@ -95,10 +96,9 @@ pub const compatibility = std.StaticStringMap( }); /// Set Ghostty's graphical user interface language to a language other than the -/// system default language. The language must be fully specified, including the -/// encoding. For example: +/// system default language. For example: /// -/// language = de_DE.UTF-8 +/// language = de /// /// will force the strings in Ghostty's graphical user interface to be in German /// rather than the system default. @@ -749,7 +749,7 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// The null character (U+0000) is always treated as a boundary and does not /// need to be included in this configuration. /// -/// Default: ` \t'"│`|:;,()[]{}<>$` +/// Default: `` \t'"│`|:;,()[]{}<>$ `` /// /// To add or remove specific characters, you can set this to a custom value. /// For example, to treat semicolons as part of words: @@ -802,11 +802,35 @@ palette: Palette = .{}, /// look. Colors that have been explicitly set via `palette` are never /// overwritten. /// +/// The default value is false (disabled), because many legacy programs +/// using the 256-color palette hardcode assumptions about what these +/// colors are (mostly assuming the xterm 256 color palette). However, this +/// is still a very useful tool for theme authors and users who want +/// to customize their palette without having to specify all 256 colors. +/// /// For more information on how the generation works, see here: /// https://gist.github.com/jake-stewart/0a8ea46159a7da2c808e5be2177e1783 /// /// Available since: 1.3.0 -@"palette-generate": bool = true, +@"palette-generate": bool = false, + +/// Invert the palette colors generated when `palette-generate` is enabled, +/// so that the colors go in reverse order. This allows palette-based +/// applications to work well in both light and dark mode since the +/// palettes are always relatively good colors. +/// +/// This defaults to off because some legacy terminal applications +/// hardcode the assumption that palette indices 16–231 are ordered from +/// darkest to lightest, so enabling this would make them unreadable. +/// This is not a generally good assumption and we encourage modern +/// terminal applications to use the indices in a more semantic way. +/// +/// This has no effect if `palette-generate` is disabled. +/// +/// For more information see `palette-generate`. +/// +/// Available since: 1.3.0 +@"palette-harmonious": bool = false, /// The color of the cursor. If this is not set, a default will be chosen. /// @@ -874,19 +898,18 @@ palette: Palette = .{}, /// background color. @"cursor-text": ?TerminalColor = null, -/// Enables the ability to move the cursor at prompts by using `alt+click` on -/// Linux and `option+click` on macOS. +/// Enables the ability to move the cursor at prompts by clicking on a +/// location in the prompt text. /// -/// This feature requires shell integration (specifically prompt marking -/// via `OSC 133`) and only works in primary screen mode. Alternate screen -/// applications like vim usually have their own version of this feature but -/// this configuration doesn't control that. +/// This feature requires shell integration, specifically prompt marking +/// via `OSC 133`. Some shells like Fish (v4) and Nu (0.111+) natively +/// support this while others may require additional configuration or +/// Ghostty's shell integration features to be enabled. /// -/// It should be noted that this feature works by translating your desired -/// position into a series of synthetic arrow key movements, so some weird -/// behavior around edge cases are to be expected. This is unfortunately how -/// this feature is implemented across terminals because there isn't any other -/// way to implement it. +/// Depending on the shell, this works either by translating your click +/// position into a series of synthetic arrow key movements or by sending +/// a click event directly to the shell. In either case, some unexpected +/// behavior around edge cases is possible. @"cursor-click-to-move": bool = true, /// Hide the mouse immediately when typing. The mouse becomes visible again @@ -906,7 +929,7 @@ palette: Palette = .{}, /// anything but modifiers or keybinds that are processed by Ghostty). /// /// - `output` If set, scroll the surface to the bottom if there is new data -/// to display. (Currently unimplemented.) +/// to display (e.g., when new lines are printed to the terminal). /// /// The default is `keystroke, no-output`. @"scroll-to-bottom": ScrollToBottom = .default, @@ -1188,8 +1211,6 @@ command: ?Command = null, /// notifications for a single command, overriding the `never` and `unfocused` /// options. /// -/// GTK only. -/// /// Available since 1.3.0. @"notify-on-command-finish": NotifyOnCommandFinish = .never, @@ -1204,8 +1225,6 @@ command: ?Command = null, /// 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, @@ -1243,8 +1262,6 @@ command: ?Command = null, /// 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 }, @@ -1381,8 +1398,6 @@ input: RepeatableReadableIO = .{}, /// * `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 @@ -1428,10 +1443,27 @@ maximize: bool = false, /// does not apply to tabs, splits, etc. However, this setting will apply to all /// new windows, not just the first one. /// -/// On macOS, this setting does not work if window-decoration is set to -/// "none", because native fullscreen on macOS requires window decorations -/// to be set. -fullscreen: bool = false, +/// Allowable values are: +/// +/// * `false` - Don't start in fullscreen (default) +/// * `true` - Start in native fullscreen +/// * `non-native` - (macOS only) Start in non-native fullscreen, hiding the +/// menu bar. This is faster than native fullscreen since it doesn't use +/// animations. On non-macOS platforms, this behaves the same as `true`. +/// * `non-native-visible-menu` - (macOS only) Start in non-native fullscreen, +/// keeping the menu bar visible. On non-macOS platforms, behaves like `true`. +/// * `non-native-padded-notch` - (macOS only) Start in non-native fullscreen, +/// hiding the menu bar but padding for the notch on applicable devices. +/// On non-macOS platforms, behaves like `true`. +/// +/// Important: tabs DO NOT WORK with non-native fullscreen modes. Non-native +/// fullscreen removes the titlebar and macOS native tabs require the titlebar. +/// If you use tabs, use `true` (native) instead. +/// +/// On macOS, `true` (native fullscreen) does not work if `window-decoration` +/// is set to `false`, because native fullscreen on macOS requires window +/// decorations. +fullscreen: Fullscreen = .false, /// The title Ghostty will use for the window. This will force the title of the /// window to be this title at all times and Ghostty will ignore any set title @@ -1492,13 +1524,14 @@ class: ?[:0]const u8 = null, /// `open`, then it defaults to `home`. On Linux with GTK, if Ghostty can detect /// it was launched from a desktop launcher, then it defaults to `home`. /// -/// The value of this must be an absolute value or one of the special values -/// below: +/// The value of this must be an absolute path, a path prefixed with `~/` +/// (the tilde will be expanded to the user's home directory), or +/// one of the special values below: /// /// * `home` - The home directory of the executing user. /// /// * `inherit` - The working directory of the launching process. -@"working-directory": ?[]const u8 = null, +@"working-directory": ?WorkingDirectory = null, /// Key bindings. The format is `trigger=action`. Duplicate triggers will /// overwrite previously set values. The list of actions is available in @@ -1822,6 +1855,12 @@ class: ?[:0]const u8 = null, /// If an invalid key is pressed, the sequence ends but the table remains /// active. /// +/// * Chain actions work within tables, the `chain` keyword applies to +/// the most recently defined binding in the table. e.g. if you set +/// `table/ctrl+a=new_window` you can chain by using `chain=text:hello`. +/// Important: chain itself doesn't get prefixed with the table name, +/// since it applies to the most recent binding in any table. +/// /// * Prefixes like `global:` work within tables: /// `foo/global:ctrl+a=new_window`. /// @@ -1924,7 +1963,16 @@ keybind: Keybinds = .{}, /// apply. The other padding is applied first and may affect how many grid cells /// actually exist, and this is applied last in order to balance the padding /// given a certain viewport size and grid cell size. -@"window-padding-balance": bool = false, +/// +/// Valid values are: +/// +/// * `false` - No balancing is applied. +/// * `true` - Balance the padding, but cap the top padding to avoid +/// excessive space above the first row. Any excess is shifted to the +/// bottom. +/// * `equal` - Balance the padding equally on all sides without any +/// top-padding cap. (Available since: 1.4.0) +@"window-padding-balance": WindowPaddingBalance = .false, /// The color of the padding area of the window. Valid values are: /// @@ -2904,6 +2952,20 @@ keybind: Keybinds = .{}, /// /// * `vec4 iPreviousCursorColor` - Color of the previous terminal cursor. /// +/// * `vec4 iCurrentCursorStyle` - Style of the terminal cursor +/// +/// Macros simplified use are defined for the various cursor styles: +/// +/// - `CURSORSTYLE_BLOCK` or `0` +/// - `CURSORSTYLE_BLOCK_HOLLOW` or `1` +/// - `CURSORSTYLE_BAR` or `2` +/// - `CURSORSTYLE_UNDERLINE` or `3` +/// - `CURSORSTYLE_LOCK` or `4` +/// +/// * `vec4 iPreviousCursorStyle` - Style of the previous terminal cursor +/// +/// * `vec4 iCursorVisible` - Visibility of the terminal cursor. +/// /// * `float iTimeCursorChange` - Timestamp of terminal cursor change. /// /// When the terminal cursor changes position or color, this is set to @@ -2993,7 +3055,7 @@ keybind: Keybinds = .{}, /// /// * `audio` /// -/// Play a custom sound. (GTK only) +/// Play a custom sound. (Available since 1.3.0 on macOS) /// /// * `attention` *(enabled by default)* /// @@ -3033,17 +3095,16 @@ keybind: Keybinds = .{}, /// the path is not absolute, it is considered relative to the directory of the /// configuration file that it is referenced from, or from the current working /// directory if this is used as a CLI flag. The path may be prefixed with `~/` -/// to reference the user's home directory. (GTK only) +/// to reference the user's home directory. /// -/// Available since: 1.2.0 +/// Available since: 1.2.0 on GTK, 1.3.0 on macOS. @"bell-audio-path": ?Path = null, /// If `audio` is an enabled bell feature, this is the volume to play the audio /// file at (relative to the system volume). This is a floating point number /// ranging from 0.0 (silence) to 1.0 (as loud as possible). The default is 0.5. -/// (GTK only) /// -/// Available since: 1.2.0 +/// Available since: 1.2.0 on GTK, 1.3.0 on macOS. @"bell-audio-volume": f64 = 0.5, /// Control the in-app notifications that Ghostty shows. @@ -3294,6 +3355,16 @@ keybind: Keybinds = .{}, /// you may want to disable it. @"macos-secure-input-indication": bool = true, +/// If true, Ghostty exposes and handles the built-in AppleScript dictionary +/// on macOS. +/// +/// If false, all AppleScript interactions are disabled. This includes +/// AppleScript commands and AppleScript object lookup for windows, tabs, +/// and terminals. +/// +/// The default is true. +@"macos-applescript": bool = true, + /// Customize the macOS app icon. /// /// This only affects the icon that appears in the dock, application @@ -3583,6 +3654,11 @@ else /// notifications using certain escape sequences such as OSC 9 or OSC 777. @"desktop-notifications": bool = true, +/// If `true` (default), applications running in the terminal can show +/// graphical progress bars using the ConEmu OSC 9;4 escape sequence. +/// If `false`, progress bar sequences are silently ignored. +@"progress-style": bool = true, + /// Modifies the color used for bold text in the terminal. /// /// This can be set to a specific color, using the same format as @@ -3950,10 +4026,28 @@ pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { const app_support_path = try file_load.preferredAppSupportPath(alloc); defer alloc.free(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); + const legacy_app_support_action = self.loadOptionalFile( + alloc, + legacy_app_support_path, + ); + + // The app support path and legacy may be the same, since we + // use the `preferred` call above. If its the same, avoid + // a double-load. + const app_support_action: OptionalFileAction = if (!std.mem.eql( + u8, + legacy_app_support_path, + app_support_path, + )) self.loadOptionalFile( + alloc, + app_support_path, + ) else .not_found; + 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( + "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; } @@ -4438,23 +4532,18 @@ pub fn finalize(self: *Config) !void { } // The default for the working directory depends on the system. - const wd = self.@"working-directory" orelse if (probable_cli) - // From the CLI, we want to inherit where we were launched from. - "inherit" + var wd: WorkingDirectory = self.@"working-directory" orelse if (probable_cli) + .inherit else - // Otherwise we typically just want the home directory because - // our pwd is probably a runtime state dir or root or something - // (launchers and desktop environments typically do this). - "home"; + .home; // If we are missing either a command or home directory, we need // to look up defaults which is kind of expensive. We only do this // on desktop. - const wd_home = std.mem.eql(u8, "home", wd); if ((comptime !builtin.target.cpu.arch.isWasm()) and (comptime !builtin.is_test)) { - if (self.command == null or wd_home) command: { + if (self.command == null or wd == .home) command: { // First look up the command using the SHELL env var if needed. // We don't do this in flatpak because SHELL in Flatpak is always // set to /bin/sh. @@ -4476,7 +4565,7 @@ pub fn finalize(self: *Config) !void { self.command = .{ .shell = copy }; // If we don't need the working directory, then we can exit now. - if (!wd_home) break :command; + if (wd != .home) break :command; } else |_| {} } @@ -4487,10 +4576,12 @@ pub fn finalize(self: *Config) !void { self.command = .{ .shell = "cmd.exe" }; } - if (wd_home) { + if (wd == .home) { var buf: [std.fs.max_path_bytes]u8 = undefined; if (try internal_os.home(&buf)) |home| { - self.@"working-directory" = try alloc.dupe(u8, home); + wd = .{ .path = try alloc.dupe(u8, home) }; + } else { + wd = .inherit; } } }, @@ -4505,10 +4596,12 @@ pub fn finalize(self: *Config) !void { } } - if (wd_home) { + if (wd == .home) { if (pw.home) |home| { log.info("default working directory src=passwd value={s}", .{home}); - self.@"working-directory" = home; + wd = .{ .path = home }; + } else { + wd = .inherit; } } @@ -4519,6 +4612,8 @@ pub fn finalize(self: *Config) !void { } } } + try wd.finalize(alloc); + self.@"working-directory" = wd; // Apprt-specific defaults switch (build_config.app_runtime) { @@ -4537,10 +4632,6 @@ pub fn finalize(self: *Config) !void { }, } - // If we have the special value "inherit" then set it to null which - // does the same. In the future we should change to a tagged union. - if (std.mem.eql(u8, wd, "inherit")) self.@"working-directory" = null; - // Default our click interval if (self.@"click-repeat-interval" == 0 and (comptime !builtin.is_test)) @@ -4740,8 +4831,8 @@ fn compatBoldIsBright( _ = alloc; assert(std.mem.eql(u8, key, "bold-is-bright")); - const set = cli.args.parseBool(value_ orelse "t") catch return false; - if (set) { + const isset = cli.args.parseBool(value_ orelse "t") catch return false; + if (isset) { self.@"bold-color" = .bright; } @@ -5136,6 +5227,23 @@ pub const NonNativeFullscreen = enum(c_int) { @"padded-notch", }; +/// Valid values for fullscreen config option +/// c_int because it needs to be extern compatible +/// If this is changed, you must also update ghostty.h +pub const Fullscreen = enum(c_int) { + false, + true, + @"non-native", + @"non-native-visible-menu", + @"non-native-padded-notch", +}; + +pub const WindowPaddingBalance = enum { + false, + true, + equal, +}; + pub const WindowPaddingColor = enum { background, extend, @@ -5153,6 +5261,127 @@ pub const LinkPreviews = enum { osc8, }; +/// See working-directory +pub const WorkingDirectory = union(enum) { + const Self = @This(); + + /// Resolve to the current user's home directory during config finalize. + home, + + /// Inherit the working directory from the launching process. + inherit, + + /// Use an explicit working directory path. This may be not be + /// expanded until finalize is called. + path: []const u8, + + pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void { + var input = input_ orelse return error.ValueRequired; + input = std.mem.trim(u8, input, &std.ascii.whitespace); + if (input.len == 0) return error.ValueRequired; + + // Match path.zig behavior for quoted values. + if (input.len >= 2 and input[0] == '"' and input[input.len - 1] == '"') { + input = input[1 .. input.len - 1]; + } + + if (std.mem.eql(u8, input, "home")) { + self.* = .home; + return; + } + + if (std.mem.eql(u8, input, "inherit")) { + self.* = .inherit; + return; + } + + self.* = .{ .path = try alloc.dupe(u8, input) }; + } + + /// Expand tilde paths in .path values. + pub fn finalize(self: *Self, alloc: Allocator) Allocator.Error!void { + const path = switch (self.*) { + .path => |path| path, + else => return, + }; + + if (!std.mem.startsWith(u8, path, "~/")) return; + + var buf: [std.fs.max_path_bytes]u8 = undefined; + const expanded = internal_os.expandHome(path, &buf) catch |err| { + log.warn( + "error expanding home directory for working-directory path={s}: {}", + .{ path, err }, + ); + return; + }; + + if (std.mem.eql(u8, expanded, path)) return; + self.* = .{ .path = try alloc.dupe(u8, expanded) }; + } + + pub fn value(self: Self) ?[]const u8 { + return switch (self) { + .path => |path| path, + .home, .inherit => null, + }; + } + + pub fn clone(self: Self, alloc: Allocator) Allocator.Error!Self { + return switch (self) { + .path => |path| .{ .path = try alloc.dupe(u8, path) }, + else => self, + }; + } + + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { + switch (self) { + .home, .inherit => try formatter.formatEntry([]const u8, @tagName(self)), + .path => |path| try formatter.formatEntry([]const u8, path), + } + } + + test "WorkingDirectory parseCLI" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var wd: Self = .inherit; + + try wd.parseCLI(alloc, "inherit"); + try testing.expectEqual(.inherit, wd); + + try wd.parseCLI(alloc, "home"); + try testing.expectEqual(.home, wd); + + try wd.parseCLI(alloc, "~/projects/ghostty"); + try testing.expectEqualStrings("~/projects/ghostty", wd.path); + + try wd.parseCLI(alloc, "\"/tmp path\""); + try testing.expectEqualStrings("/tmp path", wd.path); + } + + test "WorkingDirectory finalize" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + { + var wd: Self = .{ .path = "~/projects/ghostty" }; + try wd.finalize(alloc); + + var buf: [std.fs.max_path_bytes]u8 = undefined; + const expected = internal_os.expandHome( + "~/projects/ghostty", + &buf, + ) catch "~/projects/ghostty"; + try testing.expectEqualStrings(expected, wd.value().?); + } + } +}; + /// Color represents a color using RGB. /// /// This is a packed struct so that the C API to read color values just @@ -5578,7 +5807,7 @@ pub const Palette = struct { /// ghostty_config_palette_s pub const C = extern struct { - colors: [265]Color.C, + colors: [256]Color.C, }; pub fn cval(self: Self) Palette.C { @@ -5919,22 +6148,15 @@ pub const SelectionWordChars = struct { pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void { const value = input orelse return error.ValueRequired; - // Parse UTF-8 string into codepoints + // Parse string with Zig escape sequence support into codepoints var list: std.ArrayList(u21) = .empty; defer list.deinit(alloc); // Always include null as first boundary try list.append(alloc, 0); - // Parse the UTF-8 string - const utf8_view = std.unicode.Utf8View.init(value) catch { - // Invalid UTF-8, just use null boundary - self.codepoints = try list.toOwnedSlice(alloc); - return; - }; - - var utf8_it = utf8_view.iterator(); - while (utf8_it.nextCodepoint()) |codepoint| { + var it = string.codepointIterator(value); + while (it.next() catch return error.InvalidValue) |codepoint| { try list.append(alloc, codepoint); } @@ -5987,6 +6209,56 @@ pub const SelectionWordChars = struct { try testing.expectEqual(@as(u21, ';'), chars.codepoints[3]); try testing.expectEqual(@as(u21, ','), chars.codepoints[4]); } + + test "parseCLI escape sequences" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + // \t escape should be parsed as tab + var chars: Self = .{}; + try chars.parseCLI(alloc, " \\t;,"); + + try testing.expectEqual(@as(usize, 5), chars.codepoints.len); + try testing.expectEqual(@as(u21, 0), chars.codepoints[0]); + try testing.expectEqual(@as(u21, ' '), chars.codepoints[1]); + try testing.expectEqual(@as(u21, '\t'), chars.codepoints[2]); + try testing.expectEqual(@as(u21, ';'), chars.codepoints[3]); + try testing.expectEqual(@as(u21, ','), chars.codepoints[4]); + } + + test "parseCLI backslash escape" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + // \\ should be parsed as a single backslash + var chars: Self = .{}; + try chars.parseCLI(alloc, "\\\\;"); + + try testing.expectEqual(@as(usize, 3), chars.codepoints.len); + try testing.expectEqual(@as(u21, 0), chars.codepoints[0]); + try testing.expectEqual(@as(u21, '\\'), chars.codepoints[1]); + try testing.expectEqual(@as(u21, ';'), chars.codepoints[2]); + } + + test "parseCLI unicode escape" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + // \u{2502} should be parsed as │ + var chars: Self = .{}; + try chars.parseCLI(alloc, "\\u{2502};"); + + try testing.expectEqual(@as(usize, 3), chars.codepoints.len); + try testing.expectEqual(@as(u21, 0), chars.codepoints[0]); + try testing.expectEqual(@as(u21, '│'), chars.codepoints[1]); + try testing.expectEqual(@as(u21, ';'), chars.codepoints[2]); + } }; /// FontVariation is a repeatable configuration value that sets a single @@ -6123,6 +6395,15 @@ pub const Keybinds = struct { /// which allows all table names to be available without reservation. tables: std.StringArrayHashMapUnmanaged(inputpkg.Binding.Set) = .empty, + /// The most recent binding target for `chain=` additions. + /// + /// This is intentionally tracked at the Keybinds level so that chains can + /// apply across table boundaries according to parse order. + chain_target: union(enum) { + root, + table: []const u8, + } = .root, + pub fn init(self: *Keybinds, alloc: Allocator) !void { // We don't clear the memory because it's in the arena and unlikely // to be free-able anyways (since arenas can only clear the last @@ -6130,6 +6411,7 @@ pub const Keybinds = struct { // will be freed when the config is freed. self.set = .{}; self.tables = .empty; + self.chain_target = .root; // keybinds for opening and reloading config try self.set.put( @@ -6187,10 +6469,11 @@ pub const Keybinds = struct { .{ .copy_to_clipboard = .mixed }, .{ .performable = true }, ); - try self.set.put( + try self.set.putFlags( alloc, .{ .key = .{ .unicode = 'v' }, .mods = mods }, - .{ .paste_from_clipboard = {} }, + .paste_from_clipboard, + .{ .performable = true }, ); } @@ -6912,6 +7195,7 @@ pub const Keybinds = struct { log.info("config has 'keybind = clear', all keybinds cleared", .{}); self.set = .{}; self.tables = .empty; + self.chain_target = .root; return; } @@ -6949,16 +7233,39 @@ pub const Keybinds = struct { if (binding.len == 0) { log.debug("config has 'keybind = {s}/', table cleared", .{table_name}); gop.value_ptr.* = .{}; + self.chain_target = .root; return; } + // Chains are only allowed at the root level. Their target is + // tracked globally by parse order in `self.chain_target`. + if (std.mem.startsWith(u8, binding, "chain=")) { + return error.InvalidFormat; + } + // Parse and add the binding to the table try gop.value_ptr.parseAndPut(alloc, binding); + self.chain_target = .{ .table = gop.key_ptr.* }; + return; + } + + if (std.mem.startsWith(u8, value, "chain=")) { + switch (self.chain_target) { + .root => try self.set.parseAndPut(alloc, value), + .table => |table_name| { + const table = self.tables.getPtr(table_name) orelse { + self.chain_target = .root; + return error.InvalidFormat; + }; + try table.parseAndPut(alloc, value); + }, + } return; } // Parse into default set try self.set.parseAndPut(alloc, value); + self.chain_target = .root; } /// Deep copy of the struct. Required by Config. @@ -7400,6 +7707,63 @@ pub const Keybinds = struct { try testing.expect(keybinds.tables.contains("mytable")); } + test "parseCLI chain without prior parsed binding is invalid" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + try testing.expectError( + error.InvalidFormat, + keybinds.parseCLI(alloc, "chain=new_tab"), + ); + } + + test "parseCLI table chain syntax is invalid" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + try keybinds.parseCLI(alloc, "foo/a=text:hello"); + try testing.expectError( + error.InvalidFormat, + keybinds.parseCLI(alloc, "foo/chain=deactivate_key_table"), + ); + } + + test "parseCLI chain applies to most recent table binding" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + try keybinds.parseCLI(alloc, "ctrl+n=activate_key_table:foo"); + try keybinds.parseCLI(alloc, "foo/a=text:hello"); + try keybinds.parseCLI(alloc, "chain=deactivate_key_table"); + + const root_entry = keybinds.set.get(.{ + .mods = .{ .ctrl = true }, + .key = .{ .unicode = 'n' }, + }).?.value_ptr.*; + try testing.expect(root_entry == .leaf); + try testing.expect(root_entry.leaf.action == .activate_key_table); + + const foo_entry = keybinds.tables.get("foo").?.get(.{ + .key = .{ .unicode = 'a' }, + }).?.value_ptr.*; + try testing.expect(foo_entry == .leaf_chained); + try testing.expectEqual(@as(usize, 2), foo_entry.leaf_chained.actions.items.len); + try testing.expect(foo_entry.leaf_chained.actions.items[0] == .text); + try testing.expect(foo_entry.leaf_chained.actions.items[1] == .deactivate_key_table); + } + test "clone with tables" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); @@ -10082,6 +10446,26 @@ test "clone preserves conditional set" { try testing.expect(clone1._conditional_set.contains(.theme)); } +test "working-directory expands tilde" { + const testing = std.testing; + const alloc = testing.allocator; + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + var it: TestIterator = .{ .data = &.{ + "--working-directory=~/projects/ghostty", + } }; + try cfg.loadIter(alloc, &it); + try cfg.finalize(); + + var buf: [std.fs.max_path_bytes]u8 = undefined; + const expected = internal_os.expandHome( + "~/projects/ghostty", + &buf, + ) catch "~/projects/ghostty"; + try testing.expectEqualStrings(expected, cfg.@"working-directory".?.value().?); +} + test "changed" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/config/command.zig b/src/config/command.zig index 7e16ad5c70d..7cd70acb317 100644 --- a/src/config/command.zig +++ b/src/config/command.zig @@ -165,6 +165,16 @@ pub const Command = union(enum) { }; } + pub fn deinit(self: *const Self, alloc: Allocator) void { + switch (self.*) { + .shell => |v| alloc.free(v), + .direct => |l| { + for (l) |v| alloc.free(v); + alloc.free(l); + }, + } + } + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { switch (self) { .shell => |v| try formatter.formatEntry([]const u8, v), diff --git a/src/config/path.zig b/src/config/path.zig index ebcd084d2b6..793cf1845ae 100644 --- a/src/config/path.zig +++ b/src/config/path.zig @@ -32,6 +32,20 @@ pub const Path = union(enum) { return std.meta.eql(self, other); } + /// ghostty_config_path_s + pub const C = extern struct { + path: [*:0]const u8, + optional: bool, + }; + + /// Returns the path as a C-compatible struct. + pub fn cval(self: Path) C { + return switch (self) { + .optional => |path| .{ .path = path.ptr, .optional = true }, + .required => |path| .{ .path = path.ptr, .optional = false }, + }; + } + /// Parse the input and return a Path. A leading `?` indicates that the path /// is _optional_ and an error should not be logged or displayed to the user /// if that path does not exist. Otherwise the path is required and an error diff --git a/src/config/string.zig b/src/config/string.zig index 71826f005f2..45079937321 100644 --- a/src/config/string.zig +++ b/src/config/string.zig @@ -36,6 +36,40 @@ pub fn parse(out: []u8, bytes: []const u8) ![]u8 { return out[0..dst_i]; } +/// Creates an iterator that requires no allocation to extract codepoints +/// from the string literal, parsing escape sequences as it goes. +pub fn codepointIterator(bytes: []const u8) CodepointIterator { + return .{ .bytes = bytes, .i = 0 }; +} + +pub const CodepointIterator = struct { + bytes: []const u8, + i: usize, + + pub fn next(self: *CodepointIterator) error{InvalidString}!?u21 { + if (self.i >= self.bytes.len) return null; + switch (self.bytes[self.i]) { + // An escape sequence + '\\' => return switch (std.zig.string_literal.parseEscapeSequence( + self.bytes, + &self.i, + )) { + .failure => error.InvalidString, + .success => |cp| cp, + }, + + // Not an escape, parse as UTF-8 + else => |start| { + const cp_len = std.unicode.utf8ByteSequenceLength(start) catch + return error.InvalidString; + defer self.i += cp_len; + return std.unicode.utf8Decode(self.bytes[self.i..][0..cp_len]) catch + return error.InvalidString; + }, + } + } +}; + test "parse: empty" { const testing = std.testing; @@ -65,3 +99,48 @@ test "parse: escapes" { try testing.expectEqualStrings("hello\u{1F601}world", result); } } + +test "codepointIterator: empty" { + var it = codepointIterator(""); + try std.testing.expectEqual(null, try it.next()); +} + +test "codepointIterator: ascii no escapes" { + var it = codepointIterator("abc"); + try std.testing.expectEqual(@as(u21, 'a'), (try it.next()).?); + try std.testing.expectEqual(@as(u21, 'b'), (try it.next()).?); + try std.testing.expectEqual(@as(u21, 'c'), (try it.next()).?); + try std.testing.expectEqual(null, try it.next()); +} + +test "codepointIterator: multibyte utf8" { + // │ is U+2502 (3 bytes in UTF-8) + var it = codepointIterator("a│b"); + try std.testing.expectEqual(@as(u21, 'a'), (try it.next()).?); + try std.testing.expectEqual(@as(u21, '│'), (try it.next()).?); + try std.testing.expectEqual(@as(u21, 'b'), (try it.next()).?); + try std.testing.expectEqual(null, try it.next()); +} + +test "codepointIterator: escape sequences" { + var it = codepointIterator("a\\tb\\n\\\\"); + try std.testing.expectEqual(@as(u21, 'a'), (try it.next()).?); + try std.testing.expectEqual(@as(u21, '\t'), (try it.next()).?); + try std.testing.expectEqual(@as(u21, 'b'), (try it.next()).?); + try std.testing.expectEqual(@as(u21, '\n'), (try it.next()).?); + try std.testing.expectEqual(@as(u21, '\\'), (try it.next()).?); + try std.testing.expectEqual(null, try it.next()); +} + +test "codepointIterator: unicode escape" { + var it = codepointIterator("\\u{2502}x"); + try std.testing.expectEqual(@as(u21, '│'), (try it.next()).?); + try std.testing.expectEqual(@as(u21, 'x'), (try it.next()).?); + try std.testing.expectEqual(null, try it.next()); +} + +test "codepointIterator: emoji unicode escape" { + var it = codepointIterator("\\u{1F601}"); + try std.testing.expectEqual(@as(u21, 0x1F601), (try it.next()).?); + try std.testing.expectEqual(null, try it.next()); +} diff --git a/src/config/url.zig b/src/config/url.zig index da0892a911b..e7cf8603c68 100644 --- a/src/config/url.zig +++ b/src/config/url.zig @@ -65,11 +65,11 @@ const non_dotted_path_lookahead = ; const dotted_path_space_segments = - \\(?:(?= 0 and target.y >= 0); + assert(target.maxX() <= 1 and target.maxY() <= 1); + switch (direction) { + .left => target.x += 1, + .right => target.x -= 1, + .up => target.y += 1, + .down => target.y -= 1, + } + + return self.nearest( + sp, + from, + direction, + target, + ); + } + /// Resize the given node in place. The node MUST be a split (asserted). /// /// In general, this is an immutable data structure so this is @@ -1974,6 +2014,60 @@ test "SplitTree: spatial goto" { try testing.expectEqualStrings("A", view.label); } + // Spatial A => left (wrapped) + { + const target = (try split.goto( + alloc, + from: { + var it = split.iterator(); + break :from while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "A")) { + break entry.handle; + } + } else return error.NotFound; + }, + .{ .spatial = .left }, + )).?; + const view = split.nodes[target.idx()].leaf; + try testing.expectEqualStrings("B", view.label); + } + + // Spatial B => right (wrapped) + { + const target = (try split.goto( + alloc, + from: { + var it = split.iterator(); + break :from while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "B")) { + break entry.handle; + } + } else return error.NotFound; + }, + .{ .spatial = .right }, + )).?; + const view = split.nodes[target.idx()].leaf; + try testing.expectEqualStrings("A", view.label); + } + + // Spatial C => down (wrapped) + { + const target = (try split.goto( + alloc, + from: { + var it = split.iterator(); + break :from while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "C")) { + break entry.handle; + } + } else return error.NotFound; + }, + .{ .spatial = .down }, + )).?; + const view = split.nodes[target.idx()].leaf; + try testing.expectEqualStrings("A", view.label); + } + // Equalize var equal = try split.equalize(alloc); defer equal.deinit(); diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index ab3c6aaab55..ff7c6d9d347 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -850,7 +850,7 @@ test "run iterator" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("ABCD"); + s.nextSlice("ABCD"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -874,7 +874,7 @@ test "run iterator" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("ABCD EFG"); + s.nextSlice("ABCD EFG"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -897,7 +897,7 @@ test "run iterator" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("A😃D"); + s.nextSlice("A😃D"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -922,7 +922,7 @@ test "run iterator" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(bad); + s.nextSlice(bad); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -955,8 +955,8 @@ test "run iterator: empty cells with background set" { var s = t.vtStream(); defer s.deinit(); // Set red background - try s.nextSlice("\x1b[48;2;255;0;0m"); - try s.nextSlice("A"); + s.nextSlice("\x1b[48;2;255;0;0m"); + s.nextSlice("A"); // Get our first row { @@ -1014,7 +1014,7 @@ test "shape" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); + s.nextSlice(buf[0..buf_idx]); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1053,7 +1053,7 @@ test "shape nerd fonts" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); + s.nextSlice(buf[0..buf_idx]); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1086,7 +1086,7 @@ test "shape inconsolata ligs" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(">="); + s.nextSlice(">="); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1115,7 +1115,7 @@ test "shape inconsolata ligs" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("==="); + s.nextSlice("==="); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1152,7 +1152,7 @@ test "shape monaspace ligs" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("==="); + s.nextSlice("==="); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1190,7 +1190,7 @@ test "shape left-replaced lig in last run" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("!=="); + s.nextSlice("!=="); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1228,7 +1228,7 @@ test "shape left-replaced lig in early run" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("!==X"); + s.nextSlice("!==X"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1263,7 +1263,7 @@ test "shape U+3C9 with JB Mono" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("\u{03C9} foo"); + s.nextSlice("\u{03C9} foo"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1300,7 +1300,7 @@ test "shape emoji width" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("ðŸ‘"); + s.nextSlice("ðŸ‘"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1390,7 +1390,7 @@ test "shape variation selector VS15" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); + s.nextSlice(buf[0..buf_idx]); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1429,7 +1429,7 @@ test "shape variation selector VS16" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); + s.nextSlice(buf[0..buf_idx]); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1463,9 +1463,9 @@ test "shape with empty cells in between" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("A"); - try s.nextSlice("\x1b[5C"); // 5 spaces forward - try s.nextSlice("B"); + s.nextSlice("A"); + s.nextSlice("\x1b[5C"); // 5 spaces forward + s.nextSlice("B"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1510,7 +1510,7 @@ test "shape Combining characters" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); + s.nextSlice(buf[0..buf_idx]); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1560,7 +1560,7 @@ test "shape Devanagari string" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("अपारà¥à¤Ÿà¤®à¥‡à¤‚ट"); + s.nextSlice("अपारà¥à¤Ÿà¤®à¥‡à¤‚ट"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1619,7 +1619,7 @@ test "shape Tai Tham vowels (position differs from advance)" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); + s.nextSlice(buf[0..buf_idx]); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1680,7 +1680,7 @@ test "shape Tai Tham letters (position.y differs from advance)" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); + s.nextSlice(buf[0..buf_idx]); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1740,7 +1740,7 @@ test "shape Javanese ligatures" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); + s.nextSlice(buf[0..buf_idx]); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1803,7 +1803,7 @@ test "shape Chakma vowel sign with ligature (vowel sign renders first)" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); + s.nextSlice(buf[0..buf_idx]); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1874,7 +1874,7 @@ test "shape Bengali ligatures with out of order vowels" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); + s.nextSlice(buf[0..buf_idx]); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1895,7 +1895,7 @@ test "shape Bengali ligatures with out of order vowels" { try testing.expectEqual(@as(u16, 0), cells[0].x); try testing.expectEqual(@as(u16, 0), cells[1].x); // See the giant "We need to reset the `cell_offset`" comment, but here - // we should technically have the rest of these be `x` of 1, but that + // we should technically have the rest of these be `x` of 2, but that // would require going back in the stream to adjust past cells, and // we don't take on that complexity. try testing.expectEqual(@as(u16, 0), cells[2].x); @@ -1929,7 +1929,7 @@ test "shape box glyphs" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); + s.nextSlice(buf[0..buf_idx]); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1967,7 +1967,7 @@ test "shape selection boundary" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("a1b2c3d4e5"); + s.nextSlice("a1b2c3d4e5"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -2072,7 +2072,7 @@ test "shape cursor boundary" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("a1b2c3d4e5"); + s.nextSlice("a1b2c3d4e5"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -2209,7 +2209,7 @@ test "shape cursor boundary and colored emoji" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("ðŸ‘ðŸ¼"); + s.nextSlice("ðŸ‘ðŸ¼"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -2306,7 +2306,7 @@ test "shape cell attribute change" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(">="); + s.nextSlice(">="); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -2332,9 +2332,9 @@ test "shape cell attribute change" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(">"); - try s.nextSlice("\x1b[1m"); // Bold - try s.nextSlice("="); + s.nextSlice(">"); + s.nextSlice("\x1b[1m"); // Bold + s.nextSlice("="); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -2361,11 +2361,11 @@ test "shape cell attribute change" { var s = t.vtStream(); defer s.deinit(); // RGB 1, 2, 3 - try s.nextSlice("\x1b[38;2;1;2;3m"); - try s.nextSlice(">"); + s.nextSlice("\x1b[38;2;1;2;3m"); + s.nextSlice(">"); // RGB 3, 2, 1 - try s.nextSlice("\x1b[38;2;3;2;1m"); - try s.nextSlice("="); + s.nextSlice("\x1b[38;2;3;2;1m"); + s.nextSlice("="); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -2392,11 +2392,11 @@ test "shape cell attribute change" { var s = t.vtStream(); defer s.deinit(); // RGB 1, 2, 3 bg - try s.nextSlice("\x1b[48;2;1;2;3m"); - try s.nextSlice(">"); + s.nextSlice("\x1b[48;2;1;2;3m"); + s.nextSlice(">"); // RGB 3, 2, 1 bg - try s.nextSlice("\x1b[48;2;3;2;1m"); - try s.nextSlice("="); + s.nextSlice("\x1b[48;2;3;2;1m"); + s.nextSlice("="); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -2423,9 +2423,9 @@ test "shape cell attribute change" { var s = t.vtStream(); defer s.deinit(); // RGB 1, 2, 3 bg - try s.nextSlice("\x1b[48;2;1;2;3m"); - try s.nextSlice(">"); - try s.nextSlice("="); + s.nextSlice("\x1b[48;2;1;2;3m"); + s.nextSlice(">"); + s.nextSlice("="); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -2468,7 +2468,7 @@ test "shape high plane sprite font codepoint" { var s = t.vtStream(); defer s.deinit(); // U+1FB70: Vertical One Eighth Block-2 - try s.nextSlice("\u{1FB70}"); + s.nextSlice("\u{1FB70}"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index 946611e797c..d17df4b1e12 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -448,7 +448,7 @@ test "run iterator" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("ABCD"); + s.nextSlice("ABCD"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -472,7 +472,7 @@ test "run iterator" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("ABCD EFG"); + s.nextSlice("ABCD EFG"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -495,7 +495,7 @@ test "run iterator" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("A😃D"); + s.nextSlice("A😃D"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -533,7 +533,7 @@ test "run iterator: empty cells with background set" { var s = t.vtStream(); defer s.deinit(); // Set red background and write A - try s.nextSlice("\x1b[48;2;255;0;0mA"); + s.nextSlice("\x1b[48;2;255;0;0mA"); // Get our first row { @@ -592,7 +592,7 @@ test "shape" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); + s.nextSlice(buf[0..buf_idx]); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -626,7 +626,7 @@ test "shape inconsolata ligs" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(">="); + s.nextSlice(">="); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -655,7 +655,7 @@ test "shape inconsolata ligs" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("==="); + s.nextSlice("==="); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -692,7 +692,7 @@ test "shape monaspace ligs" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("==="); + s.nextSlice("==="); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -732,7 +732,7 @@ test "shape arabic forced LTR" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(@embedFile("testdata/arabic.txt")); + s.nextSlice(@embedFile("testdata/arabic.txt")); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -773,7 +773,7 @@ test "shape emoji width" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("ðŸ‘"); + s.nextSlice("ðŸ‘"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -870,7 +870,7 @@ test "shape variation selector VS15" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); + s.nextSlice(buf[0..buf_idx]); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -911,7 +911,7 @@ test "shape variation selector VS16" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); + s.nextSlice(buf[0..buf_idx]); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -950,9 +950,9 @@ test "shape with empty cells in between" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("A"); - try s.nextSlice("\x1b[5C"); - try s.nextSlice("B"); + s.nextSlice("A"); + s.nextSlice("\x1b[5C"); + s.nextSlice("B"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -997,7 +997,7 @@ test "shape Combining characters" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); + s.nextSlice(buf[0..buf_idx]); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1048,7 +1048,7 @@ test "shape Devanagari string" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("अपारà¥à¤Ÿà¤®à¥‡à¤‚ट"); + s.nextSlice("अपारà¥à¤Ÿà¤®à¥‡à¤‚ट"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1078,67 +1078,70 @@ test "shape Devanagari string" { try testing.expect(try it.next(alloc) == null); } +// This test fails on Linux if you have the "Noto Sans Tai Tham" font installed +// locally. Disabling this test until it can be fixed. test "shape Tai Tham vowels (position differs from advance)" { - // Note that while this test was necessary for CoreText, the old logic was - // working for HarfBuzz. Still we keep it to ensure it has the correct - // behavior. - const testing = std.testing; - const alloc = testing.allocator; - - // We need a font that supports Tai Tham for this to work, if we can't find - // Noto Sans Tai Tham, which is a system font on macOS, we just skip the - // test. - var testdata = testShaperWithDiscoveredFont( - alloc, - "Noto Sans Tai Tham", - ) catch return error.SkipZigTest; - defer testdata.deinit(); - - var buf: [32]u8 = undefined; - var buf_idx: usize = 0; - buf_idx += try std.unicode.utf8Encode(0x1a2F, buf[buf_idx..]); // ᨯ - buf_idx += try std.unicode.utf8Encode(0x1a70, buf[buf_idx..]); // á©° - - // Make a screen with some data - var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); - defer t.deinit(alloc); - - // Enable grapheme clustering - t.modes.set(.grapheme_cluster, true); - - var s = t.vtStream(); - defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); - - var state: terminal.RenderState = .empty; - defer state.deinit(alloc); - try state.update(alloc, &t); - - // Get our run iterator - var shaper = &testdata.shaper; - var it = shaper.runIterator(.{ - .grid = testdata.grid, - .cells = state.row_data.get(0).cells.slice(), - }); - var count: usize = 0; - while (try it.next(alloc)) |run| { - count += 1; - - const cells = try shaper.shape(run); - try testing.expectEqual(@as(usize, 2), cells.len); - try testing.expectEqual(@as(u16, 0), cells[0].x); - try testing.expectEqual(@as(u16, 0), cells[1].x); - - // The first glyph renders in the next cell. We expect the x_offset - // to equal the cell width. However, with FreeType the cell_width is - // computed from ASCII glyphs, and Noto Sans Tai Tham only has the - // space character in ASCII (with a 3px advance), so the cell_width - // metric doesn't match the actual Tai Tham glyph positioning. - const expected_x_offset: i16 = if (comptime font.options.backend.hasFreetype()) 7 else @intCast(run.grid.metrics.cell_width); - try testing.expectEqual(expected_x_offset, cells[0].x_offset); - try testing.expectEqual(@as(i16, 0), cells[1].x_offset); - } - try testing.expectEqual(@as(usize, 1), count); + return error.SkipZigTest; + // // Note that while this test was necessary for CoreText, the old logic was + // // working for HarfBuzz. Still we keep it to ensure it has the correct + // // behavior. + // const testing = std.testing; + // const alloc = testing.allocator; + + // // We need a font that supports Tai Tham for this to work, if we can't find + // // Noto Sans Tai Tham, which is a system font on macOS, we just skip the + // // test. + // var testdata = testShaperWithDiscoveredFont( + // alloc, + // "Noto Sans Tai Tham", + // ) catch return error.SkipZigTest; + // defer testdata.deinit(); + + // var buf: [32]u8 = undefined; + // var buf_idx: usize = 0; + // buf_idx += try std.unicode.utf8Encode(0x1a2F, buf[buf_idx..]); // ᨯ + // buf_idx += try std.unicode.utf8Encode(0x1a70, buf[buf_idx..]); // á©° + + // // Make a screen with some data + // var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + // defer t.deinit(alloc); + + // // Enable grapheme clustering + // t.modes.set(.grapheme_cluster, true); + + // var s = t.vtStream(); + // defer s.deinit(); + // s.nextSlice(buf[0..buf_idx]); + + // var state: terminal.RenderState = .empty; + // defer state.deinit(alloc); + // try state.update(alloc, &t); + + // // Get our run iterator + // var shaper = &testdata.shaper; + // var it = shaper.runIterator(.{ + // .grid = testdata.grid, + // .cells = state.row_data.get(0).cells.slice(), + // }); + // var count: usize = 0; + // while (try it.next(alloc)) |run| { + // count += 1; + + // const cells = try shaper.shape(run); + // try testing.expectEqual(@as(usize, 2), cells.len); + // try testing.expectEqual(@as(u16, 0), cells[0].x); + // try testing.expectEqual(@as(u16, 0), cells[1].x); + + // // The first glyph renders in the next cell. We expect the x_offset + // // to equal the cell width. However, with FreeType the cell_width is + // // computed from ASCII glyphs, and Noto Sans Tai Tham only has the + // // space character in ASCII (with a 3px advance), so the cell_width + // // metric doesn't match the actual Tai Tham glyph positioning. + // const expected_x_offset: i16 = if (comptime font.options.backend.hasFreetype()) 7 else @intCast(run.grid.metrics.cell_width); + // try testing.expectEqual(expected_x_offset, cells[0].x_offset); + // try testing.expectEqual(@as(i16, 0), cells[1].x_offset); + // } + // try testing.expectEqual(@as(usize, 1), count); } test "shape Tibetan characters" { @@ -1167,7 +1170,7 @@ test "shape Tibetan characters" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); + s.nextSlice(buf[0..buf_idx]); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1194,125 +1197,131 @@ test "shape Tibetan characters" { try testing.expectEqual(@as(usize, 1), count); } +// This test fails on Linux if you have the "Noto Sans Tai Tham" font installed +// locally. Disabling this test until it can be fixed. test "shape Tai Tham letters (run_offset.y differs from zero)" { - const testing = std.testing; - const alloc = testing.allocator; - - // We need a font that supports Tai Tham for this to work, if we can't find - // Noto Sans Tai Tham, which is a system font on macOS, we just skip the - // test. - var testdata = testShaperWithDiscoveredFont( - alloc, - "Noto Sans Tai Tham", - ) catch return error.SkipZigTest; - defer testdata.deinit(); - - var buf: [32]u8 = undefined; - var buf_idx: usize = 0; - - // First grapheme cluster: - buf_idx += try std.unicode.utf8Encode(0x1a49, buf[buf_idx..]); // HA - buf_idx += try std.unicode.utf8Encode(0x1a60, buf[buf_idx..]); // SAKOT - // Second grapheme cluster, combining with the first in a ligature: - buf_idx += try std.unicode.utf8Encode(0x1a3f, buf[buf_idx..]); // YA - buf_idx += try std.unicode.utf8Encode(0x1a69, buf[buf_idx..]); // U - - // Make a screen with some data - var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); - defer t.deinit(alloc); - - // Enable grapheme clustering - t.modes.set(.grapheme_cluster, true); - - var s = t.vtStream(); - defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); - - var state: terminal.RenderState = .empty; - defer state.deinit(alloc); - try state.update(alloc, &t); - - // Get our run iterator - var shaper = &testdata.shaper; - var it = shaper.runIterator(.{ - .grid = testdata.grid, - .cells = state.row_data.get(0).cells.slice(), - }); - var count: usize = 0; - while (try it.next(alloc)) |run| { - count += 1; - - const cells = try shaper.shape(run); - try testing.expectEqual(@as(usize, 3), cells.len); - try testing.expectEqual(@as(u16, 0), cells[0].x); - try testing.expectEqual(@as(u16, 0), cells[1].x); - try testing.expectEqual(@as(u16, 0), cells[2].x); // U from second grapheme - - // The U glyph renders at a y below zero - try testing.expectEqual(@as(i16, -3), cells[2].y_offset); - } - try testing.expectEqual(@as(usize, 1), count); + return error.SkipZigTest; + // const testing = std.testing; + // const alloc = testing.allocator; + + // // We need a font that supports Tai Tham for this to work, if we can't find + // // Noto Sans Tai Tham, which is a system font on macOS, we just skip the + // // test. + // var testdata = testShaperWithDiscoveredFont( + // alloc, + // "Noto Sans Tai Tham", + // ) catch return error.SkipZigTest; + // defer testdata.deinit(); + + // var buf: [32]u8 = undefined; + // var buf_idx: usize = 0; + + // // First grapheme cluster: + // buf_idx += try std.unicode.utf8Encode(0x1a49, buf[buf_idx..]); // HA + // buf_idx += try std.unicode.utf8Encode(0x1a60, buf[buf_idx..]); // SAKOT + // // Second grapheme cluster, combining with the first in a ligature: + // buf_idx += try std.unicode.utf8Encode(0x1a3f, buf[buf_idx..]); // YA + // buf_idx += try std.unicode.utf8Encode(0x1a69, buf[buf_idx..]); // U + + // // Make a screen with some data + // var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + // defer t.deinit(alloc); + + // // Enable grapheme clustering + // t.modes.set(.grapheme_cluster, true); + + // var s = t.vtStream(); + // defer s.deinit(); + // s.nextSlice(buf[0..buf_idx]); + + // var state: terminal.RenderState = .empty; + // defer state.deinit(alloc); + // try state.update(alloc, &t); + + // // Get our run iterator + // var shaper = &testdata.shaper; + // var it = shaper.runIterator(.{ + // .grid = testdata.grid, + // .cells = state.row_data.get(0).cells.slice(), + // }); + // var count: usize = 0; + // while (try it.next(alloc)) |run| { + // count += 1; + + // const cells = try shaper.shape(run); + // try testing.expectEqual(@as(usize, 3), cells.len); + // try testing.expectEqual(@as(u16, 0), cells[0].x); + // try testing.expectEqual(@as(u16, 0), cells[1].x); + // try testing.expectEqual(@as(u16, 0), cells[2].x); // U from second grapheme + + // // The U glyph renders at a y below zero + // try testing.expectEqual(@as(i16, -3), cells[2].y_offset); + // } + // try testing.expectEqual(@as(usize, 1), count); } +// This test fails on Linux if you have the "Noto Sans Javanese" font installed +// locally. Disabling this test until it can be fixed. test "shape Javanese ligatures" { - const testing = std.testing; - const alloc = testing.allocator; - - // We need a font that supports Javanese for this to work, if we can't find - // Noto Sans Javanese Regular, which is a system font on macOS, we just - // skip the test. - var testdata = testShaperWithDiscoveredFont( - alloc, - "Noto Sans Javanese", - ) catch return error.SkipZigTest; - defer testdata.deinit(); - - var buf: [32]u8 = undefined; - var buf_idx: usize = 0; - - // First grapheme cluster: - buf_idx += try std.unicode.utf8Encode(0xa9a4, buf[buf_idx..]); // NA - buf_idx += try std.unicode.utf8Encode(0xa9c0, buf[buf_idx..]); // PANGKON - // Second grapheme cluster, combining with the first in a ligature: - buf_idx += try std.unicode.utf8Encode(0xa9b2, buf[buf_idx..]); // HA - buf_idx += try std.unicode.utf8Encode(0xa9b8, buf[buf_idx..]); // Vowel sign SUKU - - // Make a screen with some data - var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); - defer t.deinit(alloc); - - // Enable grapheme clustering - t.modes.set(.grapheme_cluster, true); - - var s = t.vtStream(); - defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); - - var state: terminal.RenderState = .empty; - defer state.deinit(alloc); - try state.update(alloc, &t); - - // Get our run iterator - var shaper = &testdata.shaper; - var it = shaper.runIterator(.{ - .grid = testdata.grid, - .cells = state.row_data.get(0).cells.slice(), - }); - var count: usize = 0; - while (try it.next(alloc)) |run| { - count += 1; - - const cells = try shaper.shape(run); - const cell_width = run.grid.metrics.cell_width; - try testing.expectEqual(@as(usize, 3), cells.len); - try testing.expectEqual(@as(u16, 0), cells[0].x); - try testing.expectEqual(@as(u16, 0), cells[1].x); - try testing.expectEqual(@as(u16, 0), cells[2].x); - - // The vowel sign SUKU renders with correct x_offset - try testing.expect(cells[2].x_offset > 3 * cell_width); - } - try testing.expectEqual(@as(usize, 1), count); + return error.SkipZigTest; + // const testing = std.testing; + // const alloc = testing.allocator; + + // // We need a font that supports Javanese for this to work, if we can't find + // // Noto Sans Javanese Regular, which is a system font on macOS, we just + // // skip the test. + // var testdata = testShaperWithDiscoveredFont( + // alloc, + // "Noto Sans Javanese", + // ) catch return error.SkipZigTest; + // defer testdata.deinit(); + + // var buf: [32]u8 = undefined; + // var buf_idx: usize = 0; + + // // First grapheme cluster: + // buf_idx += try std.unicode.utf8Encode(0xa9a4, buf[buf_idx..]); // NA + // buf_idx += try std.unicode.utf8Encode(0xa9c0, buf[buf_idx..]); // PANGKON + // // Second grapheme cluster, combining with the first in a ligature: + // buf_idx += try std.unicode.utf8Encode(0xa9b2, buf[buf_idx..]); // HA + // buf_idx += try std.unicode.utf8Encode(0xa9b8, buf[buf_idx..]); // Vowel sign SUKU + + // // Make a screen with some data + // var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + // defer t.deinit(alloc); + + // // Enable grapheme clustering + // t.modes.set(.grapheme_cluster, true); + + // var s = t.vtStream(); + // defer s.deinit(); + // s.nextSlice(buf[0..buf_idx]); + + // var state: terminal.RenderState = .empty; + // defer state.deinit(alloc); + // try state.update(alloc, &t); + + // // Get our run iterator + // var shaper = &testdata.shaper; + // var it = shaper.runIterator(.{ + // .grid = testdata.grid, + // .cells = state.row_data.get(0).cells.slice(), + // }); + // var count: usize = 0; + // while (try it.next(alloc)) |run| { + // count += 1; + + // const cells = try shaper.shape(run); + // const cell_width = run.grid.metrics.cell_width; + // try testing.expectEqual(@as(usize, 3), cells.len); + // try testing.expectEqual(@as(u16, 0), cells[0].x); + // try testing.expectEqual(@as(u16, 0), cells[1].x); + // try testing.expectEqual(@as(u16, 0), cells[2].x); + + // // The vowel sign SUKU renders with correct x_offset + // try testing.expect(cells[2].x_offset > 3 * cell_width); + // } + // try testing.expectEqual(@as(usize, 1), count); } test "shape Chakma vowel sign with ligature (vowel sign renders first)" { @@ -1349,7 +1358,7 @@ test "shape Chakma vowel sign with ligature (vowel sign renders first)" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); + s.nextSlice(buf[0..buf_idx]); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1424,7 +1433,7 @@ test "shape Bengali ligatures with out of order vowels" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); + s.nextSlice(buf[0..buf_idx]); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1447,12 +1456,12 @@ test "shape Bengali ligatures with out of order vowels" { // Whereas CoreText puts everything all into the first cell (see the // corresponding test), HarfBuzz splits into two clusters. - try testing.expectEqual(@as(u16, 1), cells[2].x); - try testing.expectEqual(@as(u16, 1), cells[3].x); - try testing.expectEqual(@as(u16, 1), cells[4].x); - try testing.expectEqual(@as(u16, 1), cells[5].x); - try testing.expectEqual(@as(u16, 1), cells[6].x); - try testing.expectEqual(@as(u16, 1), cells[7].x); + try testing.expectEqual(@as(u16, 2), cells[2].x); + try testing.expectEqual(@as(u16, 2), cells[3].x); + try testing.expectEqual(@as(u16, 2), cells[4].x); + try testing.expectEqual(@as(u16, 2), cells[5].x); + try testing.expectEqual(@as(u16, 2), cells[6].x); + try testing.expectEqual(@as(u16, 2), cells[7].x); // The vowel sign E renders before the SSA: try testing.expect(cells[2].x_offset < cells[3].x_offset); @@ -1478,7 +1487,7 @@ test "shape box glyphs" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(buf[0..buf_idx]); + s.nextSlice(buf[0..buf_idx]); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1517,7 +1526,7 @@ test "shape selection boundary" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("a1b2c3d4e5"); + s.nextSlice("a1b2c3d4e5"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1622,7 +1631,7 @@ test "shape cursor boundary" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("a1b2c3d4e5"); + s.nextSlice("a1b2c3d4e5"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1762,7 +1771,7 @@ test "shape cursor boundary and colored emoji" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice("ðŸ‘ðŸ¼"); + s.nextSlice("ðŸ‘ðŸ¼"); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1859,7 +1868,7 @@ test "shape cell attribute change" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(">="); + s.nextSlice(">="); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1885,9 +1894,9 @@ test "shape cell attribute change" { var s = t.vtStream(); defer s.deinit(); - try s.nextSlice(">"); - try s.nextSlice("\x1b[1m"); - try s.nextSlice("="); + s.nextSlice(">"); + s.nextSlice("\x1b[1m"); + s.nextSlice("="); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1914,11 +1923,11 @@ test "shape cell attribute change" { var s = t.vtStream(); defer s.deinit(); // RGB 1, 2, 3 - try s.nextSlice("\x1b[38;2;1;2;3m"); - try s.nextSlice(">"); + s.nextSlice("\x1b[38;2;1;2;3m"); + s.nextSlice(">"); // RGB 3, 2, 1 - try s.nextSlice("\x1b[38;2;3;2;1m"); - try s.nextSlice("="); + s.nextSlice("\x1b[38;2;3;2;1m"); + s.nextSlice("="); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1945,11 +1954,11 @@ test "shape cell attribute change" { var s = t.vtStream(); defer s.deinit(); // RGB 1, 2, 3 bg - try s.nextSlice("\x1b[48;2;1;2;3m"); - try s.nextSlice(">"); + s.nextSlice("\x1b[48;2;1;2;3m"); + s.nextSlice(">"); // RGB 3, 2, 1 bg - try s.nextSlice("\x1b[48;2;3;2;1m"); - try s.nextSlice("="); + s.nextSlice("\x1b[48;2;3;2;1m"); + s.nextSlice("="); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -1976,9 +1985,9 @@ test "shape cell attribute change" { var s = t.vtStream(); defer s.deinit(); // RGB 1, 2, 3 bg - try s.nextSlice("\x1b[48;2;1;2;3m"); - try s.nextSlice(">"); - try s.nextSlice("="); + s.nextSlice("\x1b[48;2;1;2;3m"); + s.nextSlice(">"); + s.nextSlice("="); var state: terminal.RenderState = .empty; defer state.deinit(alloc); diff --git a/src/input.zig b/src/input.zig index bad3ac1f34a..833e0582092 100644 --- a/src/input.zig +++ b/src/input.zig @@ -12,6 +12,7 @@ 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 mouse_encode = @import("input/mouse_encode.zig"); pub const paste = @import("input/paste.zig"); pub const ctrlOrSuper = key.ctrlOrSuper; @@ -25,6 +26,7 @@ pub const KeyEvent = key.KeyEvent; pub const KeyRemapSet = key_mods.RemapSet; pub const InspectorMode = Binding.Action.InspectorMode; pub const Mods = key_mods.Mods; +pub const MouseAction = mouse.Action; pub const MouseButton = mouse.Button; pub const MouseButtonState = mouse.ButtonState; pub const MousePressureStage = mouse.PressureStage; diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 57414d76482..62a4e39acf0 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -570,16 +570,23 @@ pub const Action = union(enum) { toggle_tab_overview, /// Change the title of the current focused surface via a pop-up prompt. - /// - /// This requires libadwaita 1.5 or newer on Linux. The current libadwaita - /// version can be found by running `ghostty +version`. prompt_surface_title, - /// Change the title of the current tab/window via a pop-up prompt. The + /// Change the title of the current tab via a pop-up prompt. The /// title set via this prompt overrides any title set by the terminal /// and persists across focus changes within the tab. prompt_tab_title, + /// Set the title for the current focused surface. + /// + /// If the title is empty, the surface title is reset to an empty title. + set_surface_title: []const u8, + + /// Set the title for the current focused tab. + /// + /// If the title is empty, the tab title override is cleared. + set_tab_title: []const u8, + /// Create a new split in the specified direction. /// /// Valid arguments: @@ -1327,6 +1334,8 @@ pub const Action = union(enum) { .set_font_size, .prompt_surface_title, .prompt_tab_title, + .set_surface_title, + .set_tab_title, .clear_screen, .select_all, .scroll_to_top, @@ -3295,6 +3304,16 @@ test "parse: action with string" { try testing.expect(binding.action == .esc); try testing.expectEqualStrings("A", binding.action.esc); } + { + const binding = try parseSingle("a=set_surface_title:surface"); + try testing.expect(binding.action == .set_surface_title); + try testing.expectEqualStrings("surface", binding.action.set_surface_title); + } + { + const binding = try parseSingle("a=set_tab_title:tab"); + try testing.expect(binding.action == .set_tab_title); + try testing.expectEqualStrings("tab", binding.action.set_tab_title); + } } test "parse: action with enum" { @@ -4560,6 +4579,18 @@ test "action: format" { try testing.expectEqualStrings("text:\\xf0\\x9f\\x91\\xbb", buf.written()); } +test "action: format set title" { + const testing = std.testing; + const alloc = testing.allocator; + + const a: Action = .{ .set_tab_title = "foo bar" }; + + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try a.format(&buf.writer); + try testing.expectEqualStrings("set_tab_title:foo bar", buf.written()); +} + test "set: appendChain with no parent returns error" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/input/command.zig b/src/input/command.zig index d6d2b024783..ac048eec083 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -440,13 +440,13 @@ fn actionCommands(action: Action.Key) []const Command { .prompt_surface_title => comptime &.{.{ .action = .prompt_surface_title, - .title = "Change Terminal Title...", + .title = "Change Terminal Title…", .description = "Prompt for a new title for the current terminal.", }}, .prompt_tab_title => comptime &.{.{ .action = .prompt_tab_title, - .title = "Change Tab Title...", + .title = "Change Tab Title…", .description = "Prompt for a new title for the current tab.", }}, @@ -689,6 +689,8 @@ fn actionCommands(action: Action.Key) []const Command { .esc, .cursor_key, .set_font_size, + .set_surface_title, + .set_tab_title, .search, .scroll_to_row, .scroll_page_fractional, diff --git a/src/input/key_encode.zig b/src/input/key_encode.zig index 3716c226ef2..0373fb5f94c 100644 --- a/src/input/key_encode.zig +++ b/src/input/key_encode.zig @@ -214,7 +214,13 @@ fn kitty( } } - const entry = entry_ orelse return; + const entry = entry_ orelse { + // No entry found. If we have UTF-8 text this is a pure text event + // (e.g. composed/IME text), so send it as-is so programs can + // still receive it. + if (event.utf8.len > 0) return try writer.writeAll(event.utf8); + return; + }; // If this is just a modifier we require "report all" to send the sequence. if (entry.modifier and !opts.kitty_flags.report_all) return; @@ -1443,6 +1449,25 @@ test "kitty: composing with modifier" { try testing.expectEqualStrings("\x1b[57441;2u", writer.buffered()); } +test "kitty: composed text with report all" { + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .unidentified, + .mods = .{}, + .utf8 = "\xc3\xbb", // û + }, .{ + .kitty_flags = .{ + .disambiguate = true, + .report_events = true, + .report_alternates = true, + .report_all = true, + .report_associated = true, + }, + }); + try testing.expectEqualStrings("\xc3\xbb", writer.buffered()); +} + test "kitty: shift+a on US keyboard" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); diff --git a/src/input/mouse.zig b/src/input/mouse.zig index bdf967ed29f..8a769557f39 100644 --- a/src/input/mouse.zig +++ b/src/input/mouse.zig @@ -1,5 +1,11 @@ const std = @import("std"); +/// The type of action associated with a mouse event. This is different +/// from ButtonState because button state is simply the current state +/// of a mouse button but an action is something that triggers via +/// an GUI event and supports more. +pub const Action = enum { press, release, motion }; + /// The state of a mouse button. /// /// This is backed by a c_int so we can use this as-is for our embedding API. diff --git a/src/input/mouse_encode.zig b/src/input/mouse_encode.zig new file mode 100644 index 00000000000..2dfe084fb09 --- /dev/null +++ b/src/input/mouse_encode.zig @@ -0,0 +1,780 @@ +const std = @import("std"); +const testing = std.testing; +const Terminal = @import("../terminal/Terminal.zig"); +const renderer_size = @import("../renderer/size.zig"); +const point = @import("../terminal/point.zig"); +const key = @import("key.zig"); +const mouse = @import("mouse.zig"); + +const log = std.log.scoped(.mouse_encode); + +/// Options that affect mouse encoding behavior and provide runtime context. +pub const Options = struct { + /// Terminal mouse reporting mode (X10, normal, button, any). + event: Terminal.MouseEvent = .none, + + /// Terminal mouse reporting format. + format: Terminal.MouseFormat = .x10, + + /// Full renderer size used to convert surface-space pixel positions + /// into grid cell coordinates (for most formats) and terminal-space + /// pixel coordinates (for SGR-Pixels), as well as to determine + /// whether a position falls outside the visible viewport. + size: renderer_size.Size, + + /// Whether any mouse button is currently pressed. When a motion + /// event occurs outside the viewport, it is only reported if a + /// button is held down and the event mode supports motion tracking. + /// Without this, out-of-viewport motions are silently dropped. + /// + /// This should reflect the state of the current event as well, so + /// if the encoded event is a button press, this should be true. + any_button_pressed: bool = false, + + /// Last reported viewport cell for motion deduplication. + /// If null, motion deduplication state is not tracked. + last_cell: ?*?point.Coordinate = null, + + /// Initialize from terminal and renderer state. The caller may still + /// set any_button_pressed and last_cell on the returned value. + pub fn fromTerminal( + t: *const Terminal, + size: renderer_size.Size, + ) Options { + return .{ + .event = t.flags.mouse_event, + .format = t.flags.mouse_format, + .size = size, + }; + } +}; + +/// A normalized mouse event for protocol encoding. +pub const Event = struct { + /// The action of this mouse event. + action: mouse.Action = .press, + + /// The button involved in this event. This can be null in the + /// case of a motion action with no pressed buttons. + button: ?mouse.Button = null, + + /// Keyboard modifiers held during this event. + mods: key.Mods = .{}, + + /// Mouse position in terminal-space pixels, with (0, 0) at the top-left + /// of the terminal. Negative values are allowed and indicate positions + /// above or to the left of the terminal. Values larger than the terminal + /// size are also allowed and indicate right or below the terminal. + pos: Pos = .{}, + + /// Mouse position in surface-space pixels. + pub const Pos = struct { + x: f32 = 0, + y: f32 = 0, + }; +}; + +/// Encode the mouse event to the writer according to the options. +/// +/// Not all events result in output. +pub fn encode( + writer: *std.Io.Writer, + event: Event, + opts: Options, +) std.Io.Writer.Error!void { + if (!shouldReport(event, opts)) return; + + // Handle scenarios where the mouse position is outside the viewport. + // We always report release events no matter where they happen. + if (event.action != .release and + posOutOfViewport(event.pos, opts.size)) + { + // If we don't have a motion-tracking event mode, do nothing, + // because events outside the viewport are never reported in + // such cases. + if (!opts.event.motion()) return; + + // For motion modes, we only report if a button is currently pressed. + // This lets a TUI detect a click over the surface + drag out + // of the surface. + if (!opts.any_button_pressed) return; + } + + const cell = posToCell(event.pos, opts.size); + + // We only send motion events when the cell changed unless + // we're tracking raw pixels. + if (event.action == .motion and opts.format != .sgr_pixels) { + if (opts.last_cell) |last| { + if (last.*) |last_cell| { + if (last_cell.eql(cell)) return; + } + } + } + + // Update the last reported cell if we are tracking it. + if (opts.last_cell) |last| last.* = cell; + + const button_code = buttonCode(event, opts) orelse return; + switch (opts.format) { + .x10 => { + if (cell.x > 222 or cell.y > 222) { + log.info("X10 mouse format can only encode X/Y up to 223", .{}); + return; + } + + // + 1 because our x/y are zero-indexed and the protocol uses 1-indexing. + try writer.writeAll("\x1B[M"); + try writer.writeByte(32 + button_code); + try writer.writeByte(32 + @as(u8, @intCast(cell.x)) + 1); + try writer.writeByte(32 + @as(u8, @intCast(cell.y)) + 1); + }, + + .utf8 => { + try writer.writeAll("\x1B[M"); + + // The button code always fits in a single byte. + try writer.writeByte(32 + button_code); + + var buf: [4]u8 = undefined; + const x_cp: u21 = @intCast(@as(u32, cell.x) + 33); + const y_cp: u21 = @intCast(cell.y + 33); + + const x_len = std.unicode.utf8Encode(x_cp, &buf) catch unreachable; + try writer.writeAll(buf[0..x_len]); + + const y_len = std.unicode.utf8Encode(y_cp, &buf) catch unreachable; + try writer.writeAll(buf[0..y_len]); + }, + + .sgr => try writer.print("\x1B[<{d};{d};{d}{c}", .{ + button_code, + cell.x + 1, + cell.y + 1, + @as(u8, if (event.action == .release) 'm' else 'M'), + }), + + .urxvt => try writer.print("\x1B[{d};{d};{d}M", .{ + 32 + button_code, + cell.x + 1, + cell.y + 1, + }), + + .sgr_pixels => { + const pixels = posToPixels(event.pos, opts.size); + try writer.print("\x1B[<{d};{d};{d}{c}", .{ + button_code, + pixels.x, + pixels.y, + @as(u8, if (event.action == .release) 'm' else 'M'), + }); + }, + } +} + +/// Returns true if this event should be reported for the given mouse +/// event mode. +fn shouldReport(event: Event, opts: Options) bool { + return switch (opts.event) { + .none => false, + + // X10 only reports button presses of left, middle, and right. + .x10 => event.action == .press and + event.button != null and + (event.button.? == .left or + event.button.? == .middle or + event.button.? == .right), + + // Normal mode does not report motion. + .normal => event.action != .motion, + + // Button mode requires an active button for motion events. + .button => event.button != null, + + // Any mode reports everything. + .any => true, + }; +} + +fn buttonCode(event: Event, opts: Options) ?u8 { + var acc: u8 = code: { + if (event.button == null) { + // Null button means motion with no pressed button. + break :code 3; + } + + if (event.action == .release and + opts.format != .sgr and + opts.format != .sgr_pixels) + { + // Legacy releases are always encoded as button 3. + break :code 3; + } + + break :code switch (event.button.?) { + .left => 0, + .middle => 1, + .right => 2, + .four => 64, + .five => 65, + .six => 66, + .seven => 67, + .eight => 128, + .nine => 129, + else => return null, + }; + }; + + // X10 does not include modifiers. + if (opts.event != .x10) { + if (event.mods.shift) acc += 4; + if (event.mods.alt) acc += 8; + if (event.mods.ctrl) acc += 16; + } + + // Motion adds another bit. + if (event.action == .motion) acc += 32; + + return acc; +} + +/// Terminal-space pixel position for SGR pixel reporting. +const PixelPoint = struct { + x: i32, + y: i32, +}; + +/// Returns true if the surface-space pixel position is outside the +/// visible viewport bounds (negative or beyond screen dimensions). +fn posOutOfViewport(pos: Event.Pos, size: renderer_size.Size) bool { + const max_x: f32 = @floatFromInt(size.screen.width); + const max_y: f32 = @floatFromInt(size.screen.height); + return pos.x < 0 or pos.y < 0 or pos.x > max_x or pos.y > max_y; +} + +/// Converts a surface-space pixel position to a zero-based grid cell +/// coordinate (column, row) within the terminal viewport. Out-of-bounds +/// values are clamped to the valid grid range (0 to columns/rows - 1). +fn posToCell(pos: Event.Pos, size: renderer_size.Size) point.Coordinate { + const coord: renderer_size.Coordinate = .{ .surface = .{ + .x = @as(f64, @floatCast(pos.x)), + .y = @as(f64, @floatCast(pos.y)), + } }; + const grid = coord.convert(.grid, size).grid; + return .{ .x = grid.x, .y = grid.y }; +} + +/// Converts a surface-space pixel position to terminal-space pixel +/// coordinates (accounting for padding/scaling) used by SGR-Pixels mode. +/// Unlike grid conversion, terminal-space coordinates are not clamped +/// and may be negative or exceed the terminal dimensions. +fn posToPixels(pos: Event.Pos, size: renderer_size.Size) PixelPoint { + const coord: renderer_size.Coordinate.Terminal = (renderer_size.Coordinate{ .surface = .{ + .x = @as(f64, @floatCast(pos.x)), + .y = @as(f64, @floatCast(pos.y)), + } }).convert(.terminal, size).terminal; + + return .{ + .x = @as(i32, @intFromFloat(@round(coord.x))), + .y = @as(i32, @intFromFloat(@round(coord.y))), + }; +} + +fn testSize() renderer_size.Size { + return .{ + .screen = .{ .width = 1_000, .height = 1_000 }, + .cell = .{ .width = 1, .height = 1 }, + .padding = .{}, + }; +} + +test "shouldReport: none mode never reports" { + const size = testSize(); + inline for ([_]mouse.Action{ .press, .release, .motion }) |action| { + try testing.expect(!shouldReport(.{ + .button = .left, + .action = action, + }, .{ .event = .none, .size = size })); + } +} + +test "shouldReport: x10 reports only left/middle/right press" { + const size = testSize(); + // Left, middle, right presses should report. + inline for ([_]mouse.Button{ .left, .middle, .right }) |btn| { + try testing.expect(shouldReport(.{ + .button = btn, + .action = .press, + }, .{ .event = .x10, .size = size })); + } + + // Release is not reported. + try testing.expect(!shouldReport(.{ + .button = .left, + .action = .release, + }, .{ .event = .x10, .size = size })); + + // Motion is not reported. + try testing.expect(!shouldReport(.{ + .button = .left, + .action = .motion, + }, .{ .event = .x10, .size = size })); + + // Other buttons are not reported. + try testing.expect(!shouldReport(.{ + .button = .four, + .action = .press, + }, .{ .event = .x10, .size = size })); + + // Null button is not reported. + try testing.expect(!shouldReport(.{ + .button = null, + .action = .press, + }, .{ .event = .x10, .size = size })); +} + +test "shouldReport: normal reports press and release but not motion" { + const size = testSize(); + try testing.expect(shouldReport(.{ + .button = .left, + .action = .press, + }, .{ .event = .normal, .size = size })); + + try testing.expect(shouldReport(.{ + .button = .left, + .action = .release, + }, .{ .event = .normal, .size = size })); + + try testing.expect(!shouldReport(.{ + .button = .left, + .action = .motion, + }, .{ .event = .normal, .size = size })); +} + +test "shouldReport: button mode requires a button" { + const size = testSize(); + // With a button, all actions report. + inline for ([_]mouse.Action{ .press, .release, .motion }) |action| { + try testing.expect(shouldReport(.{ + .button = .left, + .action = action, + }, .{ .event = .button, .size = size })); + } + + // Without a button (null), nothing reports. + inline for ([_]mouse.Action{ .press, .release, .motion }) |action| { + try testing.expect(!shouldReport(.{ + .button = null, + .action = action, + }, .{ .event = .button, .size = size })); + } +} + +test "shouldReport: any mode reports everything" { + const size = testSize(); + inline for ([_]mouse.Action{ .press, .release, .motion }) |action| { + try testing.expect(shouldReport(.{ + .button = .left, + .action = action, + }, .{ .event = .any, .size = size })); + } + + // Even null button + motion reports. + try testing.expect(shouldReport(.{ + .button = null, + .action = .motion, + }, .{ .event = .any, .size = size })); +} + +test "x10 press left" { + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + var last: ?point.Coordinate = null; + try encode(&writer, .{ + .button = .left, + .action = .press, + .mods = .{ .shift = true, .alt = true, .ctrl = true }, + .pos = .{ .x = 0, .y = 0 }, + }, .{ + .event = .x10, + .format = .x10, + .size = testSize(), + .last_cell = &last, + }); + + try testing.expectEqualSlices(u8, &.{ + 0x1B, + '[', + 'M', + 32, + 33, + 33, + }, writer.buffered()); +} + +test "x10 ignores release" { + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + var last: ?point.Coordinate = null; + try encode(&writer, .{ + .button = .left, + .action = .release, + }, .{ + .event = .x10, + .format = .x10, + .size = testSize(), + .last_cell = &last, + }); + + try testing.expectEqual(@as(usize, 0), writer.buffered().len); +} + +test "normal ignores motion" { + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + var last: ?point.Coordinate = null; + try encode(&writer, .{ + .button = .left, + .action = .motion, + }, .{ + .event = .normal, + .format = .sgr, + .size = testSize(), + .last_cell = &last, + }); + + try testing.expectEqual(@as(usize, 0), writer.buffered().len); +} + +test "button mode requires button" { + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + var last: ?point.Coordinate = null; + try encode(&writer, .{ + .button = null, + .action = .motion, + }, .{ + .event = .button, + .format = .sgr, + .size = testSize(), + .last_cell = &last, + }); + + try testing.expectEqual(@as(usize, 0), writer.buffered().len); +} + +test "sgr release keeps button identity" { + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + var last: ?point.Coordinate = null; + try encode(&writer, .{ + .button = .right, + .action = .release, + .pos = .{ .x = 4, .y = 5 }, + }, .{ + .event = .any, + .format = .sgr, + .size = testSize(), + .last_cell = &last, + }); + + try testing.expectEqualStrings("\x1B[<2;5;6m", writer.buffered()); +} + +test "sgr motion with no button" { + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + var last: ?point.Coordinate = null; + try encode(&writer, .{ + .button = null, + .action = .motion, + .pos = .{ .x = 1, .y = 2 }, + }, .{ + .event = .any, + .format = .sgr, + .size = testSize(), + .last_cell = &last, + }); + + try testing.expectEqualStrings("\x1B[<35;2;3M", writer.buffered()); +} + +test "urxvt with modifiers" { + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + var last: ?point.Coordinate = null; + try encode(&writer, .{ + .button = .left, + .action = .press, + .mods = .{ .shift = true, .alt = true, .ctrl = true }, + .pos = .{ .x = 2, .y = 3 }, + }, .{ + .event = .any, + .format = .urxvt, + .size = testSize(), + .last_cell = &last, + }); + + try testing.expectEqualStrings("\x1B[60;3;4M", writer.buffered()); +} + +test "utf8 encodes large coordinates" { + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + var last: ?point.Coordinate = null; + try encode(&writer, .{ + .button = .left, + .action = .press, + .pos = .{ .x = 300, .y = 400 }, + }, .{ + .event = .any, + .format = .utf8, + .size = testSize(), + .last_cell = &last, + }); + + const out = writer.buffered(); + try testing.expectEqualSlices(u8, &.{ 0x1B, '[', 'M', 32 }, out[0..4]); + + const view = try std.unicode.Utf8View.init(out[4..]); + var it = view.iterator(); + try testing.expectEqual(@as(u21, 333), it.nextCodepoint().?); + try testing.expectEqual(@as(u21, 433), it.nextCodepoint().?); + try testing.expectEqual(@as(?u21, null), it.nextCodepoint()); +} + +test "x10 coordinate limit" { + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + var last: ?point.Coordinate = null; + try encode(&writer, .{ + .button = .left, + .action = .press, + .pos = .{ .x = 223, .y = 0 }, + }, .{ + .event = .x10, + .format = .x10, + .size = testSize(), + .last_cell = &last, + }); + + try testing.expectEqual(@as(usize, 0), writer.buffered().len); +} + +test "sgr wheel button mappings" { + const Case = struct { + button: mouse.Button, + code: u8, + }; + + inline for ([_]Case{ + .{ .button = .four, .code = 64 }, + .{ .button = .five, .code = 65 }, + .{ .button = .six, .code = 66 }, + .{ .button = .seven, .code = 67 }, + }) |c| { + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + var last: ?point.Coordinate = null; + try encode(&writer, .{ + .button = c.button, + .action = .press, + .pos = .{ .x = 0, .y = 0 }, + }, .{ + .event = .any, + .format = .sgr, + .size = testSize(), + .last_cell = &last, + }); + + var expected: [32]u8 = undefined; + const want = try std.fmt.bufPrint(&expected, "\x1B[<{d};1;1M", .{c.code}); + try testing.expectEqualStrings(want, writer.buffered()); + } +} + +test "urxvt release uses legacy button 3 encoding" { + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + var last: ?point.Coordinate = null; + try encode(&writer, .{ + .button = .right, + .action = .release, + .pos = .{ .x = 2, .y = 3 }, + }, .{ + .event = .any, + .format = .urxvt, + .size = testSize(), + .last_cell = &last, + }); + + try testing.expectEqualStrings("\x1B[35;3;4M", writer.buffered()); +} + +test "unsupported button is ignored" { + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + var last: ?point.Coordinate = null; + try encode(&writer, .{ + .button = .ten, + .action = .press, + .pos = .{ .x = 1, .y = 1 }, + }, .{ + .event = .any, + .format = .sgr, + .size = testSize(), + .last_cell = &last, + }); + + try testing.expectEqual(@as(usize, 0), writer.buffered().len); +} + +test "sgr pixels uses terminal-space cursor coordinates" { + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + var last: ?point.Coordinate = null; + try encode(&writer, .{ + .button = .left, + .action = .press, + .pos = .{ .x = 10, .y = 20 }, + }, .{ + .event = .any, + .format = .sgr_pixels, + .size = testSize(), + .last_cell = &last, + }); + + try testing.expectEqualStrings("\x1B[<0;10;20M", writer.buffered()); +} + +test "sgr pixels release keeps button identity" { + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + var last: ?point.Coordinate = null; + try encode(&writer, .{ + .button = .right, + .action = .release, + .pos = .{ .x = 10, .y = 20 }, + }, .{ + .event = .any, + .format = .sgr_pixels, + .size = testSize(), + .last_cell = &last, + }); + + try testing.expectEqualStrings("\x1B[<2;10;20m", writer.buffered()); +} + +test "position exactly at viewport boundary is encoded in final cell" { + const size: renderer_size.Size = .{ + .screen = .{ .width = 10, .height = 10 }, + .cell = .{ .width = 2, .height = 2 }, + .padding = .{}, + }; + + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + var last: ?point.Coordinate = null; + try encode(&writer, .{ + .button = .left, + .action = .press, + .pos = .{ .x = 10, .y = 10 }, + }, .{ + .event = .any, + .format = .sgr, + .size = size, + .last_cell = &last, + }); + + try testing.expectEqualStrings("\x1B[<0;5;5M", writer.buffered()); +} + +test "outside viewport motion with no pressed button is ignored" { + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + var last: ?point.Coordinate = null; + try encode(&writer, .{ + .button = .left, + .action = .motion, + .pos = .{ .x = -1, .y = -1 }, + }, .{ + .event = .any, + .format = .sgr, + .size = testSize(), + .any_button_pressed = false, + .last_cell = &last, + }); + + try testing.expectEqual(@as(usize, 0), writer.buffered().len); +} + +test "outside viewport motion with pressed button is reported" { + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + var last: ?point.Coordinate = null; + try encode(&writer, .{ + .button = .left, + .action = .motion, + .pos = .{ .x = -1, .y = -1 }, + }, .{ + .event = .any, + .format = .sgr, + .size = testSize(), + .any_button_pressed = true, + .last_cell = &last, + }); + + try testing.expectEqualStrings("\x1B[<32;1;1M", writer.buffered()); +} + +test "motion is deduped by last cell except sgr pixels" { + var last: ?point.Coordinate = null; + + { + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + try encode(&writer, .{ + .button = .left, + .action = .motion, + .pos = .{ .x = 5, .y = 6 }, + }, .{ + .event = .any, + .format = .sgr, + .size = testSize(), + .last_cell = &last, + }); + try testing.expect(writer.buffered().len > 0); + } + + { + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + try encode(&writer, .{ + .button = .left, + .action = .motion, + .pos = .{ .x = 5, .y = 6 }, + }, .{ + .event = .any, + .format = .sgr, + .size = testSize(), + .last_cell = &last, + }); + try testing.expectEqual(@as(usize, 0), writer.buffered().len); + } + + { + var data: [32]u8 = undefined; + var writer: std.Io.Writer = .fixed(&data); + try encode(&writer, .{ + .button = .left, + .action = .motion, + .pos = .{ .x = 5, .y = 6 }, + }, .{ + .event = .any, + .format = .sgr_pixels, + .size = testSize(), + .last_cell = &last, + }); + try testing.expect(writer.buffered().len > 0); + } +} diff --git a/src/inspector/widgets/termio.zig b/src/inspector/widgets/termio.zig index a6c8f60814a..b721d94220d 100644 --- a/src/inspector/widgets/termio.zig +++ b/src/inspector/widgets/termio.zig @@ -54,7 +54,7 @@ pub const Stream = struct { .events = &self.events, }; defer self.parser_stream.handler.state = null; - try self.parser_stream.nextSlice(data); + self.parser_stream.nextSlice(data); } pub fn draw( @@ -736,7 +736,7 @@ const VTHandler = struct { self: *VTHandler, comptime action: VTHandler.Stream.Action.Tag, value: VTHandler.Stream.Action.Value(action), - ) !void { + ) void { _ = self; _ = value; } diff --git a/src/lib/enum.zig b/src/lib/enum.zig index 6fc75984613..bdec2ab88e6 100644 --- a/src/lib/enum.zig +++ b/src/lib/enum.zig @@ -91,3 +91,55 @@ test "abi by removing a key" { try testing.expectEqual(2, @intFromEnum(T.d)); } } + +/// Verify that for every key in enum T, there is a matching declaration in +/// `ghostty.h` with the correct value. This should only ever be called inside a `test` +/// because the `ghostty.h` module is only available then. +pub fn checkGhosttyHEnum(comptime T: type, comptime prefix: []const u8) !void { + const info = @typeInfo(T); + + try std.testing.expect(info == .@"enum"); + try std.testing.expect(info.@"enum".tag_type == c_int); + try std.testing.expect(info.@"enum".is_exhaustive == true); + + @setEvalBranchQuota(1000000); + + const c = @import("ghostty.h"); + + var set: std.EnumSet(T) = .initFull(); + + const c_decls = @typeInfo(c).@"struct".decls; + const enum_fields = info.@"enum".fields; + + inline for (enum_fields) |field| { + const upper_name = comptime u: { + var buf: [128]u8 = undefined; + break :u std.ascii.upperString(&buf, field.name); + }; + + inline for (c_decls) |decl| { + if (!comptime std.mem.startsWith(u8, decl.name, prefix)) continue; + + const suffix = decl.name[prefix.len..]; + + if (!comptime std.mem.eql(u8, suffix, upper_name)) continue; + + std.testing.expectEqual(field.value, @field(c, decl.name)) catch |e| { + std.log.err(@typeName(T) ++ " key " ++ field.name ++ " does not have the same backing int as " ++ decl.name, .{}); + return e; + }; + + set.remove(@enumFromInt(field.value)); + } + } + + std.testing.expect(set.count() == 0) catch |e| { + var it = set.iterator(); + while (it.next()) |v| { + var buf: [128]u8 = undefined; + const upper_string = std.ascii.upperString(&buf, @tagName(v)); + std.log.err("ghostty.h is missing value for {s}{s}", .{ prefix, upper_string }); + } + return e; + }; +} diff --git a/src/lib/main.zig b/src/lib/main.zig index 5a626b1e885..89c6f6c47f5 100644 --- a/src/lib/main.zig +++ b/src/lib/main.zig @@ -5,10 +5,12 @@ const unionpkg = @import("union.zig"); pub const allocator = @import("allocator.zig"); pub const Enum = enumpkg.Enum; +pub const checkGhosttyHEnum = enumpkg.checkGhosttyHEnum; pub const String = types.String; pub const Struct = @import("struct.zig").Struct; pub const Target = @import("target.zig").Target; pub const TaggedUnion = unionpkg.TaggedUnion; +pub const cutPrefix = @import("string.zig").cutPrefix; test { std.testing.refAllDecls(@This()); diff --git a/src/lib/string.zig b/src/lib/string.zig new file mode 100644 index 00000000000..795823c25fd --- /dev/null +++ b/src/lib/string.zig @@ -0,0 +1,15 @@ +const std = @import("std"); + +// This is a copy of std.mem.cutPrefix from 0.16. Once Ghostty has been ported +// to 0.16 this can be removed. + +/// If slice starts with prefix, returns the rest of slice starting at +/// prefix.len. +pub fn cutPrefix(comptime T: type, slice: []const T, prefix: []const T) ?[]const T { + return if (std.mem.startsWith(T, slice, prefix)) slice[prefix.len..] else null; +} + +test cutPrefix { + try std.testing.expectEqualStrings("foo", cutPrefix(u8, "--example=foo", "--example=").?); + try std.testing.expectEqual(null, cutPrefix(u8, "--example=foo", "-example=")); +} diff --git a/src/lib/struct.zig b/src/lib/struct.zig index d494da2e692..134f6bebcc4 100644 --- a/src/lib/struct.zig +++ b/src/lib/struct.zig @@ -1,6 +1,17 @@ const std = @import("std"); +const testing = std.testing; const Target = @import("target.zig").Target; +/// Create a struct type that is C ABI compatible from a Zig struct type. +/// +/// When the target is `.zig`, the original struct type is returned as-is. +/// When the target is `.c`, the struct is recreated with an `extern` layout, +/// ensuring a stable, C-compatible memory layout. +/// +/// This handles packed structs by resolving zero alignments to the natural +/// alignment of each field's type, since extern structs require explicit +/// alignment. This means packed struct fields like `bool` will take up +/// their full size (1 byte) rather than being bit-packed. pub fn Struct( comptime target: Target, comptime Zig: type, @@ -16,7 +27,7 @@ pub fn Struct( .type = field.type, .default_value_ptr = field.default_value_ptr, .is_comptime = field.is_comptime, - .alignment = field.alignment, + .alignment = if (field.alignment > 0) field.alignment else @alignOf(field.type), }; } @@ -29,3 +40,20 @@ pub fn Struct( }, }; } + +test "packed struct converts to extern with full-size bools" { + const Packed = packed struct { + flag1: bool, + flag2: bool, + value: u8, + }; + + const C = Struct(.c, Packed); + const info = @typeInfo(C).@"struct"; + + try testing.expectEqual(.@"extern", info.layout); + try testing.expectEqual(@as(usize, 1), @sizeOf(@FieldType(C, "flag1"))); + try testing.expectEqual(@as(usize, 1), @sizeOf(@FieldType(C, "flag2"))); + try testing.expectEqual(@as(usize, 1), @sizeOf(@FieldType(C, "value"))); + try testing.expectEqual(@as(usize, 3), @sizeOf(C)); +} diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 03a883e20f4..0fa808b5906 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -32,6 +32,7 @@ pub const modes = terminal.modes; pub const page = terminal.page; pub const parse_table = terminal.parse_table; pub const search = terminal.search; +pub const sgr = terminal.sgr; pub const size = terminal.size; pub const x11_color = terminal.x11_color; @@ -58,6 +59,8 @@ pub const Style = terminal.Style; pub const Terminal = terminal.Terminal; pub const Stream = terminal.Stream; pub const StreamAction = terminal.StreamAction; +pub const ReadonlyStream = terminal.ReadonlyStream; +pub const ReadonlyHandler = terminal.ReadonlyHandler; pub const Cursor = Screen.Cursor; pub const CursorStyle = Screen.CursorStyle; pub const CursorStyleReq = terminal.CursorStyle; @@ -121,6 +124,7 @@ comptime { @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_setopt_from_terminal, .{ .name = "ghostty_key_encoder_setopt_from_terminal" }); @export(&c.key_encoder_encode, .{ .name = "ghostty_key_encoder_encode" }); @export(&c.osc_new, .{ .name = "ghostty_osc_new" }); @export(&c.osc_free, .{ .name = "ghostty_osc_free" }); @@ -140,6 +144,16 @@ comptime { @export(&c.sgr_unknown_partial, .{ .name = "ghostty_sgr_unknown_partial" }); @export(&c.sgr_attribute_tag, .{ .name = "ghostty_sgr_attribute_tag" }); @export(&c.sgr_attribute_value, .{ .name = "ghostty_sgr_attribute_value" }); + @export(&c.formatter_terminal_new, .{ .name = "ghostty_formatter_terminal_new" }); + @export(&c.formatter_format_buf, .{ .name = "ghostty_formatter_format_buf" }); + @export(&c.formatter_format_alloc, .{ .name = "ghostty_formatter_format_alloc" }); + @export(&c.formatter_free, .{ .name = "ghostty_formatter_free" }); + @export(&c.terminal_new, .{ .name = "ghostty_terminal_new" }); + @export(&c.terminal_free, .{ .name = "ghostty_terminal_free" }); + @export(&c.terminal_reset, .{ .name = "ghostty_terminal_reset" }); + @export(&c.terminal_resize, .{ .name = "ghostty_terminal_resize" }); + @export(&c.terminal_vt_write, .{ .name = "ghostty_terminal_vt_write" }); + @export(&c.terminal_scroll_viewport, .{ .name = "ghostty_terminal_scroll_viewport" }); // On Wasm we need to export our allocator convenience functions. if (builtin.target.cpu.arch.isWasm()) { diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index 531a0646134..b81297fce4d 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -72,7 +72,9 @@ pub fn main() !MainReturn { } if (comptime build_config.app_runtime == .none) { - const stdout = std.io.getStdOut().writer(); + var stdout_buf: [1024]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&stdout_buf); + const stdout = &stdout_writer.interface; try stdout.print("Usage: ghostty + [flags]\n\n", .{}); try stdout.print( \\This is the Ghostty helper CLI that accompanies the graphical Ghostty app. @@ -89,6 +91,7 @@ pub fn main() !MainReturn { , .{}, ); + try stdout.flush(); posix.exit(0); } diff --git a/src/os/homedir.zig b/src/os/homedir.zig index 0868a4fa5ef..14a4558cc1a 100644 --- a/src/os/homedir.zig +++ b/src/os/homedir.zig @@ -13,7 +13,7 @@ const Error = error{ /// is generally an expensive process so the value should be cached. pub inline fn home(buf: []u8) !?[]const u8 { return switch (builtin.os.tag) { - inline .linux, .freebsd, .macos => try homeUnix(buf), + .linux, .freebsd, .macos => try homeUnix(buf), .windows => try homeWindows(buf), // iOS doesn't have a user-writable home directory @@ -122,7 +122,13 @@ pub const ExpandError = error{ pub fn expandHome(path: []const u8, buf: []u8) ExpandError![]const u8 { return switch (builtin.os.tag) { .linux, .freebsd, .macos => try expandHomeUnix(path, buf), + + // `~/` is not an idiom generally used on Windows + .windows => return path, + + // iOS doesn't have a user-writable home directory .ios => return path, + else => @compileError("unimplemented"), }; } diff --git a/src/os/hostname.zig b/src/os/hostname.zig index f728a24551e..af9148fbf76 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const posix = std.posix; pub const LocalHostnameValidationError = error{ @@ -99,9 +100,21 @@ pub fn isLocal(hostname: []const u8) LocalHostnameValidationError!bool { if (std.mem.eql(u8, "localhost", hostname)) return true; // If hostname is not "localhost" it must match our hostname. - var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const ourHostname = try posix.gethostname(&buf); - return std.mem.eql(u8, hostname, ourHostname); + switch (builtin.os.tag) { + .windows => { + const windows = @import("windows.zig"); + var buf: [256:0]u8 = undefined; + var nSize: windows.DWORD = buf.len; + if (windows.exp.kernel32.GetComputerNameA(&buf, &nSize) == 0) return false; + const ourHostname = buf[0..nSize]; + return std.mem.eql(u8, hostname, ourHostname); + }, + else => { + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const ourHostname = try posix.gethostname(&buf); + return std.mem.eql(u8, hostname, ourHostname); + }, + } } test "isLocal returns true when provided hostname is localhost" { @@ -109,9 +122,21 @@ test "isLocal returns true when provided hostname is localhost" { } 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 isLocal(localHostname)); + switch (builtin.os.tag) { + .windows => { + const windows = @import("windows.zig"); + var buf: [256:0]u8 = undefined; + var nSize: windows.DWORD = buf.len; + if (windows.exp.kernel32.GetComputerNameA(&buf, &nSize) == 0) return error.GetComputerNameFailed; + const localHostname = buf[0..nSize]; + try std.testing.expect(try isLocal(localHostname)); + }, + else => { + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const localHostname = try posix.gethostname(&buf); + try std.testing.expect(try isLocal(localHostname)); + }, + } } test "isLocal returns false when hostname is not local" { diff --git a/src/os/i18n_locales.zig b/src/os/i18n_locales.zig index 48efbaf2890..a044bfa4835 100644 --- a/src/os/i18n_locales.zig +++ b/src/os/i18n_locales.zig @@ -28,30 +28,33 @@ /// we don't have a good way to determine this. We can always reorder /// with some data. pub const locales = [_][:0]const u8{ - "zh_CN.UTF-8", - "de_DE.UTF-8", - "fr_FR.UTF-8", - "ja_JP.UTF-8", - "nl_NL.UTF-8", - "nb_NO.UTF-8", - "ru_RU.UTF-8", - "uk_UA.UTF-8", - "pl_PL.UTF-8", - "ko_KR.UTF-8", - "mk_MK.UTF-8", - "tr_TR.UTF-8", - "id_ID.UTF-8", - "es_BO.UTF-8", - "es_AR.UTF-8", - "pt_BR.UTF-8", - "ca_ES.UTF-8", - "it_IT.UTF-8", - "bg_BG.UTF-8", - "ga_IE.UTF-8", - "hu_HU.UTF-8", - "he_IL.UTF-8", - "zh_TW.UTF-8", - "hr_HR.UTF-8", - "lt_LT.UTF-8", - "lv_LV.UTF-8", + "zh_CN", + "de", + "fr", + "ja", + "nl", + "nb", + "ru", + "uk", + "pl", + "ko_KR", + "mk", + "tr", + "id", + "es_BO", + "es_AR", + "es_ES", + "pt_BR", + "ca", + "it", + "bg", + "ga", + "hu", + "he", + "zh_TW", + "hr", + "lt", + "lv", + "vi", + "kk", }; diff --git a/src/os/open.zig b/src/os/open.zig index 28d1c23ee51..0cead55521b 100644 --- a/src/os/open.zig +++ b/src/os/open.zig @@ -1,6 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; +const build_config = @import("../build_config.zig"); const apprt = @import("../apprt.zig"); const log = std.log.scoped(.@"os-open"); @@ -48,6 +49,17 @@ pub fn open( exe.stdout_behavior = .Pipe; exe.stderr_behavior = .Pipe; + // In the snap on Linux the launcher exports LD_LIBRARY_PATH pointing at + // the snap's bundled libraries. Leaking this into child process can + // can be problematic, so let's drop it from the env + var snap_env: std.process.EnvMap = if (comptime build_config.snap) blk: { + var env = try std.process.getEnvMap(alloc); + env.remove("LD_LIBRARY_PATH"); + break :blk env; + } else undefined; + defer if (comptime build_config.snap) snap_env.deinit(); + if (comptime build_config.snap) exe.env_map = &snap_env; + // Spawn the process on our same thread so we can detect failure // quickly. try exe.spawn(); diff --git a/src/os/string_encoding.zig b/src/os/string_encoding.zig index 042001ea75e..875a5bc17e4 100644 --- a/src/os/string_encoding.zig +++ b/src/os/string_encoding.zig @@ -1,35 +1,30 @@ 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: { +/// Decode date from the buffer that has been encoded in the same way that +/// `bash`'s `printf %q` encodes a string and write it to the writer. If an +/// error is returned garbage may have been written to the buffer. +pub fn printfQDecode(writer: *std.Io.Writer, buf: []const u8) (std.Io.Writer.Error || error{DecodeError})!void { + const data: []const 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]; + break :data buf[2 .. buf.len - 1]; } // 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[1 .. buf.len - 1]; } break :data buf; }; var src: usize = 0; - var dst: usize = 0; while (src < data.len) { switch (data[src]) { else => { - data[dst] = data[src]; + try writer.writeByte(data[src]); src += 1; - dst += 1; }, '\\' => { if (src + 1 >= data.len) return error.DecodeError; @@ -40,132 +35,141 @@ pub fn printfQDecode(buf: [:0]u8) error{DecodeError}![:0]const u8 { '\'', '$', => |c| { - data[dst] = c; + try writer.writeByte(c); src += 2; - dst += 1; }, 'e' => { - data[dst] = std.ascii.control_code.esc; + try writer.writeByte(std.ascii.control_code.esc); src += 2; - dst += 1; }, 'n' => { - data[dst] = std.ascii.control_code.lf; + try writer.writeByte(std.ascii.control_code.lf); src += 2; - dst += 1; }, 'r' => { - data[dst] = std.ascii.control_code.cr; + try writer.writeByte(std.ascii.control_code.cr); src += 2; - dst += 1; }, 't' => { - data[dst] = std.ascii.control_code.ht; + try writer.writeByte(std.ascii.control_code.ht); src += 2; - dst += 1; }, 'v' => { - data[dst] = std.ascii.control_code.vt; + try writer.writeByte(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); + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: []const u8 = "bobr\\ kurwa"; + + try printfQDecode(&w.writer, s); + try std.testing.expectEqualStrings("bobr kurwa", w.written()); } test "printf_q 2" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + 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); + + try printfQDecode(&w.writer, s); + try std.testing.expectEqualStrings("bobr\nkurwa", w.written()); } test "printf_q 3" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + 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)); + + try std.testing.expectError(error.DecodeError, printfQDecode(&w.writer, s)); } test "printf_q 4" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + 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)); + + try std.testing.expectError(error.DecodeError, printfQDecode(&w.writer, s)); } test "printf_q 5" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + 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); + + try printfQDecode(&w.writer, s); + try std.testing.expectEqualStrings("bobr kurwa", w.written()); } test "printf_q 6" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + 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); + + try printfQDecode(&w.writer, s); + try std.testing.expectEqualStrings("bobr kurwa", w.written()); } test "printf_q 7" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + 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)); + + try std.testing.expectError(error.DecodeError, printfQDecode(&w.writer, s)); } test "printf_q 8" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); const s: [:0]const u8 = "$'"; var src: [s.len:0]u8 = undefined; @memcpy(&src, s); - try std.testing.expectError(error.DecodeError, printfQDecode(&src)); + try std.testing.expectError(error.DecodeError, printfQDecode(&w.writer, s)); } test "printf_q 9" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); 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)); + try std.testing.expectError(error.DecodeError, printfQDecode(&w.writer, s)); } test "printf_q 10" { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + const s: [:0]const u8 = "'"; var src: [s.len:0]u8 = undefined; @memcpy(&src, s); - try std.testing.expectError(error.DecodeError, printfQDecode(&src)); + try std.testing.expectError(error.DecodeError, printfQDecode(&w.writer, s)); } -/// 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 { +/// Decode data from the buffer that has been URL percent encoded and write +/// it to the given buffer. If an error is returned the garbage may have been +/// written to the writer. +pub fn urlPercentDecode(writer: *std.Io.Writer, buf: []const u8) (std.Io.Writer.Error || error{DecodeError})!void { var src: usize = 0; - var dst: usize = 0; while (src < buf.len) { switch (buf[src]) { else => { - buf[dst] = buf[src]; + try writer.writeByte(buf[src]); src += 1; - dst += 1; }, '%' => { if (src + 2 >= buf.len) return error.DecodeError; @@ -173,9 +177,8 @@ pub fn urlPercentDecode(buf: [:0]u8) error{DecodeError}![:0]const u8 { '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]); + try writer.writeByte(std.math.shl(u8, hex(buf[src + 1]), 4) | hex(buf[src + 2])); src += 3; - dst += 1; }, else => return error.DecodeError, } @@ -185,8 +188,6 @@ pub fn urlPercentDecode(buf: [:0]u8) error{DecodeError}![:0]const u8 { }, } } - buf[dst] = 0; - return buf[0..dst :0]; } inline fn hex(c: u8) u4 { @@ -200,70 +201,96 @@ inline fn hex(c: u8) u4 { test "singles percent" { for (0..255) |c| { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + var buf_: [4]u8 = undefined; const buf = try std.fmt.bufPrintZ(&buf_, "%{x:0>2}", .{c}); - const decoded = try urlPercentDecode(buf); + + try urlPercentDecode(&w.writer, buf); + const decoded = w.written(); + try std.testing.expectEqual(1, decoded.len); try std.testing.expectEqual(c, decoded[0]); } for (0..255) |c| { + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + var buf_: [4]u8 = undefined; const buf = try std.fmt.bufPrintZ(&buf_, "%{X:0>2}", .{c}); - const decoded = try urlPercentDecode(buf); + + try urlPercentDecode(&w.writer, buf); + const decoded = w.written(); + 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); + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: []const u8 = "bobr%20kurwa"; + + try urlPercentDecode(&w.writer, s); + try std.testing.expectEqualStrings("bobr kurwa", w.written()); } 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)); + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: []const u8 = "bobr%2kurwa"; + + try std.testing.expectError(error.DecodeError, urlPercentDecode(&w.writer, s)); } 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)); + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: []const u8 = "bobr%kurwa"; + + try std.testing.expectError(error.DecodeError, urlPercentDecode(&w.writer, s)); } 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)); + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: []const u8 = "bobr%%kurwa"; + + try std.testing.expectError(error.DecodeError, urlPercentDecode(&w.writer, s)); } 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); + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: []const u8 = "bobr%20kurwa%20"; + + try urlPercentDecode(&w.writer, s); + try std.testing.expectEqualStrings("bobr kurwa ", w.written()); } 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)); + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: []const u8 = "bobr%20kurwa%2"; + + try std.testing.expectError(error.DecodeError, urlPercentDecode(&w.writer, s)); } 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)); + var w: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer w.deinit(); + + const s: []const u8 = "bobr%20kurwa%"; + + try std.testing.expectError(error.DecodeError, urlPercentDecode(&w.writer, s)); } /// Is the given character valid in URI percent encoding? diff --git a/src/os/windows.zig b/src/os/windows.zig index 1853f416269..e92a545374e 100644 --- a/src/os/windows.zig +++ b/src/os/windows.zig @@ -53,22 +53,22 @@ pub const exp = struct { hWritePipe: *windows.HANDLE, lpPipeAttributes: ?*const windows.SECURITY_ATTRIBUTES, nSize: windows.DWORD, - ) callconv(windows.WINAPI) windows.BOOL; + ) callconv(.winapi) windows.BOOL; pub extern "kernel32" fn CreatePseudoConsole( size: windows.COORD, hInput: windows.HANDLE, hOutput: windows.HANDLE, dwFlags: windows.DWORD, phPC: *HPCON, - ) callconv(windows.WINAPI) windows.HRESULT; - pub extern "kernel32" fn ResizePseudoConsole(hPC: HPCON, size: windows.COORD) callconv(windows.WINAPI) windows.HRESULT; - pub extern "kernel32" fn ClosePseudoConsole(hPC: HPCON) callconv(windows.WINAPI) void; + ) callconv(.winapi) windows.HRESULT; + pub extern "kernel32" fn ResizePseudoConsole(hPC: HPCON, size: windows.COORD) callconv(.winapi) windows.HRESULT; + pub extern "kernel32" fn ClosePseudoConsole(hPC: HPCON) callconv(.winapi) void; pub extern "kernel32" fn InitializeProcThreadAttributeList( lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST, dwAttributeCount: windows.DWORD, dwFlags: windows.DWORD, lpSize: *windows.SIZE_T, - ) callconv(windows.WINAPI) windows.BOOL; + ) callconv(.winapi) windows.BOOL; pub extern "kernel32" fn UpdateProcThreadAttribute( lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST, dwFlags: windows.DWORD, @@ -77,7 +77,7 @@ pub const exp = struct { cbSize: windows.SIZE_T, lpPreviousValue: ?windows.PVOID, lpReturnSize: ?*windows.SIZE_T, - ) callconv(windows.WINAPI) windows.BOOL; + ) callconv(.winapi) windows.BOOL; pub extern "kernel32" fn PeekNamedPipe( hNamedPipe: windows.HANDLE, lpBuffer: ?windows.LPVOID, @@ -85,7 +85,7 @@ pub const exp = struct { lpBytesRead: ?*windows.DWORD, lpTotalBytesAvail: ?*windows.DWORD, lpBytesLeftThisMessage: ?*windows.DWORD, - ) callconv(windows.WINAPI) windows.BOOL; + ) callconv(.winapi) windows.BOOL; // Duplicated here because lpCommandLine is not marked optional in zig std pub extern "kernel32" fn CreateProcessW( lpApplicationName: ?windows.LPWSTR, @@ -98,7 +98,12 @@ pub const exp = struct { lpCurrentDirectory: ?windows.LPWSTR, lpStartupInfo: *windows.STARTUPINFOW, lpProcessInformation: *windows.PROCESS_INFORMATION, - ) callconv(windows.WINAPI) windows.BOOL; + ) callconv(.winapi) windows.BOOL; + /// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getcomputernamea + pub extern "kernel32" fn GetComputerNameA( + lpBuffer: windows.LPSTR, + nSize: *windows.DWORD, + ) callconv(.winapi) windows.BOOL; }; pub const PROC_THREAD_ATTRIBUTE_NUMBER = 0x0000FFFF; diff --git a/src/renderer.zig b/src/renderer.zig index 9b5164e918f..747556847af 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -31,6 +31,7 @@ pub const ScreenSize = size.ScreenSize; pub const GridSize = size.GridSize; pub const Padding = size.Padding; pub const cursorStyle = cursor.style; +pub const lib = @import("lib/main.zig"); /// The implementation to use for the renderer. This is comptime chosen /// so that every build has exactly one renderer implementation. @@ -44,8 +45,12 @@ pub const Renderer = switch (build_config.renderer) { /// renderers even if some states aren't reachable so that our API users /// can use the same enum for all renderers. pub const Health = enum(c_int) { - healthy = 0, - unhealthy = 1, + healthy, + unhealthy, + + test "ghostty.h Health" { + try lib.checkGhosttyHEnum(Health, "GHOSTTY_RENDERER_HEALTH_"); + } }; test { diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 6c7432d2100..a3ba15c2248 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -17,6 +17,7 @@ const shadertoy = @import("shadertoy.zig"); const mtl = @import("metal/api.zig"); const IOSurfaceLayer = @import("metal/IOSurfaceLayer.zig"); +const IOSurface = macos.iosurface.IOSurface; pub const GraphicsAPI = Metal; pub const Target = @import("metal/Target.zig"); @@ -61,6 +62,13 @@ max_texture_size: u32, /// We start an AutoreleasePool before `drawFrame` and end it afterwards. autorelease_pool: ?*objc.AutoreleasePool = null, +/// Keep a retained reference to the last presented IOSurface so we can +/// re-present it during resize/display callbacks without drawing a new frame. +/// +/// This prevents transient blank frames when CA requests a synchronous display +/// before the terminal has rebuilt its cell buffers for the new size. +last_surface: ?*IOSurface = null, + pub fn init(alloc: Allocator, opts: rendererpkg.Options) !Metal { comptime switch (builtin.os.tag) { .macos, .ios => {}, @@ -152,10 +160,12 @@ pub fn init(alloc: Allocator, opts: rendererpkg.Options) !Metal { .blending = opts.config.blending, .default_storage_mode = default_storage_mode, .max_texture_size = max_texture_size, + .last_surface = null, }; } pub fn deinit(self: *Metal) void { + if (self.last_surface) |s| s.release(); self.queue.release(); self.device.release(); self.layer.release(); @@ -251,6 +261,14 @@ pub fn initTarget(self: *const Metal, width: usize, height: usize) !Target { /// Present the provided target. pub inline fn present(self: *Metal, target: Target, sync: bool) !void { + // Most of the time we want top-left gravity to avoid stretching/jank. + self.layer.layer.setProperty("contentsGravity", macos.animation.kCAGravityTopLeft); + + // Track the last presented surface so `presentLastTarget` can re-present it. + if (self.last_surface) |s| s.release(); + target.surface.retain(); + self.last_surface = target.surface; + if (sync) { self.layer.setSurfaceSync(target.surface); } else { @@ -258,9 +276,24 @@ pub inline fn present(self: *Metal, target: Target, sync: bool) !void { } } -/// Present the last presented target again. (noop for Metal) +/// Present the last presented target again. pub inline fn presentLastTarget(self: *Metal) !void { - _ = self; + const surface = self.last_surface orelse return; + + // Keep top-left gravity during resize replay so stale surfaces never stretch. + // Newly exposed regions use the layer background until a correctly sized frame arrives. + self.layer.layer.setProperty("contentsGravity", macos.animation.kCAGravityTopLeft); + + // Prefer synchronous set on the main thread (matches the CA display callback + // path). If we're off-main-thread, use the async helper which marshals to the + // main queue to avoid visual artifacts. + const NSThread = objc.getClass("NSThread").?; + if (NSThread.msgSend(bool, "isMainThread", .{})) { + self.layer.setSurfaceSync(surface); + } else { + // During resize replay off the main thread, keep non-stretch sampling. + try self.layer.setSurfaceUnchecked(surface); + } } /// Returns the options to use when constructing buffers. diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 5087213797d..8ce77acb533 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -360,11 +360,6 @@ fn drainMailbox(self: *Thread) !void { // Visibility affects our QoS class self.setQosClass(); - // If we became visible then we immediately trigger a draw. - // We don't need to update frame data because that should - // still be happening. - if (v) self.drawFrame(false); - // Notify the renderer so it can update any state. self.renderer.setVisible(v); diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index 5ea5b7ab0db..196ebb1757f 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -528,7 +528,7 @@ test "Cell constraint widths" { // symbol->nothing: 2 { t.fullReset(); - try s.nextSlice(""); + s.nextSlice(""); try state.update(alloc, &t); try testing.expectEqual(2, constraintWidth( state.row_data.get(0).cells.items(.raw), @@ -540,7 +540,7 @@ test "Cell constraint widths" { // symbol->character: 1 { t.fullReset(); - try s.nextSlice("z"); + s.nextSlice("z"); try state.update(alloc, &t); try testing.expectEqual(1, constraintWidth( state.row_data.get(0).cells.items(.raw), @@ -552,7 +552,7 @@ test "Cell constraint widths" { // symbol->space: 2 { t.fullReset(); - try s.nextSlice(" z"); + s.nextSlice(" z"); try state.update(alloc, &t); try testing.expectEqual(2, constraintWidth( state.row_data.get(0).cells.items(.raw), @@ -563,7 +563,7 @@ test "Cell constraint widths" { // symbol->no-break space: 1 { t.fullReset(); - try s.nextSlice("\u{00a0}z"); + s.nextSlice("\u{00a0}z"); try state.update(alloc, &t); try testing.expectEqual(1, constraintWidth( state.row_data.get(0).cells.items(.raw), @@ -575,7 +575,7 @@ test "Cell constraint widths" { // symbol->end of row: 1 { t.fullReset(); - try s.nextSlice(" "); + s.nextSlice(" "); try state.update(alloc, &t); try testing.expectEqual(1, constraintWidth( state.row_data.get(0).cells.items(.raw), @@ -587,7 +587,7 @@ test "Cell constraint widths" { // character->symbol: 2 { t.fullReset(); - try s.nextSlice("z"); + s.nextSlice("z"); try state.update(alloc, &t); try testing.expectEqual(2, constraintWidth( state.row_data.get(0).cells.items(.raw), @@ -599,7 +599,7 @@ test "Cell constraint widths" { // symbol->symbol: 1,1 { t.fullReset(); - try s.nextSlice(""); + s.nextSlice(""); try state.update(alloc, &t); try testing.expectEqual(1, constraintWidth( state.row_data.get(0).cells.items(.raw), @@ -616,7 +616,7 @@ test "Cell constraint widths" { // symbol->space->symbol: 2,2 { t.fullReset(); - try s.nextSlice(" "); + s.nextSlice(" "); try state.update(alloc, &t); try testing.expectEqual(2, constraintWidth( state.row_data.get(0).cells.items(.raw), @@ -633,7 +633,7 @@ test "Cell constraint widths" { // symbol->powerline: 1 (dedicated test because powerline is special-cased in cellpkg) { t.fullReset(); - try s.nextSlice(""); + s.nextSlice(""); try state.update(alloc, &t); try testing.expectEqual(1, constraintWidth( state.row_data.get(0).cells.items(.raw), @@ -645,7 +645,7 @@ test "Cell constraint widths" { // powerline->symbol: 2 (dedicated test because powerline is special-cased in cellpkg) { t.fullReset(); - try s.nextSlice(""); + s.nextSlice(""); try state.update(alloc, &t); try testing.expectEqual(2, constraintWidth( state.row_data.get(0).cells.items(.raw), @@ -657,7 +657,7 @@ test "Cell constraint widths" { // powerline->nothing: 2 (dedicated test because powerline is special-cased in cellpkg) { t.fullReset(); - try s.nextSlice(""); + s.nextSlice(""); try state.update(alloc, &t); try testing.expectEqual(2, constraintWidth( state.row_data.get(0).cells.items(.raw), @@ -669,7 +669,7 @@ test "Cell constraint widths" { // powerline->space: 2 (dedicated test because powerline is special-cased in cellpkg) { t.fullReset(); - try s.nextSlice(" z"); + s.nextSlice(" z"); try state.update(alloc, &t); try testing.expectEqual(2, constraintWidth( state.row_data.get(0).cells.items(.raw), diff --git a/src/renderer/cursor.zig b/src/renderer/cursor.zig index bfa92f31db3..33992bc5559 100644 --- a/src/renderer/cursor.zig +++ b/src/renderer/cursor.zig @@ -15,7 +15,7 @@ pub const Style = enum { lock, /// Create a cursor style from the terminal style request. - pub fn fromTerminal(term: terminal.CursorStyle) ?Style { + pub fn fromTerminal(term: terminal.CursorStyle) Style { return switch (term) { .bar => .bar, .block => .block, @@ -143,7 +143,7 @@ test "cursor: always block with preedit" { // If we're scrolled though, then we don't show the cursor. for (0..100) |_| try term.index(); - try term.scrollViewport(.{ .top = {} }); + term.scrollViewport(.{ .top = {} }); try state.update(alloc, &term); // In any bool state diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 19c7b33751e..e0d8a4dd67a 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -125,6 +125,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type { scrollbar: terminal.Scrollbar, scrollbar_dirty: bool, + /// Tracks the last bottom-right pin of the screen to detect new output. + /// When the final line changes (node or y differs), new content was added. + /// Used for scroll-to-bottom on output feature. + last_bottom_node: ?usize, + last_bottom_y: terminal.size.CellCountInt, + /// The most recent viewport matches so that we can render search /// matches in the visible frame. This is provided asynchronously /// from the search thread so we have the dirty flag to also note @@ -146,6 +152,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// cells for the draw call. cells_rebuilt: bool = false, + /// The current GPU uniform values. uniforms: shaderpkg.Uniforms, @@ -196,6 +203,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// Health of the most recently completed frame. health: std.atomic.Value(Health) = .{ .raw = .healthy }, + /// True once we've successfully presented at least one frame. Used to + /// safely re-present the last target during synchronous resize callbacks + /// without risking an initial blank window. + has_presented: std.atomic.Value(bool) = .{ .raw = false }, + /// Our swap chain (multiple buffering) swap_chain: SwapChain, @@ -563,6 +575,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { colorspace: configpkg.Config.WindowColorspace, blending: configpkg.Config.AlphaBlending, background_blur: configpkg.Config.BackgroundBlur, + scroll_to_bottom_on_output: bool, pub fn init( alloc_gpa: Allocator, @@ -636,6 +649,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .colorspace = config.@"window-colorspace", .blending = config.@"alpha-blending", .background_blur = config.@"background-blur", + .scroll_to_bottom_on_output = config.@"scroll-to-bottom".output, .arena = arena, }; } @@ -699,6 +713,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .focused = true, .scrollbar = .zero, .scrollbar_dirty = false, + .last_bottom_node = null, + .last_bottom_y = 0, .search_matches = null, .search_selected_match = null, .search_matches_dirty = false, @@ -746,6 +762,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .previous_cursor = @splat(0), .current_cursor_color = @splat(0), .previous_cursor_color = @splat(0), + .current_cursor_style = 0, + .previous_cursor_style = 0, + .cursor_visible = 0, .cursor_change_time = 0, .time_focus = 0, .focus = 1, // assume focused initially @@ -990,9 +1009,19 @@ pub fn Renderer(comptime GraphicsAPI: type) type { if (comptime DisplayLink == void) return; const display_link = self.display_link orelse return; log.info("updating display link display id={}", .{id}); + const was_running = display_link.isRunning(); display_link.setCurrentCGDisplay(id) catch |err| { log.warn("error setting display link display id err={}", .{err}); + return; }; + + // CVDisplayLink can silently "run" without ever delivering callbacks if it + // was started before a valid current display was set. Restarting it after + // we successfully set the display fixes the stuck-vsync-no-frames state. + if (was_running and self.focused) { + display_link.stop() catch {}; + display_link.start() catch {}; + } } /// True if our renderer has animations so that a higher frequency @@ -1166,6 +1195,26 @@ pub fn Renderer(comptime GraphicsAPI: type) type { return; } + // If scroll-to-bottom on output is enabled, check if the final line + // changed by comparing the bottom-right pin. If the node pointer or + // y offset changed, new content was added to the screen. + // Update this BEFORE we update our render state so we can + // draw the new scrolled data immediately. + if (self.config.scroll_to_bottom_on_output) scroll: { + const br = state.terminal.screens.active.pages.getBottomRight(.screen) orelse break :scroll; + + // If the pin hasn't changed, then don't scroll. + if (self.last_bottom_node == @intFromPtr(br.node) and + self.last_bottom_y == br.y) break :scroll; + + // Update tracked pin state for next frame + self.last_bottom_node = @intFromPtr(br.node); + self.last_bottom_y = br.y; + + // Scroll + state.terminal.scrollViewport(.bottom); + } + // Update our terminal state try self.terminal_state.update(self.alloc, state.terminal); @@ -1196,6 +1245,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // kitty state on every frame because any cell change can move // an image. if (self.images.kittyRequiresUpdate(state.terminal)) { + // We need to grab the draw mutex since this updates + // our image state that drawFrame uses. + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); self.images.kittyUpdate( self.alloc, state.terminal, @@ -1323,6 +1376,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self.draw_mutex.lock(); defer self.draw_mutex.unlock(); + _ = self.terminal_state.rows; + _ = self.terminal_state.cols; + // Build our GPU cells self.rebuildCells( critical.preedit, @@ -1435,6 +1491,40 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self.size.screen.width != surface_size.width or self.size.screen.height != surface_size.height; + // On macOS, CoreAnimation can synchronously request a display during bounds changes. + // If we resize our render target and present a freshly cleared surface before the IO + // thread delivers the new terminal state/cell buffers, we can show a single-frame + // blank flash. To avoid this, satisfy the synchronous display by re-presenting the + // last completed frame and let the normal render loop catch up on the next tick. + if (sync and size_changed and self.has_presented.load(.monotonic)) { + try self.api.presentLastTarget(); + return; + } + + // During resize/layout transitions, the platform can trigger draws before the IO + // thread has delivered the corresponding terminal resize (and thus before updateFrame + // has rebuilt GPU cell buffers for the new grid). If we draw in that window we can + // render nothing but background (visually blank) because the projection/padding math + // uses a stale `cells.size` that doesn't match the new screen size. + // + // Detect this by computing the expected grid for the current surface size and + // comparing it to the currently rebuilt cell buffer grid. If they don't match, keep + // the last presented frame on-screen until the new cells arrive. + if (size_changed) { + const expected_grid = (renderer.Size{ + .screen = .{ .width = surface_size.width, .height = surface_size.height }, + .cell = self.size.cell, + .padding = self.size.padding, + }).grid(); + + if (expected_grid.columns != self.cells.size.columns or + expected_grid.rows != self.cells.size.rows) + { + try self.api.presentLastTarget(); + return; + } + } + // Conditions under which we need to draw the frame, otherwise we // don't need to since the previous frame should be identical. const needs_redraw = @@ -1702,6 +1792,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Always release our semaphore self.swap_chain.releaseFrame(); + + if (health == .healthy) { + // Track that we have a last good frame to re-present during sync resize callbacks. + self.has_presented.store(true, .monotonic); + } } /// Call this any time the background image path changes. @@ -1977,11 +2072,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Only update when terminal state is dirty. if (self.terminal_state.dirty == .false) return; + const uniforms: *shadertoy.Uniforms = &self.custom_shader_uniforms; const colors: *const terminal.RenderState.Colors = &self.terminal_state.colors; // 256-color palette for (colors.palette, 0..) |color, i| { - self.custom_shader_uniforms.palette[i] = .{ + uniforms.palette[i] = .{ @as(f32, @floatFromInt(color.r)) / 255.0, @as(f32, @floatFromInt(color.g)) / 255.0, @as(f32, @floatFromInt(color.b)) / 255.0, @@ -1990,7 +2086,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } // Background color - self.custom_shader_uniforms.background_color = .{ + uniforms.background_color = .{ @as(f32, @floatFromInt(colors.background.r)) / 255.0, @as(f32, @floatFromInt(colors.background.g)) / 255.0, @as(f32, @floatFromInt(colors.background.b)) / 255.0, @@ -1998,7 +2094,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { }; // Foreground color - self.custom_shader_uniforms.foreground_color = .{ + uniforms.foreground_color = .{ @as(f32, @floatFromInt(colors.foreground.r)) / 255.0, @as(f32, @floatFromInt(colors.foreground.g)) / 255.0, @as(f32, @floatFromInt(colors.foreground.b)) / 255.0, @@ -2007,7 +2103,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Cursor color if (colors.cursor) |cursor_color| { - self.custom_shader_uniforms.cursor_color = .{ + uniforms.cursor_color = .{ @as(f32, @floatFromInt(cursor_color.r)) / 255.0, @as(f32, @floatFromInt(cursor_color.g)) / 255.0, @as(f32, @floatFromInt(cursor_color.b)) / 255.0, @@ -2021,7 +2117,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Cursor text color if (self.config.cursor_text) |cursor_text| { - self.custom_shader_uniforms.cursor_text = .{ + uniforms.cursor_text = .{ @as(f32, @floatFromInt(cursor_text.color.r)) / 255.0, @as(f32, @floatFromInt(cursor_text.color.g)) / 255.0, @as(f32, @floatFromInt(cursor_text.color.b)) / 255.0, @@ -2031,7 +2127,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Selection background color if (self.config.selection_background) |selection_bg| { - self.custom_shader_uniforms.selection_background_color = .{ + uniforms.selection_background_color = .{ @as(f32, @floatFromInt(selection_bg.color.r)) / 255.0, @as(f32, @floatFromInt(selection_bg.color.g)) / 255.0, @as(f32, @floatFromInt(selection_bg.color.b)) / 255.0, @@ -2041,13 +2137,21 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Selection foreground color if (self.config.selection_foreground) |selection_fg| { - self.custom_shader_uniforms.selection_foreground_color = .{ + uniforms.selection_foreground_color = .{ @as(f32, @floatFromInt(selection_fg.color.r)) / 255.0, @as(f32, @floatFromInt(selection_fg.color.g)) / 255.0, @as(f32, @floatFromInt(selection_fg.color.b)) / 255.0, 1.0, }; } + + // Cursor visibility + uniforms.cursor_visible = @intFromBool(self.terminal_state.cursor.visible); + + // Cursor style + const cursor_style: renderer.CursorStyle = .fromTerminal(self.terminal_state.cursor.visual_style); + uniforms.previous_cursor_style = uniforms.current_cursor_style; + uniforms.current_cursor_style = @as(i32, @intFromEnum(cursor_style)); } /// Update per-frame custom shader uniforms. @@ -2057,7 +2161,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // We only need to do this if we have custom shaders. if (!self.has_custom_shaders) return; - const uniforms = &self.custom_shader_uniforms; + const uniforms: *shadertoy.Uniforms = &self.custom_shader_uniforms; const now = try std.time.Instant.now(); defer self.last_frame_time = now; @@ -2091,7 +2195,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { 0, }; - // Update custom cursor uniforms, if we have a cursor. if (self.cells.getCursorGlyph()) |cursor| { const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]); const cursor_height: f32 = @floatFromInt(cursor.glyph_size[1]); diff --git a/src/renderer/image.zig b/src/renderer/image.zig index 85f3a01ed72..c43d27981d2 100644 --- a/src/renderer/image.zig +++ b/src/renderer/image.zig @@ -844,7 +844,7 @@ pub const Image = union(enum) { /// Converts the image data to a format that can be uploaded to the GPU. /// If the data is already in a format that can be uploaded, this is a /// no-op. - pub fn convert(self: *Image, alloc: Allocator) wuffs.Error!void { + fn convert(self: *Image, alloc: Allocator) wuffs.Error!void { const p = self.getPendingPointer().?; // As things stand, we currently convert all images to RGBA before // uploading to the GPU. This just makes things easier. In the future @@ -867,7 +867,7 @@ pub const Image = union(enum) { /// Prepare the pending image data for upload to the GPU. /// This doesn't need GPU access so is safe to call any time. - pub fn prepForUpload(self: *Image, alloc: Allocator) wuffs.Error!void { + fn prepForUpload(self: *Image, alloc: Allocator) wuffs.Error!void { assert(self.isPending()); try self.convert(alloc); } diff --git a/src/renderer/link.zig b/src/renderer/link.zig index 74df3e5966f..c5de61574d2 100644 --- a/src/renderer/link.zig +++ b/src/renderer/link.zig @@ -148,7 +148,7 @@ test "renderCellMap" { var s = t.vtStream(); defer s.deinit(); const str = "1ABCD2EFGH\r\n3IJKL"; - try s.nextSlice(str); + s.nextSlice(str); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -201,7 +201,7 @@ test "renderCellMap hover links" { var s = t.vtStream(); defer s.deinit(); const str = "1ABCD2EFGH\r\n3IJKL"; - try s.nextSlice(str); + s.nextSlice(str); var state: terminal.RenderState = .empty; defer state.deinit(alloc); @@ -279,7 +279,7 @@ test "renderCellMap mods no match" { var s = t.vtStream(); defer s.deinit(); const str = "1ABCD2EFGH\r\n3IJKL"; - try s.nextSlice(str); + s.nextSlice(str); var state: terminal.RenderState = .empty; defer state.deinit(alloc); diff --git a/src/renderer/metal/IOSurfaceLayer.zig b/src/renderer/metal/IOSurfaceLayer.zig index 34fbfbed544..93bc99b2db8 100644 --- a/src/renderer/metal/IOSurfaceLayer.zig +++ b/src/renderer/metal/IOSurfaceLayer.zig @@ -77,6 +77,30 @@ pub inline fn setSurface(self: *IOSurfaceLayer, surface: *IOSurface) !void { } } +/// Sets the layer's `contents` to the provided IOSurface without checking size. +/// +/// This is intended for "re-present last frame" paths during resize where we want +/// CoreAnimation to scale the last good surface to cover new bounds (i.e. avoid +/// a transient blank flash) even if the surface dimensions don't match the layer. +pub inline fn setSurfaceUnchecked(self: *IOSurfaceLayer, surface: *IOSurface) !void { + surface.retain(); + + var block = SetSurfaceUncheckedBlock.init(.{ + .layer = self.layer.value, + .surface = surface, + }, &setSurfaceUncheckedCallback); + + const NSThread = objc.getClass("NSThread").?; + if (NSThread.msgSend(bool, "isMainThread", .{})) { + setSurfaceUncheckedCallback(&block); + } else { + macos.dispatch.dispatch_async( + @ptrCast(macos.dispatch.queue.getMain()), + @ptrCast(&block), + ); + } +} + /// Sets the layer's `contents` to the provided IOSurface. /// /// Does not ensure this happens on the main thread. @@ -89,6 +113,11 @@ const SetSurfaceBlock = objc.Block(struct { surface: *IOSurface, }, .{}, void); +const SetSurfaceUncheckedBlock = objc.Block(struct { + layer: objc.c.id, + surface: *IOSurface, +}, .{}, void); + fn setSurfaceCallback( block: *const SetSurfaceBlock.Context, ) callconv(.c) void { @@ -118,6 +147,16 @@ fn setSurfaceCallback( layer.setProperty("contents", surface); } +fn setSurfaceUncheckedCallback( + block: *const SetSurfaceUncheckedBlock.Context, +) callconv(.c) void { + const layer = objc.Object.fromId(block.layer); + const surface: *IOSurface = block.surface; + defer surface.release(); + + layer.setProperty("contents", surface); +} + pub const DisplayCallback = ?*align(8) const fn (?*anyopaque) void; pub fn setDisplayCallback( diff --git a/src/renderer/shaders/shadertoy_prefix.glsl b/src/renderer/shaders/shadertoy_prefix.glsl index 661bd233dfc..4b6d091b853 100644 --- a/src/renderer/shaders/shadertoy_prefix.glsl +++ b/src/renderer/shaders/shadertoy_prefix.glsl @@ -15,6 +15,9 @@ layout(binding = 1, std140) uniform Globals { uniform vec4 iPreviousCursor; uniform vec4 iCurrentCursorColor; uniform vec4 iPreviousCursorColor; + uniform int iCurrentCursorStyle; + uniform int iPreviousCursorStyle; + uniform int iCursorVisible; uniform float iTimeCursorChange; uniform float iTimeFocus; uniform int iFocus; @@ -27,6 +30,12 @@ layout(binding = 1, std140) uniform Globals { uniform vec3 iSelectionBackgroundColor; }; +#define CURSORSTYLE_BLOCK 0 +#define CURSORSTYLE_BLOCK_HOLLOW 1 +#define CURSORSTYLE_BAR 2 +#define CURSORSTYLE_UNDERLINE 3 +#define CURSORSTYLE_LOCK 4 + layout(binding = 0) uniform sampler2D iChannel0; // These are unused currently by Ghostty: diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 7d0ad4b0a31..556c282938c 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -24,6 +24,9 @@ pub const Uniforms = extern struct { previous_cursor: [4]f32 align(16), current_cursor_color: [4]f32 align(16), previous_cursor_color: [4]f32 align(16), + current_cursor_style: i32 align(4), + previous_cursor_style: i32 align(4), + cursor_visible: i32 align(4), cursor_change_time: f32 align(4), time_focus: f32 align(4), focus: i32 align(4), diff --git a/src/renderer/size.zig b/src/renderer/size.zig index d8b529c26c3..e9754d1f3ed 100644 --- a/src/renderer/size.zig +++ b/src/renderer/size.zig @@ -1,5 +1,6 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const configpkg = @import("../config.zig"); const font = @import("../font/main.zig"); const terminal = @import("../terminal/main.zig"); @@ -34,7 +35,11 @@ pub const Size = struct { /// Set the padding to be balanced around the grid. The balanced /// padding is calculated AFTER the explicit padding is taken /// into account. - pub fn balancePadding(self: *Size, explicit: Padding) void { + pub fn balancePadding( + self: *Size, + explicit: Padding, + mode: configpkg.Config.WindowPaddingBalance, + ) void { // This ensure grid() does the right thing self.padding = explicit; @@ -45,14 +50,20 @@ pub const Size = struct { self.cell, ); - // The top/bottom padding is interesting. Subjectively, lots of padding - // at the top looks bad. So instead of always being equal (like left/right), - // we force the top padding to be at most equal to the maximum left padding, - // which is the balanced explicit horizontal padding plus half a cell width. - const max_padding_left = (explicit.left + explicit.right + self.cell.width) / 2; - const vshift = self.padding.top -| max_padding_left; - self.padding.top -= vshift; - self.padding.bottom += vshift; + switch (mode) { + .false => unreachable, + .equal => {}, + .true => { + // Cap the top padding to avoid excessive space above the + // first row. The maximum is the balanced explicit horizontal + // padding plus half a cell width. Any excess is shifted to + // the bottom. + const max_top = (explicit.left + explicit.right + self.cell.width) / 2; + const vshift = self.padding.top -| max_top; + self.padding.top -= vshift; + self.padding.bottom += vshift; + }, + } } }; @@ -302,6 +313,45 @@ pub const Padding = struct { } }; +test "Size.balancePadding equal distributes whitespace equally" { + const testing = std.testing; + + // screen=1050x850, cell=10x20, explicit=4 each side + // grid: (1050-8)/10=104 cols, (850-8)/20=42 rows + // leftover: 1050-1040=10 horizontal, 850-840=10 vertical + // balanced: left=right=5, top=bottom=5 + var size: Size = .{ + .screen = .{ .width = 1050, .height = 850 }, + .cell = .{ .width = 10, .height = 20 }, + .padding = .{}, + }; + size.balancePadding(.{ .top = 4, .bottom = 4, .left = 4, .right = 4 }, .equal); + try testing.expectEqual(size.padding.left, size.padding.right); + try testing.expectEqual(size.padding.top, size.padding.bottom); + try testing.expect(size.padding.top > 0); +} + +test "Size.balancePadding true shifts excess top to bottom" { + const testing = std.testing; + + // screen=1090x1070, cell=20x40, explicit=0 + // grid: 1090/20=54 cols, 1070/40=26 rows + // leftover: 1090-1080=10, 1070-1040=30 + // balanced: left=right=5, top=bottom=15 + // vshift cap: (0+0+20)/2=10, vshift=15-10=5 + // result: top=10, bottom=20 + var size: Size = .{ + .screen = .{ .width = 1090, .height = 1070 }, + .cell = .{ .width = 20, .height = 40 }, + .padding = .{}, + }; + size.balancePadding(.{}, .true); + try testing.expectEqual(size.padding.left, size.padding.right); + try testing.expect(size.padding.top < size.padding.bottom); + try testing.expectEqual(@as(u32, 10), size.padding.top); + try testing.expectEqual(@as(u32, 20), size.padding.bottom); +} + test "Padding balanced on zero" { // On some systems, our screen can be zero-sized for a bit, and we // don't want to end up with negative padding. diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 49d8de4504c..48c89164b28 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -195,20 +195,22 @@ function __ghostty_precmd() { _GHOSTTY_SAVE_PS1="$PS1" _GHOSTTY_SAVE_PS2="$PS2" - # Marks. We need to do fresh line (A) at the beginning of the prompt - # since if the cursor is not at the beginning of a line, the terminal - # will emit a newline. - PS1='\[\e]133;A;redraw=last;cl=line;aid='"$BASHPID"'\a\]'$PS1'\[\e]133;B\a\]' - PS2='\[\e]133;A;k=s\a\]'$PS2'\[\e]133;B\a\]' - - # Bash doesn't redraw the leading lines in a multiline prompt so - # we mark the start of each line (after each newline) as a secondary - # prompt. This correctly handles multiline prompts by setting the first - # to primary and the subsequent lines to secondary. - if [[ "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ]]; then - builtin local __ghostty_mark=$'\\[\\e]133;A;k=s\\a\\]' - PS1="${PS1//$'\n'/$'\n'$__ghostty_mark}" - PS1="${PS1//\\n/\\n$__ghostty_mark}" + # Use 133;P (not 133;A) inside PS1 to avoid fresh-line behavior on + # readline redraws (e.g., vi mode switches, Ctrl-L). The initial + # 133;A with fresh-line is emitted once via printf below. + PS1='\[\e]133;P;k=i\a\]'$PS1'\[\e]133;B\a\]' + PS2='\[\e]133;P;k=s\a\]'$PS2'\[\e]133;B\a\]' + + # Bash doesn't redraw the leading lines in a multiline prompt so we mark + # the start of each line (after each newline) as a secondary prompt. This + # correctly handles multiline prompts by setting the first to primary and + # the subsequent lines to secondary. + # + # We only replace the \n prompt escape, not literal newlines ($'\n'), + # because literal newlines may appear inside $(...) command substitutions + # where inserting escape sequences would break shell syntax. + if [[ "$PS1" == *"\n"* ]]; then + PS1="${PS1//\\n/\\n$'\\[\\e]133;P;k=s\\a\\]'}" fi # Cursor @@ -231,6 +233,9 @@ function __ghostty_precmd() { builtin printf "\e]133;D;%s;aid=%s\a" "$ret" "$BASHPID" fi + # Fresh line and start of prompt. + builtin printf "\e]133;A;redraw=last;cl=line;aid=%s\a" "$BASHPID" + # unfortunately bash provides no hooks to detect cwd changes # in particular this means cwd reporting will not happen for a # command like cd /test && cat. PS0 is evaluated before cd is run. @@ -268,12 +273,15 @@ if (( BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 4) ) # Use function substitution in 5.3+. Otherwise, use command substitution. # Any output (including escape sequences) goes to the terminal. - if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 3) )); then - # shellcheck disable=SC2016 - builtin readonly __ghostty_ps0='${ __ghostty_preexec_hook; }' - else - # shellcheck disable=SC2016 - builtin readonly __ghostty_ps0='$(__ghostty_preexec_hook >/dev/tty)' + # Only define if not already set (allows re-sourcing). + if [[ -z "${__ghostty_ps0+x}" ]]; then + if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 3) )); then + # shellcheck disable=SC2016 + builtin readonly __ghostty_ps0='${ __ghostty_preexec_hook; }' + else + # shellcheck disable=SC2016 + builtin readonly __ghostty_ps0='$(__ghostty_preexec_hook >/dev/tty)' + fi fi __ghostty_hook() { @@ -285,19 +293,19 @@ if (( BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 4) ) } # Append our hook to PROMPT_COMMAND, preserving its existing type. + # shellcheck disable=SC2128,SC2178,SC2179 if [[ ";${PROMPT_COMMAND[*]:-};" != *";__ghostty_hook;"* ]]; then if [[ -z "${PROMPT_COMMAND[*]}" ]]; then if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1) )); then PROMPT_COMMAND=(__ghostty_hook) else - # shellcheck disable=SC2178 PROMPT_COMMAND="__ghostty_hook" fi elif [[ $(builtin declare -p PROMPT_COMMAND 2>/dev/null) == "declare -a "* ]]; then PROMPT_COMMAND+=(__ghostty_hook) else - # shellcheck disable=SC2179 - PROMPT_COMMAND+="; __ghostty_hook" + [[ "${PROMPT_COMMAND}" =~ \;[[:space:]]*$ ]] || PROMPT_COMMAND+=";" + PROMPT_COMMAND+=" __ghostty_hook" fi fi else diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 776aab6760d..87fd0a90211 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -11,15 +11,16 @@ # List of enabled shell integration features var features = [(str:split ',' $E:GHOSTTY_SHELL_FEATURES)] - # helper used by `mark-*` functions + # State tracking for semantic prompt sequences + # Values: 'prompt-start', 'pre-exec', 'post-exec' fn set-prompt-state {|new| set-env __ghostty_prompt_state $new } fn mark-prompt-start { - if (not-eq prompt-start (constantly $E:__ghostty_prompt_state)) { - printf "\e]133;D\a" + if (not-eq $E:__ghostty_prompt_state 'prompt-start') { + printf "\e]133;D;aid="$pid"\a" } set-prompt-state 'prompt-start' - printf "\e]133;A\a" + printf "\e]133;A;aid="$pid"\a" } fn mark-output-start {|_| @@ -44,9 +45,15 @@ } } - printf "\e]133;D;"$exit-status"\a" + printf "\e]133;D;"$exit-status";aid="$pid"\a" } + # NOTE: OSC 133;B (end of prompt, start of input) cannot be reliably + # implemented at the script level in Elvish. The prompt function's output is + # escaped, and writing to /dev/tty has timing issues because Elvish renders + # its prompts on a background thread. Full semantic prompt support requires a + # native implementation: https://github.com/elves/elvish/pull/1917 + fn sudo-with-terminfo {|@args| var sudoedit = $false for arg $args { diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 8cd3dde7a16..668b18057a7 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -121,7 +121,8 @@ _ghostty_deferred_init() { fi fi - builtin local mark1=$'%{\e]133;A;cl=line\a%}' + builtin local markA=$'\e]133;A;cl=line\a' + builtin local mark1=$'%{\e]133;P;k=i\a%}' if [[ -o prompt_percent ]]; then builtin typeset -g precmd_functions if [[ ${precmd_functions[-1]} == _ghostty_precmd ]]; then @@ -131,8 +132,14 @@ _ghostty_deferred_init() { # SIGCHLD if notify is set. Themes that update prompt # asynchronously from a `zle -F` handler might still remove our # marks. Oh well. - builtin local mark2=$'%{\e]133;A;k=s\a%}' + builtin local mark2=$'%{\e]133;P;k=s\a%}' builtin local markB=$'%{\e]133;B\a%}' + if ! builtin zle; then + # Emit a single fresh-line mark for actual new prompts. + # Prompt redraws reuse the in-band P/B markers below so + # async reset-prompt flows don't grow the screen. + builtin print -rnu $_ghostty_fd -- $markA + fi # Add marks conditionally to avoid a situation where we have # several marks in place. These conditions can have false # positives and false negatives though. @@ -141,10 +148,22 @@ _ghostty_deferred_init() { # - False negative (with prompt_subst): PS1='$mark1' [[ $PS1 == *$mark1* ]] || PS1=${mark1}${PS1} [[ $PS1 == *$markB* ]] || PS1=${PS1}${markB} - # Handle multiline prompts by marking continuation lines as - # secondary by replacing newlines with being prefixed - # with k=s - if [[ $PS1 == *$'\n'* ]]; then + # Handle multiline prompts by marking newline-separated + # continuation lines with k=s (mark2). + # + # Pure-style prompts use \n%{\r%} to move back to column 0 + # before the visible prompt line. Ghostty already promotes the + # next row to a prompt continuation when prompt mode crosses a + # newline, so inserting another explicit continuation mark after + # that hidden CR creates a second prompt boundary during redraws. + if [[ $PS1 == *$'\n%{\r%}'* ]]; then + : + elif [[ $PS1 == ${mark1}$'\n'* ]]; then + builtin local rest=${PS1#${mark1}$'\n'} + if [[ $rest == *$'\n'* ]]; then + PS1=${mark1}$'\n'${rest//$'\n'/$'\n'${mark2}} + fi + elif [[ $PS1 == *$'\n'* ]]; then PS1=${PS1//$'\n'/$'\n'${mark2}} fi @@ -166,7 +185,7 @@ _ghostty_deferred_init() { # already have a mark, so the following reset-prompt will write # it. If it doesn't, there is nothing we can do. if ! builtin zle; then - builtin print -rnu $_ghostty_fd -- $mark1[3,-3] + builtin print -rnu $_ghostty_fd -- $markA (( _ghostty_state = 2 )) fi fi @@ -174,7 +193,7 @@ _ghostty_deferred_init() { # Without prompt_percent we cannot patch prompt. Just print the # mark, except when we are invoked from zle. In the latter case we # cannot do anything. - builtin print -rnu $_ghostty_fd -- $mark1[3,-3] + builtin print -rnu $_ghostty_fd -- $markA (( _ghostty_state = 2 )) fi } @@ -189,9 +208,10 @@ _ghostty_deferred_init() { # top. We cannot force prompt_subst on the user though, so we would # still need this code for the no_prompt_subst case. PS1=${PS1//$'%{\e]133;A;cl=line\a%}'} - PS1=${PS1//$'%{\e]133;A;k=s\a%}'} + PS1=${PS1//$'%{\e]133;P;k=i\a%}'} + PS1=${PS1//$'%{\e]133;P;k=s\a%}'} PS1=${PS1//$'%{\e]133;B\a%}'} - PS2=${PS2//$'%{\e]133;A;k=s\a%}'} + PS2=${PS2//$'%{\e]133;P;k=s\a%}'} PS2=${PS2//$'%{\e]133;B\a%}'} # This will work incorrectly in the presence of a preexec hook that @@ -239,6 +259,18 @@ _ghostty_deferred_init() { builtin print -rnu $_ghostty_fd \$'\\e[0 q'" fi + # Emit semantic prompt markers at line-init if PS1 doesn't contain our + # marks. This ensures the terminal sees prompt markers even if another + # plugin (like zinit or oh-my-posh) regenerated PS1 after our precmd ran. + # We use 133;P instead of 133;A to avoid fresh-line behavior which would + # disrupt the display since the prompt has already been drawn. We also + # emit 133;B to mark the input area, which is needed for click-to-move. + (( $+functions[_ghostty_zle_line_init] )) || _ghostty_zle_line_init() { builtin true; } + functions[_ghostty_zle_line_init]=" + if [[ \$PS1 != *$'%{\\e]133;A'* && \$PS1 != *$'%{\\e]133;P'* ]]; then + builtin print -nu \$_ghostty_fd '\\e]133;P;k=i\\a\\e]133;B\\a' + fi + "${functions[_ghostty_zle_line_init]} # 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 @@ -315,7 +347,7 @@ _ghostty_deferred_init() { ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$" ssh_cpath="$ssh_cpath_dir/socket" - if print "$ssh_terminfo" | command ssh "${ssh_opts[@]}" -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" ' + if builtin print -r "$ssh_terminfo" | command ssh "${ssh_opts[@]}" -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" ' infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 command -v tic >/dev/null 2>&1 || exit 1 mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 diff --git a/src/surface_mouse.zig b/src/surface_mouse.zig index 691f1b23cf9..6d3c11394a1 100644 --- a/src/surface_mouse.zig +++ b/src/surface_mouse.zig @@ -16,7 +16,7 @@ const MouseShape = @import("terminal/mouse_shape.zig").MouseShape; physical_key: input.Key, /// The mouse event tracking mode, if any. -mouse_event: terminal.Terminal.MouseEvents, +mouse_event: terminal.Terminal.MouseEvent, /// The current terminal's mouse shape. mouse_shape: MouseShape, diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 71534d0aaea..6e39428dbde 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2154,7 +2154,16 @@ fn resizeWithoutReflowGrowCols( // Unlikely fast path: we have capacity in the page. This // is only true if we resized to less cols earlier. - if (page.capacity.cols >= cols) { + if (page.capacity.cols >= cols) fast: { + // If any row has a spacer head at the old last column, it will + // be invalid at the new (wider) size. Fall through to the slow + // path which handles spacer heads correctly via cloneRowFrom. + const rows = page.rows.ptr(page.memory)[0..page.size.rows]; + for (rows) |*row| { + const cells = page.getCells(row); + if (cells[old_cols - 1].wide == .spacer_head) break :fast; + } + page.size.cols = cols; return; } @@ -2673,11 +2682,22 @@ fn scrollPrompt(self: *PageList, delta: isize) void { // delta so that we don't land back on our current viewport. const start_pin = start: { const tl = self.getTopLeft(.viewport); - const adjusted: ?Pin = if (delta > 0) - tl.down(1) - else - tl.up(1); - break :start adjusted orelse return; + + // If we're moving up we can just move the viewport up because + // promptIterator handles jumpting to the start of prompts. + if (delta <= 0) break :start tl.up(1) orelse return; + + // If we're moving down and we're presently at some kind of + // prompt, we need to skip all the continuation lines because + // promptIterator can't know if we're cutoff or continuing. + var adjusted: Pin = tl.down(1) orelse return; + if (tl.rowAndCell().row.semantic_prompt != .none) skip: { + while (adjusted.rowAndCell().row.semantic_prompt == .prompt_continuation) { + adjusted = adjusted.down(1) orelse break :skip; + } + } + + break :start adjusted; }; // Go through prompts delta times @@ -6857,6 +6877,55 @@ test "Screen: jump back one prompt" { } } +test "Screen: jump forward prompt skips multiline continuation" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 5, 3, null); + defer s.deinit(); + try s.growRows(7); + + // Multiline prompt on rows 1-3. + { + const p = s.pin(.{ .screen = .{ .y = 1 } }).?; + p.rowAndCell().row.semantic_prompt = .prompt; + } + { + const p = s.pin(.{ .screen = .{ .y = 2 } }).?; + p.rowAndCell().row.semantic_prompt = .prompt_continuation; + } + { + const p = s.pin(.{ .screen = .{ .y = 3 } }).?; + p.rowAndCell().row.semantic_prompt = .prompt_continuation; + } + + // Next prompt after command output. + { + const p = s.pin(.{ .screen = .{ .y = 6 } }).?; + p.rowAndCell().row.semantic_prompt = .prompt; + } + + // Starting at the first prompt line should jump to the next prompt, + // not to continuation lines. + s.scroll(.{ .row = 1 }); + s.scroll(.{ .delta_prompt = 1 }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 6, + } }, s.pointFromPin(.screen, s.pin(.{ .viewport = .{} }).?).?); + + // Starting in the middle of continuation lines should also jump to + // the next prompt. + s.scroll(.{ .row = 2 }); + s.scroll(.{ .delta_prompt = 1 }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 6, + } }, s.pointFromPin(.screen, s.pin(.{ .viewport = .{} }).?).?); +} + test "PageList grow fit in capacity" { const testing = std.testing; const alloc = testing.allocator; @@ -10487,6 +10556,78 @@ test "PageList resize (no reflow) more cols with spacer head" { } } +// Regression test for fuzz crash. When we shrink cols and then +// grow back, the page retains capacity from the original size so the grow +// takes the fast path (just bumps page.size.cols). If any row has a +// spacer_head at the old last column, that cell is no longer at the end +// of the wider row, violating page integrity. +test "PageList resize (no reflow) grow cols fast path with spacer head" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 3, 0); + defer s.deinit(); + + // Shrink to 5 cols. The page keeps capacity for 10 cols. + try s.resize(.{ .cols = 5, .reflow = false }); + try testing.expectEqual(@as(usize, 5), s.cols); + + // Place a spacer_head at the last column (col 4) on two rows + // to simulate a wide character that didn't fit at the right edge. + { + const page = &s.pages.first.?.data; + + // Row 0: 'x' at col 0..3, spacer_head at col 4, wrap = true + { + const rac = page.getRowAndCell(0, 0); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 'x' }, + }; + } + { + const rac = page.getRowAndCell(4, 0); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 0 }, + .wide = .spacer_head, + }; + rac.row.wrap = true; + } + + // Row 1: spacer_head at col 4, wrap = true + { + const rac = page.getRowAndCell(4, 1); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 0 }, + .wide = .spacer_head, + }; + rac.row.wrap = true; + } + } + + // Grow back to 10 cols. This must not leave stale spacer_head + // cells at col 4 (which is no longer the last column). + try s.resize(.{ .cols = 10, .reflow = false }); + try testing.expectEqual(@as(usize, 10), s.cols); + + // Verify the old spacer_head positions are now narrow. + { + const page = &s.pages.first.?.data; + { + const rac = page.getRowAndCell(4, 0); + try testing.expectEqual(pagepkg.Cell.Wide.narrow, rac.cell.wide); + try testing.expect(!rac.row.wrap); + } + { + const rac = page.getRowAndCell(4, 1); + try testing.expectEqual(pagepkg.Cell.Wide.narrow, rac.cell.wide); + try testing.expect(!rac.row.wrap); + } + } +} + // This test is a bit convoluted so I want to explain: what we are trying // to verify here is that when we increase cols such that our rows per page // shrinks, we don't fragment our rows across many pages because this ends diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 34a23787f04..88e7edf7c33 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -289,6 +289,8 @@ pub fn next(self: *Parser, c: u8) [3]?Action { break :osc_string null; }, .dcs_passthrough => dcs_hook: { + // Ignore too many parameters + if (self.params_idx >= MAX_PARAMS) break :dcs_hook null; // Finalize parameters if (self.param_acc_idx > 0) { self.params[self.params_idx] = self.param_acc; @@ -1070,3 +1072,28 @@ test "dcs: params" { try testing.expectEqual('p', hook.final); } } + +test "dcs: too many params" { + // Regression test for a crash found by fuzzing (afl). When a DCS + // sequence has more than MAX_PARAMS parameters and param_acc_idx > 0, + // entering dcs_passthrough wrote to params[params_idx] without a + // bounds check, causing an out-of-bounds access. + var p = init(); + _ = p.next(0x1B); // ESC + _ = p.next('P'); // DCS entry + + // Feed a digit then MAX_PARAMS semicolons to fill all param slots. + _ = p.next('6'); + for (0..MAX_PARAMS) |_| { + _ = p.next(';'); + } + // Feed another digit so param_acc_idx > 0 while params_idx == MAX_PARAMS. + _ = p.next('7'); + + // A final byte triggers entry to dcs_passthrough. The DCS should + // be dropped entirely, consistent with how CSI handles overflow. + const a = p.next('p'); + try testing.expect(a[0] == null); + try testing.expect(a[1] == null); + try testing.expect(a[2] == null); +} diff --git a/src/terminal/StringMap.zig b/src/terminal/StringMap.zig index f7d88d1c8cb..18dd7b19c11 100644 --- a/src/terminal/StringMap.zig +++ b/src/terminal/StringMap.zig @@ -11,6 +11,12 @@ const Screen = @import("Screen.zig"); const Pin = @import("PageList.zig").Pin; const Allocator = std.mem.Allocator; +// Retry budget for StringMap regex searches. +// +// Units are Oniguruma retry steps (internal backtracking/retry counter), +// not bytes/characters/time. +const oni_search_retry_limit = 100_000; + string: [:0]const u8, map: []Pin, @@ -44,11 +50,26 @@ pub const SearchIterator = struct { pub fn next(self: *SearchIterator) !?Match { if (self.offset >= self.map.string.len) return null; - var region = self.regex.search( + // Use per-search match params so we can bound regex retry steps + // (Oniguruma's internal backtracking work counter). + var match_param = try oni.MatchParam.init(); + defer match_param.deinit(); + try match_param.setRetryLimitInSearch(oni_search_retry_limit); + + var region = self.regex.searchWithParam( self.map.string[self.offset..], .{}, + &match_param, ) catch |err| switch (err) { - error.Mismatch => { + // Retry/stack-limit errors mean we hit our work budget and + // aborted matching. + // For iterator callers this is equivalent to "no further matches". + error.Mismatch, + error.RetryLimitInMatchOver, + error.RetryLimitInSearchOver, + error.MatchStackLimitOver, + error.SubexpCallLimitInSearchOver, + => { self.offset = self.map.string.len; return null; }, diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 81c21d3b09b..9e21ba97a43 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -5,6 +5,7 @@ const Terminal = @This(); const std = @import("std"); const build_options = @import("terminal_options"); +const lib = @import("../lib/main.zig"); const assert = @import("../quirks.zig").inlineAssert; const testing = std.testing; const Allocator = std.mem.Allocator; @@ -35,6 +36,8 @@ const Page = pagepkg.Page; const Cell = pagepkg.Cell; const Row = pagepkg.Row; +const lib_target: lib.Target = if (build_options.c_abi) .c else .zig; + const log = std.log.scoped(.terminal); /// Default tabstop interval @@ -93,7 +96,7 @@ flags: packed struct { /// set mode in modes. You can't get the right event/format to use /// based on modes alone because modes don't show you what order /// this was called so we have to track it separately. - mouse_event: MouseEvents = .none, + mouse_event: MouseEvent = .none, mouse_format: MouseFormat = .x10, /// Set via the XTSHIFTESCAPE sequence. If true (XTSHIFTESCAPE = 1) @@ -166,7 +169,7 @@ pub const Dirty = packed struct { /// The event types that can be reported for mouse-related activities. /// These are all mutually exclusive (hence in a single enum). -pub const MouseEvents = enum(u3) { +pub const MouseEvent = enum(u3) { none = 0, x10 = 1, // 9 normal = 2, // 1000 @@ -174,7 +177,7 @@ pub const MouseEvents = enum(u3) { any = 4, // 1003 /// Returns true if this event sends motion events. - pub fn motion(self: MouseEvents) bool { + pub fn motion(self: MouseEvent) bool { return self == .button or self == .any; } }; @@ -271,7 +274,7 @@ pub fn vtHandler(self: *Terminal) ReadonlyHandler { } /// The general allocator we should use for this terminal. -fn gpa(self: *Terminal) Allocator { +pub fn gpa(self: *Terminal) Allocator { return self.screens.active.alloc; } @@ -329,12 +332,16 @@ pub fn print(self: *Terminal, c: u21) !void { @branchHint(.unlikely); // We need the previous cell to determine if we're at a grapheme // break or not. If we are NOT, then we are still combining the - // same grapheme. Otherwise, we can stay in this cell. + // same grapheme, and will be appending to prev.cell. Otherwise, we are + // in a new cell. const Prev = struct { cell: *Cell, left: size.CellCountInt }; - const prev: Prev = prev: { + var prev: Prev = prev: { const left: size.CellCountInt = left: { - // If we have wraparound, then we always use the prev col - if (self.modes.get(.wraparound)) break :left 1; + // If we have wraparound, then we use the prev col unless + // there's a pending wrap, in which case we use the current. + if (self.modes.get(.wraparound)) { + break :left @intFromBool(!self.screens.active.cursor.pending_wrap); + } // If we do not have wraparound, the logic is trickier. If // we're not on the last column, then we just use the previous @@ -380,6 +387,8 @@ pub fn print(self: *Terminal, c: u21) !void { // If we can NOT break, this means that "c" is part of a grapheme // with the previous char. if (!grapheme_break) { + var desired_wide: enum { no_change, wide, narrow } = .no_change; + // If this is an emoji variation selector then we need to modify // the cell width accordingly. VS16 makes the character wide and // VS15 makes it narrow. @@ -390,71 +399,138 @@ pub fn print(self: *Terminal, c: u21) !void { if (!prev_props.emoji_vs_base) return; switch (c) { - 0xFE0F => wide: { - if (prev.cell.wide == .wide) break :wide; - - // Move our cursor back to the previous. We'll move - // the cursor within this block to the proper location. - self.screens.active.cursorLeft(prev.left); - - // If we don't have space for the wide char, we need - // to insert spacers and wrap. Then we just print the wide - // char as normal. - if (self.screens.active.cursor.x == right_limit - 1) { - if (!self.modes.get(.wraparound)) return; + 0xFE0F => desired_wide = .wide, + 0xFE0E => desired_wide = .narrow, + else => unreachable, + } + } else if (!unicode.table.get(c).width_zero_in_grapheme) { + // If we have a code point that contributes to the width of a + // grapheme, it necessarily means that we're at least at width + // 2, since the first code point must be at least width 1 to + // start. (Note that Prepend code points could effectively mean + // the first code point should be width 0, but we don't handle + // that yet.) + desired_wide = .wide; + } + + switch (desired_wide) { + .wide => wide: { + if (prev.cell.wide == .wide) break :wide; + + // Move our cursor back to the previous. We'll move + // the cursor within this block to the proper location. + self.screens.active.cursorLeft(prev.left); + + // If we don't have space for the wide char, we need to + // insert spacers and wrap. We need special handling if the + // previous cell has grapheme data. + if (self.screens.active.cursor.x == right_limit - 1) { + if (!self.modes.get(.wraparound)) return; + + // This path can write a spacer_head before printWrap + // which can trigger integrity violations so mark + // the wrap first to keep the intermediary state valid + // if we're wrapping. + const row_wrap = right_limit == self.cols; + if (row_wrap) self.screens.active.cursor.page_row.wrap = true; + + const prev_cp = prev.cell.content.codepoint; + if (prev.cell.hasGrapheme()) { + // This is like printCell but without clearing the + // grapheme data from the cell, so we can move it + // later. + prev.cell.wide = if (row_wrap) .spacer_head else .narrow; + prev.cell.content.codepoint = 0; + + try self.printWrap(); + self.printCell(prev_cp, .wide); + + const new_pin = self.screens.active.cursor.page_pin.*; + const new_rac = new_pin.rowAndCell(); + + transfer_graphemes: { + var old_pin = self.screens.active.cursor.page_pin.up(1) orelse break :transfer_graphemes; + old_pin.x = right_limit - 1; + const old_rac = old_pin.rowAndCell(); + + if (new_pin.node == old_pin.node) { + new_pin.node.data.moveGrapheme(prev.cell, new_rac.cell); + prev.cell.content_tag = .codepoint; + new_rac.cell.content_tag = .codepoint_grapheme; + new_rac.row.grapheme = true; + } else { + const cps = old_pin.node.data.lookupGrapheme(old_rac.cell).?; + for (cps) |cp| { + try self.screens.active.appendGrapheme(new_rac.cell, cp); + } + old_pin.node.data.clearGrapheme(old_rac.cell); + } + + old_pin.node.data.updateRowGraphemeFlag(old_rac.row); + } + + // Point prev.cell to our new previous cell that + // we'll be appending graphemes to + prev.cell = new_rac.cell; + } else { self.printCell( 0, - if (right_limit == self.cols) .spacer_head else .narrow, + if (row_wrap) .spacer_head else .narrow, ); try self.printWrap(); + self.printCell(prev_cp, .wide); + + // Point prev.cell to our new previous cell that + // we'll be appending graphemes to + prev.cell = self.screens.active.cursor.page_cell; } + } else { + prev.cell.wide = .wide; + } - self.printCell(prev.cell.content.codepoint, .wide); + // Write our spacer, since prev.cell is now wide + self.screens.active.cursorRight(1); + self.printCell(0, .spacer_tail); - // Write our spacer + // Move the cursor again so we're beyond our spacer + if (self.screens.active.cursor.x == right_limit - 1) { + self.screens.active.cursor.pending_wrap = true; + } else { self.screens.active.cursorRight(1); - self.printCell(0, .spacer_tail); + } + }, - // Move the cursor again so we're beyond our spacer - if (self.screens.active.cursor.x == right_limit - 1) { - self.screens.active.cursor.pending_wrap = true; - } else { - self.screens.active.cursorRight(1); - } - }, - - 0xFE0E => narrow: { - // Prev cell is no longer wide - if (prev.cell.wide != .wide) break :narrow; - prev.cell.wide = .narrow; - - // Remove the wide spacer tail - const cell = self.screens.active.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.screens.active.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.screens.active.cursor.pending_wrap = false; - } else { - // Otherwise, move back. - self.screens.active.cursorLeft(1); - } + .narrow => narrow: { + // Prev cell is no longer wide + if (prev.cell.wide != .wide) break :narrow; + prev.cell.wide = .narrow; - break :narrow; - }, + // Remove the wide spacer tail + const cell = self.screens.active.cursorCellLeft(prev.left - 1); + cell.wide = .narrow; - else => unreachable, - } + // 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.screens.active.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.screens.active.cursor.pending_wrap = false; + } else { + // Otherwise, move back. + self.screens.active.cursorLeft(1); + } + + break :narrow; + }, + + else => {}, } log.debug("c={X} grapheme attach to left={} primary_cp={X}", .{ @@ -562,7 +638,16 @@ pub fn print(self: *Terminal, c: u21) !void { // We only create a spacer head if we're at the real edge // of the screen. Otherwise, we clear the space with a narrow. // This allows soft wrapping to work correctly. - self.printCell(0, if (right_limit == self.cols) .spacer_head else .narrow); + if (right_limit == self.cols) { + // Special-case: we need to set wrap to true even + // though we call printWrap below because if there is + // a page resize during printCell then it'll fail + // integrity checks. + self.screens.active.cursor.page_row.wrap = true; + self.printCell(0, .spacer_head); + } else { + self.printCell(0, .narrow); + } try self.printWrap(); } @@ -647,9 +732,14 @@ fn printCell( self.screens.active.cursor.page_row, spacer_cell[0..1], ); + + // If we're near the left edge, a wide char may have + // wrapped from the previous row, leaving a spacer_head + // at the end of that row. Clear it so the previous row + // doesn't keep a stale spacer_head. if (self.screens.active.cursor.y > 0 and self.screens.active.cursor.x <= 1) { const head_cell = self.screens.active.cursorCellEndOfPrev(); - head_cell.wide = .narrow; + if (head_cell.wide == .spacer_head) head_cell.wide = .narrow; } }, @@ -668,9 +758,13 @@ fn printCell( self.screens.active.cursor.page_row, wide_cell[0..1], ); + // If we're near the left edge, a wide char may have + // wrapped from the previous row, leaving a spacer_head + // at the end of that row. Clear it so the previous row + // doesn't keep a stale spacer_head. if (self.screens.active.cursor.y > 0 and self.screens.active.cursor.x <= 1) { const head_cell = self.screens.active.cursorCellEndOfPrev(); - head_cell.wide = .narrow; + if (head_cell.wide == .spacer_head) head_cell.wide = .narrow; } }, @@ -1613,7 +1707,7 @@ pub fn scrollUp(self: *Terminal, count: usize) !void { } /// Options for scrolling the viewport of the terminal grid. -pub const ScrollViewport = union(enum) { +pub const ScrollViewport = union(Tag) { /// Scroll to the top of the scrollback top, @@ -1622,10 +1716,27 @@ pub const ScrollViewport = union(enum) { /// Scroll by some delta amount, up is negative. delta: isize, + + pub const Tag = lib.Enum(lib_target, &.{ + "top", + "bottom", + "delta", + }); + + const c_union = lib.TaggedUnion( + lib_target, + @This(), + // Padding: largest variant is isize (8 bytes on 64-bit). + // Use [2]u64 (16 bytes) for future expansion. + [2]u64, + ); + pub const C = c_union.C; + pub const CValue = c_union.CValue; + pub const cval = c_union.cval; }; /// Scroll the viewport of the terminal grid. -pub fn scrollViewport(self: *Terminal, behavior: ScrollViewport) !void { +pub fn scrollViewport(self: *Terminal, behavior: ScrollViewport) void { self.screens.active.scroll(switch (behavior) { .top => .{ .top = {} }, .bottom => .{ .active = {} }, @@ -1880,6 +1991,7 @@ pub fn insertLines(self: *Terminal, count: usize) void { } } else { // Clear the cells for this row, it has been shifted. + self.rowWillBeShifted(&cur_p.node.data, cur_row); const page = &cur_p.node.data; const cells = page.getCells(cur_row); self.screens.active.clearCells( @@ -2067,6 +2179,7 @@ pub fn deleteLines(self: *Terminal, count: usize) void { } } else { // Clear the cells for this row, it's from out of bounds. + self.rowWillBeShifted(&cur_p.node.data, cur_row); const page = &cur_p.node.data; const cells = page.getCells(cur_row); self.screens.active.clearCells( @@ -2099,6 +2212,12 @@ pub fn insertBlanks(self: *Terminal, count: usize) void { // xterm does. self.screens.active.cursor.pending_wrap = false; + // If we're given a zero then we do nothing. The rest of this function + // assumes count > 0 and will crash if zero so return early. Note that + // this shouldn't be possible with real CSI sequences because the value + // is clamped to 1 min. + if (count == 0) return; + // If our cursor is outside the margins then do nothing. We DO reset // wrap state still so this must remain below the above logic. if (self.screens.active.cursor.x < self.scrolling_region.left or @@ -2127,6 +2246,18 @@ pub fn insertBlanks(self: *Terminal, count: usize) void { // Remaining cols from our cursor to the right margin. const rem = self.scrolling_region.right - self.screens.active.cursor.x + 1; + // If the cell at the right margin is wide, its spacer tail is + // outside the scroll region and would be orphaned by either the + // shift or the clear. Clean up both halves up front. + { + const right_cell: *Cell = @ptrCast(left + (rem - 1)); + if (right_cell.wide == .wide) self.screens.active.clearCells( + page, + self.screens.active.cursor.page_row, + @as([*]Cell, @ptrCast(right_cell))[0..2], + ); + } + // We can only insert blanks up to our remaining cols const adjusted_count = @min(count, rem); @@ -3247,6 +3378,44 @@ test "Terminal: print over wide char at 0,0" { try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 1 } })); } +test "Terminal: print over wide char at col 0 corrupts previous row" { + // Crash found by AFL++ fuzzer (afl-out/stream/default/crashes/id:000002). + // + // printCell, when overwriting a wide cell with a narrow cell at x<=1 + // and y>0, sets the last cell of the previous row to .narrow — even + // when that cell is a .spacer_tail rather than a .spacer_head. This + // orphans the .wide cell at cols-2. + const alloc = testing.allocator; + var t = try init(alloc, .{ .cols = 10, .rows = 3 }); + defer t.deinit(alloc); + + // Fill rows 0 and 1 with wide chars (5 per row on a 10-col terminal). + for (0..10) |_| try t.print(0x4E2D); + + // Move cursor to row 1, col 0 (on top of a wide char) and print a + // narrow character. This triggers printCell's .wide branch which + // corrupts row 0's last cell: col 9 changes from .spacer_tail to + // .narrow, orphaning the .wide at col 8. + t.setCursorPos(2, 1); + try t.print('A'); + + // Row 1, col 0 should be narrow (we just overwrote the wide char). + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?; + try testing.expectEqual(Cell.Wide.narrow, list_cell.cell.wide); + } + // Row 0, col 8 should still be .wide (the last wide char on the row). + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 8, .y = 0 } }).?; + try testing.expectEqual(Cell.Wide.wide, list_cell.cell.wide); + } + // Row 0, col 9 must remain .spacer_tail to pair with the .wide at col 8. + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 9, .y = 0 } }).?; + try testing.expectEqual(Cell.Wide.spacer_tail, list_cell.cell.wide); + } +} + test "Terminal: print over wide spacer tail" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator); @@ -3834,19 +4003,23 @@ test "Terminal: print invalid VS15 in emoji ZWJ sequence" { } test "Terminal: VS15 to make narrow character with pending wrap" { - var t = try init(testing.allocator, .{ .rows = 5, .cols = 2 }); + var t = try init(testing.allocator, .{ .rows = 5, .cols = 4 }); defer t.deinit(testing.allocator); // Enable grapheme clustering t.modes.set(.grapheme_cluster, true); + try testing.expect(t.modes.get(.wraparound)); + + try t.print(0x1F34B); // Lemon, width=2 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. + // We only move to the end of the line because we're in a pending wrap + // state. try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); - try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.x); + try testing.expectEqual(@as(usize, 3), t.screens.active.cursor.x); try testing.expect(t.screens.active.cursor.pending_wrap); try t.print(0xFE0E); // VS15 to make narrow @@ -3855,17 +4028,17 @@ test "Terminal: VS15 to make narrow character with pending wrap" { // VS15 should clear the pending wrap state try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); - try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.x); + try testing.expectEqual(@as(usize, 3), t.screens.active.cursor.x); try testing.expect(!t.screens.active.cursor.pending_wrap); { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); - try testing.expectEqualStrings("☔︎", str); + try testing.expectEqualStrings("ðŸ‹â˜”︎", str); } { - const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; const cell = list_cell.cell; try testing.expectEqual(@as(u21, 0x2614), cell.content.codepoint); try testing.expect(cell.hasGrapheme()); @@ -3873,6 +4046,154 @@ test "Terminal: VS15 to make narrow character with pending wrap" { const cps = list_cell.node.data.lookupGrapheme(cell).?; try testing.expectEqual(@as(usize, 1), cps.len); } + + // VS15 should not affect the previous grapheme + { + const lemon_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?.cell; + try testing.expectEqual(@as(u21, 0x1F34B), lemon_cell.content.codepoint); + try testing.expectEqual(Cell.Wide.wide, lemon_cell.wide); + const spacer_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?.cell; + try testing.expectEqual(@as(u21, 0), spacer_cell.content.codepoint); + try testing.expectEqual(Cell.Wide.spacer_tail, spacer_cell.wide); + } +} + +test "Terminal: VS16 to make wide character on next line" { + var t = try init(testing.allocator, .{ .rows = 5, .cols = 3 }); + defer t.deinit(testing.allocator); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + t.cursorRight(2); + try t.print('#'); + try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); + try testing.expect(t.screens.active.cursor.pending_wrap); + try testing.expect(t.isDirty(.{ .screen = .{ .x = 2, .y = 0 } })); + t.clearDirty(); + + try t.print(0xFE0F); // VS16 to make wide + + try testing.expect(t.isDirty(.{ .screen = .{ .x = 2, .y = 0 } })); + t.clearDirty(); + try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y); + try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); + try testing.expect(!t.screens.active.cursor.pending_wrap); + + { + // Previous cell turns into spacer_head + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0), cell.content.codepoint); + try testing.expect(!cell.hasGrapheme()); + try testing.expectEqual(Cell.Wide.spacer_head, cell.wide); + } + { + // '#' cell is wide + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, '#'), cell.content.codepoint); + try testing.expect(cell.hasGrapheme()); + try testing.expectEqualSlices(u21, &.{0xFE0F}, list_cell.node.data.lookupGrapheme(cell).?); + try testing.expectEqual(Cell.Wide.wide, cell.wide); + } + { + // spacer_tail + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 1 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0), cell.content.codepoint); + try testing.expect(!cell.hasGrapheme()); + try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); + } +} + +test "Terminal: VS16 to make wide character on next line with hyperlink" { + // Regression test for the crash fixed in print's grapheme `.wide` path: + // writing a spacer_head at the screen edge before row.wrap was set. + var t = try init(testing.allocator, .{ .rows = 5, .cols = 3 }); + defer t.deinit(testing.allocator); + + // Enable grapheme clustering and activate a hyperlink so printCell + // calls cursorSetHyperlink (which runs page integrity checks). + t.modes.set(.grapheme_cluster, true); + try t.screens.active.startHyperlink("http://example.com", null); + + t.cursorRight(2); + try t.print('#'); + try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); + try testing.expect(t.screens.active.cursor.pending_wrap); + + // Without the fix, this panicked with UnwrappedSpacerHead. + try t.print(0xFE0F); // VS16 to make wide + + try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y); + try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); + try testing.expect(!t.screens.active.cursor.pending_wrap); + + { + // Previous cell turns into spacer_head and remains hyperlinked. + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0), cell.content.codepoint); + try testing.expectEqual(Cell.Wide.spacer_head, cell.wide); + try testing.expect(cell.hyperlink); + try testing.expect(list_cell.row.wrap); + } + { + // '#' cell is now wide and still hyperlinked. + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, '#'), cell.content.codepoint); + try testing.expect(cell.hasGrapheme()); + try testing.expectEqualSlices(u21, &.{0xFE0F}, list_cell.node.data.lookupGrapheme(cell).?); + try testing.expectEqual(Cell.Wide.wide, cell.wide); + try testing.expect(cell.hyperlink); + } + { + // spacer_tail inherits hyperlink as part of the same grapheme cell. + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 1 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0), cell.content.codepoint); + try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); + try testing.expect(cell.hyperlink); + } +} + +test "Terminal: VS16 to make wide character with pending wrap" { + var t = try init(testing.allocator, .{ .rows = 5, .cols = 3 }); + defer t.deinit(testing.allocator); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + t.cursorRight(1); + try t.print('#'); + try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); + try testing.expect(!t.screens.active.cursor.pending_wrap); + + try t.print(0xFE0F); // VS16 to make wide + + try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); + try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); + try testing.expect(t.screens.active.cursor.pending_wrap); + + { + // '#' cell is wide + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, '#'), cell.content.codepoint); + try testing.expect(cell.hasGrapheme()); + try testing.expectEqualSlices(u21, &.{0xFE0F}, list_cell.node.data.lookupGrapheme(cell).?); + try testing.expectEqual(Cell.Wide.wide, cell.wide); + } + { + // spacer_tail + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0), cell.content.codepoint); + try testing.expect(!cell.hasGrapheme()); + try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); + } } test "Terminal: VS16 to make wide character with mode 2027" { @@ -4013,6 +4334,173 @@ test "Terminal: print invalid VS16 with second char" { } } +test "Terminal: print grapheme oÌ€ (o with nonspacing mark) should be narrow" { + 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('o'); + try t.print(0x0300); // combining grave accent + + // We should have 1 cell taken up. + try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); + try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.x); + + // Assert various properties about our screen to verify + // we have all expected cells. + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 'o'), cell.content.codepoint); + try testing.expect(cell.hasGrapheme()); + try testing.expectEqualSlices(u21, &.{0x0300}, list_cell.node.data.lookupGrapheme(cell).?); + try testing.expectEqual(Cell.Wide.narrow, cell.wide); + } +} + +test "Terminal: print Devanagari grapheme should be wide" { + 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(0x0915); + try t.print(0x094D); + try t.print(0x200D); + try t.print(0x0937); + + // We should have 2 cells taken up. It is one character but "wide". + try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); + try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); + + // Assert various properties about our screen to verify + // we have all expected cells. + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0x0915), cell.content.codepoint); + try testing.expect(cell.hasGrapheme()); + try testing.expectEqualSlices(u21, &.{ 0x094D, 0x200D, 0x0937 }, list_cell.node.data.lookupGrapheme(cell).?); + try testing.expectEqual(Cell.Wide.wide, cell.wide); + } + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); + } +} + +test "Terminal: print Devanagari grapheme should be wide on next line" { + var t = try init(testing.allocator, .{ .rows = 5, .cols = 3 }); + defer t.deinit(testing.allocator); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + t.cursorRight(2); + + // कà¥â€à¤· + try t.print(0x0915); + try t.print(0x094D); + try t.print(0x200D); + try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); + try testing.expect(t.screens.active.cursor.pending_wrap); + + // This one increases the width to wide + try t.print(0x0937); + + // We should have 2 cells taken up. It is one character but "wide". + try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y); + try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); + try testing.expect(!t.screens.active.cursor.pending_wrap); + + { + // Previous cell turns into spacer_head + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0), cell.content.codepoint); + try testing.expect(!cell.hasGrapheme()); + try testing.expectEqual(Cell.Wide.spacer_head, cell.wide); + } + { + // Devanagari grapheme is wide + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0x0915), cell.content.codepoint); + try testing.expect(cell.hasGrapheme()); + try testing.expectEqualSlices(u21, &.{ 0x094D, 0x200D, 0x0937 }, list_cell.node.data.lookupGrapheme(cell).?); + try testing.expectEqual(Cell.Wide.wide, cell.wide); + } + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 1 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); + } +} + +test "Terminal: print Devanagari grapheme should be wide on next page" { + const rows = pagepkg.std_capacity.rows; + const cols = pagepkg.std_capacity.cols; + var t = try init(testing.allocator, .{ .rows = rows, .cols = cols }); + defer t.deinit(testing.allocator); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + t.cursorDown(rows - 1); + + for (rows..t.screens.active.pages.pages.first.?.data.capacity.rows) |_| { + try t.index(); + } + + t.cursorRight(cols - 1); + + try testing.expectEqual(cols - 1, t.screens.active.cursor.x); + try testing.expectEqual(rows - 1, t.screens.active.cursor.y); + + // कà¥â€à¤· + try t.print(0x0915); + try t.print(0x094D); + try t.print(0x200D); + try testing.expectEqual(cols - 1, t.screens.active.cursor.x); + try testing.expect(t.screens.active.cursor.pending_wrap); + + // This one increases the width to wide + try t.print(0x0937); + + // We should have 2 cells taken up. It is one character but "wide". + try testing.expectEqual(rows - 1, t.screens.active.cursor.y); + try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); + try testing.expect(!t.screens.active.cursor.pending_wrap); + + { + // Previous cell turns into spacer_head + const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = cols - 1, .y = rows - 2 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0), cell.content.codepoint); + try testing.expect(!cell.hasGrapheme()); + try testing.expectEqual(Cell.Wide.spacer_head, cell.wide); + } + { + // Devanagari grapheme is wide + const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = rows - 1 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0x0915), cell.content.codepoint); + try testing.expect(cell.hasGrapheme()); + try testing.expectEqualSlices(u21, &.{ 0x094D, 0x200D, 0x0937 }, list_cell.node.data.lookupGrapheme(cell).?); + try testing.expectEqual(Cell.Wide.wide, cell.wide); + } + { + const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 1, .y = rows - 1 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); + } +} + test "Terminal: print invalid VS16 with second char (combining)" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator); @@ -4772,6 +5260,50 @@ test "Terminal: overwrite hyperlink" { try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); } +// Printing a wide char at the right edge with an active hyperlink causes +// printCell to write a spacer_head before printWrap sets the row wrap +// flag. The integrity check inside setHyperlink (or increaseCapacity) +// sees the unwrapped spacer head and panics. Found via fuzzing. +test "Terminal: print wide char at right edge with hyperlink" { + var t = try init(testing.allocator, .{ .cols = 10, .rows = 5 }); + defer t.deinit(testing.allocator); + + try t.screens.active.startHyperlink("http://example.com", null); + + // Move cursor to the last column (1-indexed) + t.setCursorPos(1, 10); + + // Print a wide character; this will call printCell(0, .spacer_head) + // at the right edge before calling printWrap, triggering the + // integrity violation. + try t.print(0x4E2D); // U+4E2D '中' + + // Cursor wraps to row 2, after the wide char + spacer tail + try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y); + try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); + + // Row 0, col 9: spacer head with hyperlink + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 9, .y = 0 } }).?; + try testing.expectEqual(Cell.Wide.spacer_head, list_cell.cell.wide); + try testing.expect(list_cell.cell.hyperlink); + try testing.expect(list_cell.row.wrap); + } + // Row 1, col 0: the wide char with hyperlink + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?; + try testing.expectEqual(@as(u21, 0x4E2D), list_cell.cell.content.codepoint); + try testing.expectEqual(Cell.Wide.wide, list_cell.cell.wide); + try testing.expect(list_cell.cell.hyperlink); + } + // Row 1, col 1: spacer tail with hyperlink + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 1 } }).?; + try testing.expectEqual(Cell.Wide.spacer_tail, list_cell.cell.wide); + try testing.expect(list_cell.cell.hyperlink); + } +} + test "Terminal: linefeed and carriage return" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator); @@ -9075,6 +9607,25 @@ test "Terminal: DECALN resets graphemes with protected mode" { } } +test "Terminal: insertBlanks zero" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .cols = 5, .rows = 2 }); + defer t.deinit(alloc); + + try t.print('A'); + try t.print('B'); + try t.print('C'); + t.setCursorPos(1, 1); + + t.insertBlanks(0); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC", str); + } +} + test "Terminal: insertBlanks" { // NOTE: this is not verified with conformance tests, so these // tests might actually be verifying wrong behavior. @@ -9466,6 +10017,77 @@ test "Terminal: insertBlanks pushes hyperlink off end completely" { } } +test "Terminal: insertBlanks wide char straddling right margin" { + // Crash found by AFL++ fuzzer. + // + // When a wide character straddles the right scroll margin (head at the + // margin, spacer_tail just beyond it), insertBlanks shifts the wide head + // away via swapCells but leaves the orphaned spacer_tail in place, + // causing a page integrity violation. + const alloc = testing.allocator; + var t = try init(alloc, .{ .cols = 10, .rows = 5 }); + defer t.deinit(alloc); + + // Fill row: A B C D æ©‹ _ _ _ _ _ + // Positions: 0 1 2 3 4W 5T 6 7 8 9 + t.setCursorPos(1, 1); + for ("ABCD") |c| try t.print(c); + try t.print('æ©‹'); // wide char: head at 4, spacer_tail at 5 + + // Set right margin so the wide head is AT the boundary and the + // spacer_tail is just outside it. + t.scrolling_region.right = 4; + + // Position cursor at x=2 (1-indexed col 3) and insert one blank. + // This triggers the swap loop which displaces the wide head at + // position 4 without clearing the spacer_tail at position 5. + t.setCursorPos(1, 3); + t.insertBlanks(1); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("AB CD", str); + } +} + +test "Terminal: insertBlanks wide char spacer_tail orphaned beyond right margin" { + // Regression test for AFL++ crash. + // + // When insertBlanks clears the entire region from cursor to the right + // margin (scroll_amount == 0), a wide character whose head is AT the + // right margin gets cleared but its spacer_tail just beyond the margin + // is left behind, causing a page integrity violation: + // "spacer tail not following wide" + const alloc = testing.allocator; + var t = try init(alloc, .{ .cols = 10, .rows = 5 }); + defer t.deinit(alloc); + + // Fill cols 0–9 with wide chars: 中中中中中 + // Positions: 0W 1T 2W 3T 4W 5T 6W 7T 8W 9T + for (0..5) |_| try t.print(0x4E2D); + + // Set left/right margins so that the last wide char (cols 8–9) + // straddles the boundary: head at col 8 (inside), tail at col 9 (outside). + t.modes.set(.enable_left_and_right_margin, true); + t.setLeftAndRightMargin(1, 9); // 1-indexed: left=0, right=8 + + // Cursor is now at (0, 0) after DECSLRM. Print a narrow char to + // advance cursor to col 1. + try t.print('a'); + + // ICH 8: insert 8 blanks at cursor x=1. + // rem = right(8) - x(1) + 1 = 8, adjusted_count = 8, scroll_amount = 0. + // The code clears cols 1–8 without noticing the spacer_tail at col 9. + t.insertBlanks(8); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("a", str); + } +} + test "Terminal: insert mode with space" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 2 }); @@ -12429,3 +13051,32 @@ test "Terminal: mode 1049 alt screen plain" { try testing.expectEqualStrings("", str); } } + +// Reproduces a crash found by AFL++ fuzzer (afl-out/stream/default/crashes/ +// id:000007,sig:06,src:004522). The crash is a page integrity violation +// "spacer tail not following wide" triggered during scrollUp -> deleteLines +// -> clearCells. When deleteLines count >= scroll region height, all rows +// are cleared (no shifting), so rowWillBeShifted is never called and wide +// characters straddling the right margin boundary leave orphaned spacer_tails. +test "Terminal: deleteLines wide char at right margin with full clear" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .cols = 80, .rows = 24 }); + defer t.deinit(alloc); + + // Place a wide character at col 39 (1-indexed) on several rows. + // The wide cell lands at col 38 (0-indexed) with spacer_tail at col 39. + t.setCursorPos(10, 39); + try t.print(0x4E2D); // '中' + + // Set left/right scroll margins so scrolling_region.right = 38. + // clearCells will clear cells[4..39], which includes the wide cell + // at col 38 but NOT the spacer_tail at col 39. + t.modes.set(.enable_left_and_right_margin, true); + t.setLeftAndRightMargin(5, 39); + + // scrollUp with count >= region height causes deleteLines to clear + // ALL rows without any shifting, so rowWillBeShifted is never called + // and the orphaned spacer_tail at col 39 triggers a page integrity + // violation in clearCells. + try t.scrollUp(t.rows); +} diff --git a/src/terminal/c/AGENTS.md b/src/terminal/c/AGENTS.md new file mode 100644 index 00000000000..63f7fc6cc83 --- /dev/null +++ b/src/terminal/c/AGENTS.md @@ -0,0 +1,22 @@ +# libghostty-vt C API + +- C API must be designed with ABI compatibility in mind +- Zig tagged unions must be converted to C ABI compatible unions + via `lib.TaggedUnion`. +- Any functions must be updated all the way through from here to + `src/terminal/c/main.zig` to `src/lib_vt.zig` and the headers + in `include/ghostty/vt.h`. +- In `include/ghostty/vt.h`, always sort the header contents by: + (1) macros, (2) forward declarations, (3) types, (4) functions + +## ABI Compatibility + +- Prefer opaque pointers for long-lived objects, such as + `GhosttyTerminal`. +- Structs: + - May contain padding bytes if we're confident we'll never grow + beyond a certain size. + - May use the "sized struct" pattern: an `extern struct` with + `size: usize = @sizeOf(Self)` as the first field. In the C header, + callers use `GHOSTTY_INIT_SIZED` from `types.h` to zero-initialize and + set the size. diff --git a/src/terminal/c/formatter.zig b/src/terminal/c/formatter.zig new file mode 100644 index 00000000000..511d371f807 --- /dev/null +++ b/src/terminal/c/formatter.zig @@ -0,0 +1,417 @@ +const std = @import("std"); +const testing = std.testing; +const lib_alloc = @import("../../lib/allocator.zig"); +const CAllocator = lib_alloc.Allocator; +const terminal_c = @import("terminal.zig"); +const ZigTerminal = @import("../Terminal.zig"); +const formatterpkg = @import("../formatter.zig"); +const Result = @import("result.zig").Result; + +/// Wrapper around formatter that tracks the allocator for C API usage. +const FormatterWrapper = struct { + kind: Kind, + alloc: std.mem.Allocator, + + const Kind = union(enum) { + terminal: formatterpkg.TerminalFormatter, + }; +}; + +/// C: GhosttyFormatter +pub const Formatter = ?*FormatterWrapper; + +/// C: GhosttyFormatterFormat +pub const Format = formatterpkg.Format; + +/// C: GhosttyFormatterScreenOptions +pub const ScreenOptions = extern struct { + /// C: GhosttyFormatterScreenExtra + pub const Extra = extern struct { + size: usize = @sizeOf(Extra), + cursor: bool, + style: bool, + hyperlink: bool, + protection: bool, + kitty_keyboard: bool, + charsets: bool, + + comptime { + for (std.meta.fieldNames(formatterpkg.ScreenFormatter.Extra)) |name| { + if (!@hasField(Extra, name)) + @compileError("ScreenOptions.Extra missing field: " ++ name); + } + } + + fn toZig(self: Extra) formatterpkg.ScreenFormatter.Extra { + return .{ + .cursor = self.cursor, + .style = self.style, + .hyperlink = self.hyperlink, + .protection = self.protection, + .kitty_keyboard = self.kitty_keyboard, + .charsets = self.charsets, + }; + } + }; +}; + +/// C: GhosttyFormatterTerminalOptions +pub const TerminalOptions = extern struct { + size: usize = @sizeOf(TerminalOptions), + emit: Format, + unwrap: bool, + trim: bool, + extra: Extra, + + /// C: GhosttyFormatterTerminalExtra + pub const Extra = extern struct { + size: usize = @sizeOf(Extra), + palette: bool, + modes: bool, + scrolling_region: bool, + tabstops: bool, + pwd: bool, + keyboard: bool, + screen: ScreenOptions.Extra, + + comptime { + for (std.meta.fieldNames(formatterpkg.TerminalFormatter.Extra)) |name| { + if (!@hasField(Extra, name)) + @compileError("TerminalOptions.Extra missing field: " ++ name); + } + } + + fn toZig(self: Extra) formatterpkg.TerminalFormatter.Extra { + return .{ + .palette = self.palette, + .modes = self.modes, + .scrolling_region = self.scrolling_region, + .tabstops = self.tabstops, + .pwd = self.pwd, + .keyboard = self.keyboard, + .screen = self.screen.toZig(), + }; + } + }; +}; + +pub fn terminal_new( + alloc_: ?*const CAllocator, + result: *Formatter, + terminal_: terminal_c.Terminal, + opts: TerminalOptions, +) callconv(.c) Result { + result.* = terminal_new_( + alloc_, + terminal_, + opts, + ) catch |err| { + result.* = null; + return switch (err) { + error.InvalidValue => .invalid_value, + error.OutOfMemory => .out_of_memory, + }; + }; + + return .success; +} + +fn terminal_new_( + alloc_: ?*const CAllocator, + terminal_: terminal_c.Terminal, + opts: TerminalOptions, +) error{ + InvalidValue, + OutOfMemory, +}!*FormatterWrapper { + const t = terminal_ orelse return error.InvalidValue; + + const alloc = lib_alloc.default(alloc_); + const ptr = alloc.create(FormatterWrapper) catch + return error.OutOfMemory; + errdefer alloc.destroy(ptr); + + var formatter: formatterpkg.TerminalFormatter = .init(t, .{ + .emit = opts.emit, + .unwrap = opts.unwrap, + .trim = opts.trim, + }); + formatter.extra = opts.extra.toZig(); + + ptr.* = .{ + .kind = .{ .terminal = formatter }, + .alloc = alloc, + }; + + return ptr; +} + +pub fn format_buf( + formatter_: Formatter, + out_: ?[*]u8, + out_len: usize, + out_written: *usize, +) callconv(.c) Result { + const wrapper = formatter_ orelse return .invalid_value; + + var writer: std.Io.Writer = .fixed(if (out_) |out| + out[0..out_len] + else + &.{}); + + switch (wrapper.kind) { + .terminal => |*t| t.format(&writer) catch |err| switch (err) { + error.WriteFailed => { + // On write failed we always report how much + // space we actually needed. + var discarding: std.Io.Writer.Discarding = .init(&.{}); + t.format(&discarding.writer) catch unreachable; + out_written.* = @intCast(discarding.count); + return .out_of_space; + }, + }, + } + + out_written.* = writer.end; + return .success; +} + +pub fn format_alloc( + formatter_: Formatter, + alloc_: ?*const CAllocator, + out_ptr: *?[*]u8, + out_len: *usize, +) callconv(.c) Result { + const wrapper = formatter_ orelse return .invalid_value; + const alloc = lib_alloc.default(alloc_); + + var aw: std.Io.Writer.Allocating = .init(alloc); + defer aw.deinit(); + + switch (wrapper.kind) { + .terminal => |*t| t.format(&aw.writer) catch return .out_of_memory, + } + + const buf = aw.toOwnedSlice() catch return .out_of_memory; + out_ptr.* = buf.ptr; + out_len.* = buf.len; + return .success; +} + +pub fn free(formatter_: Formatter) callconv(.c) void { + const wrapper = formatter_ orelse return; + const alloc = wrapper.alloc; + alloc.destroy(wrapper); +} + +test "terminal_new/free" { + var t: terminal_c.Terminal = null; + try testing.expectEqual(Result.success, terminal_c.new( + &lib_alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + )); + defer terminal_c.free(t); + + var f: Formatter = null; + try testing.expectEqual(Result.success, terminal_new( + &lib_alloc.test_allocator, + &f, + t, + .{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } }, + )); + try testing.expect(f != null); + free(f); +} + +test "terminal_new invalid_value on null terminal" { + var f: Formatter = null; + try testing.expectEqual(Result.invalid_value, terminal_new( + &lib_alloc.test_allocator, + &f, + null, + .{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } }, + )); + try testing.expect(f == null); +} + +test "free null" { + free(null); +} + +test "format plain" { + var t: terminal_c.Terminal = null; + try testing.expectEqual(Result.success, terminal_c.new( + &lib_alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + )); + defer terminal_c.free(t); + + terminal_c.vt_write(t, "Hello", 5); + + var f: Formatter = null; + try testing.expectEqual(Result.success, terminal_new( + &lib_alloc.test_allocator, + &f, + t, + .{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } }, + )); + defer free(f); + + var buf: [1024]u8 = undefined; + var written: usize = 0; + try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written)); + try testing.expectEqualStrings("Hello", buf[0..written]); +} + +test "format reflects terminal changes" { + var t: terminal_c.Terminal = null; + try testing.expectEqual(Result.success, terminal_c.new( + &lib_alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + )); + defer terminal_c.free(t); + + terminal_c.vt_write(t, "Hello", 5); + + var f: Formatter = null; + try testing.expectEqual(Result.success, terminal_new( + &lib_alloc.test_allocator, + &f, + t, + .{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } }, + )); + defer free(f); + + var buf: [1024]u8 = undefined; + var written: usize = 0; + try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written)); + try testing.expectEqualStrings("Hello", buf[0..written]); + + // Write more data and re-format + terminal_c.vt_write(t, "\r\nWorld", 7); + + try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written)); + try testing.expectEqualStrings("Hello\nWorld", buf[0..written]); +} + +test "format null returns required size" { + var t: terminal_c.Terminal = null; + try testing.expectEqual(Result.success, terminal_c.new( + &lib_alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + )); + defer terminal_c.free(t); + + terminal_c.vt_write(t, "Hello", 5); + + var f: Formatter = null; + try testing.expectEqual(Result.success, terminal_new( + &lib_alloc.test_allocator, + &f, + t, + .{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } }, + )); + defer free(f); + + // Pass null buffer to query required size + var required: usize = 0; + try testing.expectEqual(Result.out_of_space, format_buf(f, null, 0, &required)); + try testing.expect(required > 0); + + // Now allocate and format + var buf: [1024]u8 = undefined; + var written: usize = 0; + try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written)); + try testing.expectEqual(required, written); +} + +test "format buffer too small" { + var t: terminal_c.Terminal = null; + try testing.expectEqual(Result.success, terminal_c.new( + &lib_alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + )); + defer terminal_c.free(t); + + terminal_c.vt_write(t, "Hello", 5); + + var f: Formatter = null; + try testing.expectEqual(Result.success, terminal_new( + &lib_alloc.test_allocator, + &f, + t, + .{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } }, + )); + defer free(f); + + // Buffer too small + var buf: [2]u8 = undefined; + var written: usize = 0; + try testing.expectEqual(Result.out_of_space, format_buf(f, &buf, buf.len, &written)); + // written contains the required size + try testing.expectEqual(@as(usize, 5), written); +} + +test "format null formatter" { + var written: usize = 0; + try testing.expectEqual(Result.invalid_value, format_buf(null, null, 0, &written)); +} + +test "format vt" { + var t: terminal_c.Terminal = null; + try testing.expectEqual(Result.success, terminal_c.new( + &lib_alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + )); + defer terminal_c.free(t); + + terminal_c.vt_write(t, "Test", 4); + + var f: Formatter = null; + try testing.expectEqual(Result.success, terminal_new( + &lib_alloc.test_allocator, + &f, + t, + .{ .emit = .vt, .unwrap = false, .trim = true, .extra = .{ .palette = true, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = true, .hyperlink = true, .protection = false, .kitty_keyboard = false, .charsets = false } } }, + )); + defer free(f); + + var buf: [65536]u8 = undefined; + var written: usize = 0; + try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written)); + try testing.expect(written > 0); + try testing.expect(std.mem.indexOf(u8, buf[0..written], "Test") != null); +} + +test "format html" { + var t: terminal_c.Terminal = null; + try testing.expectEqual(Result.success, terminal_c.new( + &lib_alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + )); + defer terminal_c.free(t); + + terminal_c.vt_write(t, "Html", 4); + + var f: Formatter = null; + try testing.expectEqual(Result.success, terminal_new( + &lib_alloc.test_allocator, + &f, + t, + .{ .emit = .html, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } }, + )); + defer free(f); + + var buf: [65536]u8 = undefined; + var written: usize = 0; + try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written)); + try testing.expect(written > 0); + try testing.expect(std.mem.indexOf(u8, buf[0..written], "Html") != null); +} diff --git a/src/terminal/c/key_encode.zig b/src/terminal/c/key_encode.zig index 063cd8df7e8..e83c9b22170 100644 --- a/src/terminal/c/key_encode.zig +++ b/src/terminal/c/key_encode.zig @@ -8,6 +8,7 @@ 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; +const Terminal = @import("terminal.zig").Terminal; const log = std.log.scoped(.key_encode); @@ -115,6 +116,15 @@ fn setoptTyped( } } +pub fn setopt_from_terminal( + encoder_: Encoder, + terminal_: Terminal, +) callconv(.c) void { + const wrapper = encoder_ orelse return; + const t = terminal_ orelse return; + wrapper.opts = .fromTerminal(t); +} + pub fn encode( encoder_: Encoder, event_: KeyEvent, @@ -222,6 +232,64 @@ test "setopt macos option as alt" { try testing.expectEqual(OptionAsAlt.true, e.?.opts.macos_option_as_alt); } +test "setopt_from_terminal" { + const testing = std.testing; + const terminal_c = @import("terminal.zig"); + + // Create encoder + var e: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Create terminal + var t: Terminal = undefined; + try testing.expectEqual(Result.success, terminal_c.new( + &lib_alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + )); + defer terminal_c.free(t); + + // Apply terminal state to encoder + setopt_from_terminal(e, t); + + // Options should reflect defaults from a fresh terminal + try testing.expect(!e.?.opts.cursor_key_application); + try testing.expect(!e.?.opts.alt_esc_prefix); + try testing.expectEqual(KittyFlags.disabled, e.?.opts.kitty_flags); + try testing.expectEqual(OptionAsAlt.false, e.?.opts.macos_option_as_alt); +} + +test "setopt_from_terminal null" { + // Both null should be no-ops + setopt_from_terminal(null, null); + + const testing = std.testing; + + // Encoder null with valid terminal + const terminal_c = @import("terminal.zig"); + var t: Terminal = undefined; + try testing.expectEqual(Result.success, terminal_c.new( + &lib_alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + )); + defer terminal_c.free(t); + setopt_from_terminal(null, t); + + // Valid encoder with null terminal + var e: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + setopt_from_terminal(e, null); +} + test "encode: kitty ctrl release with ctrl mod set" { const testing = std.testing; diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index bc92597f5f4..7842866dc10 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -1,9 +1,11 @@ pub const color = @import("color.zig"); +pub const formatter = @import("formatter.zig"); 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"); pub const sgr = @import("sgr.zig"); +pub const terminal = @import("terminal.zig"); // The full C API, unexported. pub const osc_new = osc.new; @@ -16,6 +18,11 @@ pub const osc_command_data = osc.commandData; pub const color_rgb_get = color.rgb_get; +pub const formatter_terminal_new = formatter.terminal_new; +pub const formatter_format_buf = formatter.format_buf; +pub const formatter_format_alloc = formatter.format_alloc; +pub const formatter_free = formatter.free; + pub const sgr_new = sgr.new; pub const sgr_free = sgr.free; pub const sgr_reset = sgr.reset; @@ -48,17 +55,27 @@ 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_setopt_from_terminal = key_encode.setopt_from_terminal; pub const key_encoder_encode = key_encode.encode; pub const paste_is_safe = paste.is_safe; +pub const terminal_new = terminal.new; +pub const terminal_free = terminal.free; +pub const terminal_reset = terminal.reset; +pub const terminal_resize = terminal.resize; +pub const terminal_vt_write = terminal.vt_write; +pub const terminal_scroll_viewport = terminal.scroll_viewport; + test { _ = color; + _ = formatter; _ = osc; _ = key_event; _ = key_encode; _ = paste; _ = sgr; + _ = terminal; // We want to make sure we run the tests for the C allocator interface. _ = @import("../../lib/allocator.zig"); diff --git a/src/terminal/c/result.zig b/src/terminal/c/result.zig index e9b5fc5e6c6..b76326e46d7 100644 --- a/src/terminal/c/result.zig +++ b/src/terminal/c/result.zig @@ -3,4 +3,5 @@ pub const Result = enum(c_int) { success = 0, out_of_memory = -1, invalid_value = -2, + out_of_space = -3, }; diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig new file mode 100644 index 00000000000..0af791f91cc --- /dev/null +++ b/src/terminal/c/terminal.zig @@ -0,0 +1,293 @@ +const std = @import("std"); +const testing = std.testing; +const lib_alloc = @import("../../lib/allocator.zig"); +const CAllocator = lib_alloc.Allocator; +const ZigTerminal = @import("../Terminal.zig"); +const size = @import("../size.zig"); +const Result = @import("result.zig").Result; + +/// C: GhosttyTerminal +pub const Terminal = ?*ZigTerminal; + +/// C: GhosttyTerminalOptions +pub const Options = extern struct { + cols: size.CellCountInt, + rows: size.CellCountInt, + max_scrollback: usize, +}; + +const NewError = error{ + InvalidValue, + OutOfMemory, +}; + +pub fn new( + alloc_: ?*const CAllocator, + result: *Terminal, + opts: Options, +) callconv(.c) Result { + result.* = new_(alloc_, opts) catch |err| { + result.* = null; + return switch (err) { + error.InvalidValue => .invalid_value, + error.OutOfMemory => .out_of_memory, + }; + }; + + return .success; +} + +fn new_( + alloc_: ?*const CAllocator, + opts: Options, +) NewError!*ZigTerminal { + if (opts.cols == 0 or opts.rows == 0) return error.InvalidValue; + + const alloc = lib_alloc.default(alloc_); + const ptr = alloc.create(ZigTerminal) catch + return error.OutOfMemory; + errdefer alloc.destroy(ptr); + + ptr.* = try .init(alloc, .{ + .cols = opts.cols, + .rows = opts.rows, + .max_scrollback = opts.max_scrollback, + }); + + return ptr; +} + +pub fn vt_write( + terminal_: Terminal, + ptr: [*]const u8, + len: usize, +) callconv(.c) void { + const t = terminal_ orelse return; + var stream = t.vtStream(); + stream.nextSlice(ptr[0..len]); +} + +/// C: GhosttyTerminalScrollViewport +pub const ScrollViewport = ZigTerminal.ScrollViewport.C; + +pub fn scroll_viewport( + terminal_: Terminal, + behavior: ScrollViewport, +) callconv(.c) void { + const t = terminal_ orelse return; + t.scrollViewport(switch (behavior.tag) { + .top => .top, + .bottom => .bottom, + .delta => .{ .delta = behavior.value.delta }, + }); +} + +pub fn resize( + terminal_: Terminal, + cols: size.CellCountInt, + rows: size.CellCountInt, +) callconv(.c) Result { + const t = terminal_ orelse return .invalid_value; + if (cols == 0 or rows == 0) return .invalid_value; + t.resize(t.gpa(), cols, rows) catch return .out_of_memory; + return .success; +} + +pub fn reset(terminal_: Terminal) callconv(.c) void { + const t = terminal_ orelse return; + t.fullReset(); +} + +pub fn free(terminal_: Terminal) callconv(.c) void { + const t = terminal_ orelse return; + + const alloc = t.gpa(); + t.deinit(alloc); + alloc.destroy(t); +} + +test "new/free" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &t, + .{ + .cols = 80, + .rows = 24, + .max_scrollback = 10_000, + }, + )); + + try testing.expect(t != null); + free(t); +} + +test "new invalid value" { + var t: Terminal = null; + + try testing.expectEqual(Result.invalid_value, new( + &lib_alloc.test_allocator, + &t, + .{ + .cols = 0, + .rows = 24, + .max_scrollback = 10_000, + }, + )); + try testing.expect(t == null); + + try testing.expectEqual(Result.invalid_value, new( + &lib_alloc.test_allocator, + &t, + .{ + .cols = 80, + .rows = 0, + .max_scrollback = 10_000, + }, + )); + try testing.expect(t == null); +} + +test "free null" { + free(null); +} + +test "scroll_viewport" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &t, + .{ + .cols = 5, + .rows = 2, + .max_scrollback = 10_000, + }, + )); + defer free(t); + + const zt = t.?; + + // Write "hello" on the first line + vt_write(t, "hello", 5); + + // Push "hello" into scrollback with 3 newlines (index = ESC D) + vt_write(t, "\x1bD\x1bD\x1bD", 6); + { + // Viewport should be empty now since hello scrolled off + const str = try zt.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("", str); + } + + // Scroll to top: "hello" should be visible again + scroll_viewport(t, .{ .tag = .top, .value = undefined }); + { + const str = try zt.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("hello", str); + } + + // Scroll to bottom: viewport should be empty again + scroll_viewport(t, .{ .tag = .bottom, .value = undefined }); + { + const str = try zt.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("", str); + } + + // Scroll up by delta to bring "hello" back into view + scroll_viewport(t, .{ .tag = .delta, .value = .{ .delta = -3 } }); + { + const str = try zt.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("hello", str); + } +} + +test "scroll_viewport null" { + scroll_viewport(null, .{ .tag = .top, .value = undefined }); +} + +test "reset" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &t, + .{ + .cols = 80, + .rows = 24, + .max_scrollback = 10_000, + }, + )); + defer free(t); + + vt_write(t, "Hello", 5); + reset(t); + + const str = try t.?.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("", str); +} + +test "reset null" { + reset(null); +} + +test "resize" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &t, + .{ + .cols = 80, + .rows = 24, + .max_scrollback = 10_000, + }, + )); + defer free(t); + + try testing.expectEqual(Result.success, resize(t, 40, 12)); + try testing.expectEqual(40, t.?.cols); + try testing.expectEqual(12, t.?.rows); +} + +test "resize null" { + try testing.expectEqual(Result.invalid_value, resize(null, 80, 24)); +} + +test "resize invalid value" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &t, + .{ + .cols = 80, + .rows = 24, + .max_scrollback = 10_000, + }, + )); + defer free(t); + + try testing.expectEqual(Result.invalid_value, resize(t, 0, 24)); + try testing.expectEqual(Result.invalid_value, resize(t, 80, 0)); +} + +test "vt_write" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &t, + .{ + .cols = 80, + .rows = 24, + .max_scrollback = 10_000, + }, + )); + defer free(t); + + vt_write(t, "Hello", 5); + + const str = try t.?.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("Hello", str); +} diff --git a/src/terminal/color.zig b/src/terminal/color.zig index 483d65e28c5..3b806f8b837 100644 --- a/src/terminal/color.zig +++ b/src/terminal/color.zig @@ -90,14 +90,31 @@ pub fn generate256Color( skip: PaletteMask, bg: RGB, fg: RGB, + harmonious: bool, ) Palette { // Convert the background, foreground, and 8 base theme colors into // CIELAB space so that all interpolation is perceptually uniform. - const bg_lab: LAB = .fromRgb(bg); - const fg_lab: LAB = .fromRgb(fg); const base8_lab: [8]LAB = base8: { - var base8: [8]LAB = undefined; - for (0..8) |i| base8[i] = .fromRgb(base[i]); + var base8: [8]LAB = .{ + .fromRgb(bg), + LAB.fromRgb(base[1]), + LAB.fromRgb(base[2]), + LAB.fromRgb(base[3]), + LAB.fromRgb(base[4]), + LAB.fromRgb(base[5]), + LAB.fromRgb(base[6]), + .fromRgb(fg), + }; + + // For light themes (where the foreground is darker than the + // background), the cube's dark-to-light orientation is inverted + // relative to the base color mapping. When `harmonious` is false, + // swap bg and fg so the cube still runs from black (16) to + // white (231). + const is_light_theme = base8[7].l < base8[0].l; + const invert = is_light_theme and !harmonious; + if (invert) std.mem.swap(LAB, &base8[0], &base8[7]); + break :base8 base8; }; @@ -115,10 +132,10 @@ pub fn generate256Color( for (0..6) |ri| { // R-axis corners: blend base colors along the red dimension. const tr = @as(f32, @floatFromInt(ri)) / 5.0; - const c0: LAB = .lerp(tr, bg_lab, base8_lab[1]); + const c0: LAB = .lerp(tr, base8_lab[0], base8_lab[1]); const c1: LAB = .lerp(tr, base8_lab[2], base8_lab[3]); const c2: LAB = .lerp(tr, base8_lab[4], base8_lab[5]); - const c3: LAB = .lerp(tr, base8_lab[6], fg_lab); + const c3: LAB = .lerp(tr, base8_lab[6], base8_lab[7]); for (0..6) |gi| { // G-axis edges: blend the R-interpolated corners along green. const tg = @as(f32, @floatFromInt(gi)) / 5.0; @@ -147,7 +164,7 @@ pub fn generate256Color( for (0..24) |i| { const t = @as(f32, @floatFromInt(i + 1)) / 25.0; if (!skip.isSet(idx)) { - const c: LAB = .lerp(t, bg_lab, fg_lab); + const c: LAB = .lerp(t, base8_lab[0], base8_lab[7]); result[idx] = c.toRgb(); } idx += 1; @@ -926,7 +943,7 @@ test "generate256Color: base16 preserved" { const bg = RGB{ .r = 0, .g = 0, .b = 0 }; const fg = RGB{ .r = 255, .g = 255, .b = 255 }; - const palette = generate256Color(default, .initEmpty(), bg, fg); + const palette = generate256Color(default, .initEmpty(), bg, fg, false); // The first 16 colors (base16) must remain unchanged. for (0..16) |i| { @@ -939,7 +956,7 @@ test "generate256Color: cube corners match base colors" { const bg = RGB{ .r = 0, .g = 0, .b = 0 }; const fg = RGB{ .r = 255, .g = 255, .b = 255 }; - const palette = generate256Color(default, .initEmpty(), bg, fg); + const palette = generate256Color(default, .initEmpty(), bg, fg, false); // Index 16 is cube (0,0,0) which should equal bg. try testing.expectEqual(bg, palette[16]); @@ -948,12 +965,43 @@ test "generate256Color: cube corners match base colors" { try testing.expectEqual(fg, palette[231]); } +test "generate256Color: cube corners black/white with harmonious=false" { + const testing = std.testing; + + const black = RGB{ .r = 0, .g = 0, .b = 0 }; + const white = RGB{ .r = 255, .g = 255, .b = 255 }; + + // Dark theme: bg=black, fg=white. + const dark = generate256Color(default, .initEmpty(), black, white, false); + try testing.expectEqual(black, dark[16]); + try testing.expectEqual(white, dark[231]); + + // Light theme: bg=white, fg=black. The bg/red swap ensures + // the cube still runs from black (16) to white (231). + const light = generate256Color(default, .initEmpty(), white, black, false); + try testing.expectEqual(black, light[16]); + try testing.expectEqual(white, light[231]); +} + +test "generate256Color: light theme cube corners with harmonious=true" { + const testing = std.testing; + + const white = RGB{ .r = 255, .g = 255, .b = 255 }; + const black = RGB{ .r = 0, .g = 0, .b = 0 }; + + // harmonious=true skips the bg/fg swap, so the cube preserves the + // original orientation: (0,0,0)=bg=white, (5,5,5)=fg=black. + const palette = generate256Color(default, .initEmpty(), white, black, true); + try testing.expectEqual(white, palette[16]); + try testing.expectEqual(black, palette[231]); +} + test "generate256Color: grayscale ramp monotonic luminance" { const testing = std.testing; const bg = RGB{ .r = 0, .g = 0, .b = 0 }; const fg = RGB{ .r = 255, .g = 255, .b = 255 }; - const palette = generate256Color(default, .initEmpty(), bg, fg); + const palette = generate256Color(default, .initEmpty(), bg, fg, false); // The grayscale ramp (232–255) should have monotonically increasing // luminance from near-black to near-white. @@ -977,7 +1025,7 @@ test "generate256Color: skip mask preserves original colors" { skip.set(100); skip.set(240); - const palette = generate256Color(default, skip, bg, fg); + const palette = generate256Color(default, skip, bg, fg, false); try testing.expectEqual(default[20], palette[20]); try testing.expectEqual(default[100], palette[100]); try testing.expectEqual(default[240], palette[240]); @@ -986,6 +1034,73 @@ test "generate256Color: skip mask preserves original colors" { try testing.expect(!palette[21].eql(default[21])); } +test "generate256Color: dark theme harmonious has no effect" { + const testing = std.testing; + + // For a dark theme (fg lighter than bg), harmonious should not change + // the output because the inversion is only relevant for light themes. + const bg = RGB{ .r = 0, .g = 0, .b = 0 }; + const fg = RGB{ .r = 255, .g = 255, .b = 255 }; + const normal = generate256Color(default, .initEmpty(), bg, fg, false); + const harmonious = generate256Color(default, .initEmpty(), bg, fg, true); + + for (16..256) |i| { + try testing.expectEqual(normal[i], harmonious[i]); + } +} + +test "generate256Color: light theme harmonious skips inversion" { + const testing = std.testing; + + // For a light theme (fg darker than bg), harmonious=true skips the + // bg/red swap, producing different cube colors than harmonious=false. + const bg = RGB{ .r = 255, .g = 255, .b = 255 }; + const fg = RGB{ .r = 0, .g = 0, .b = 0 }; + const inverted = generate256Color(default, .initEmpty(), bg, fg, false); + const harmonious = generate256Color(default, .initEmpty(), bg, fg, true); + + // Cube origin (0,0,0) at index 16: without harmonious, bg and red are + // swapped so it becomes the red base; with harmonious it stays as bg. + try testing.expectEqual(bg, harmonious[16]); + try testing.expect(!inverted[16].eql(bg)); + + // At least some cube colors should differ between the two modes. + var differ: usize = 0; + for (16..232) |i| { + if (!inverted[i].eql(harmonious[i])) differ += 1; + } + try testing.expect(differ > 0); +} + +test "generate256Color: light theme harmonious grayscale ramp" { + const testing = std.testing; + + const bg = RGB{ .r = 255, .g = 255, .b = 255 }; + const fg = RGB{ .r = 0, .g = 0, .b = 0 }; + + // harmonious=false swaps bg/fg, so the ramp runs black→white (increasing). + { + const palette = generate256Color(default, .initEmpty(), bg, fg, false); + var prev_lum: f64 = 0.0; + for (232..256) |i| { + const lum = palette[i].luminance(); + try testing.expect(lum >= prev_lum); + prev_lum = lum; + } + } + + // harmonious=true keeps original order, so the ramp runs white→black (decreasing). + { + const palette = generate256Color(default, .initEmpty(), bg, fg, true); + var prev_lum: f64 = 1.0; + for (232..256) |i| { + const lum = palette[i].luminance(); + try testing.expect(lum <= prev_lum); + prev_lum = lum; + } + } +} + test "LAB.toRgb" { const testing = std.testing; diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index 062e3969a27..4da7248e3a2 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -1,5 +1,8 @@ const std = @import("std"); +const build_options = @import("terminal_options"); const assert = @import("../quirks.zig").inlineAssert; +const lib = @import("../lib/main.zig"); +const lib_target: lib.Target = if (build_options.c_abi) .c else .zig; const Allocator = std.mem.Allocator; const color = @import("color.zig"); const size = @import("size.zig"); @@ -19,46 +22,47 @@ const Selection = @import("Selection.zig"); const Style = @import("style.zig").Style; /// Formats available. -pub const Format = enum { - /// Plain text. - plain, - - /// Include VT sequences to preserve colors, styles, URLs, etc. - /// This is predominantly SGR sequences but may contain others as needed. - /// - /// Note that for reference colors, like palette indices, this will - /// vary based on the formatter and you should see the docs. For example, - /// PageFormatter with VT will emit SGR sequences with palette indices, - /// not the color itself. - /// - /// For VT, newlines will be emitted as `\r\n` so that the cursor properly - /// moves back to the beginning prior emitting follow-up lines. - vt, - - /// HTML output. - /// - /// This will emit inline styles for as much styling as possible, - /// in the interest of simplicity and ease of editing. This isn't meant - /// to build the most beautiful or efficient HTML, but rather to be - /// stylistically correct. - /// - /// For colors, RGB values are emitted as inline CSS (#RRGGBB) while palette - /// indices use CSS variables (var(--vt-palette-N)). The palette colors are - /// emitted by TerminalFormatter.Extra.palette as a